ISCTF2025-PWN方向wp
ISCTF2025-pwn方向wp
来签个到吧
按小端序发送出去就行。
exp:
1 | from pwn import * |
ret2rop
直接就是栈溢出,要控制好覆盖时候覆盖的i和v3,利用xor。
exp:
1 | from pwn import * |
heap?
虽然第一眼看起来很像堆,但其实是一个格式化字符串漏洞 + 栈溢出,泄露libc的后面就是ret2libc
exp:
1 | from pwn import * |
2048
final()存在整数溢出,只需要扣到负数就能进入shell(),先利用printf的%s覆盖Canary的最低一字节来泄露canary,再在程序输入名字的地方输入”/bin/sh”就能直接打。
1 | if ( (unsigned int)score <= 0x1869F ) |
1 | __int64 shell() |
shell()这里会检查输入的最后一位字节是否是回车’\n’,如果是就将’\n’置为0,那咱们不发’\n’不就行了
1 | if ( v1 > 0 && *((_BYTE *)buf + v1 - 1) == 10 ) |
exp:
1 | from pwn import * |
ez_fmt
存在格式化字符串漏洞,分别泄露canary和libc_base,打ret2libc
exp:
1 | from pwn import * |
ez_tcache
Ubuntu GLIBC 2.29-0ubuntu2,打house of botcake就能double free
exp:
1 | from pwn import * |
金丝雀的诱惑
还是利用printf,第一次覆盖Canary的最低一字节来泄露canary,第二次还可以泄露libc,但这里我需要ret一下,栈对齐才能成功再次进入vuln()。
exp:
1 | from pwn import * |
接下来的三道是后续学习复现出来的,我会写的相对详细点。
ez_stack
这道题感觉主要难在逆向分析,还原函数的运行逻辑上,搞明白主要的sub()函数是在干什么就不难做出来

sysread()允许我们向rwx地址0x114514000里写入16字节的数据,但具体应该写入什么需要继续分析下去,下面的leak()这里还给了我们PIE基址和栈地址

虽然附件保护全开,且开启了沙箱(常规ORW即可绕过,这里就不贴沙箱具体规则了),但是stack_overflow()这里并不需要管canary,因为并没有检查canary,但是如果修改了返回地址,程序就会调用sys_exit退出,而在这里发现stack_overflow()正常退出时会进行leave_ret,回到main()之后又会进行leave_ret,两次连续的leave_ret就让我们想到栈迁移


所以利用栈迁移跳转到0x114514000上面,利用前面给的机会写入shellcode来调用read(),二次读入,利用第二段shellcode构造ORW来获取flag
exp:
1 | from pwn import * |
myvm
IDA中恢复结构体
IDA反汇编之后本来是这样的


这样也能分析,感觉问题不大,但是在恢复结构体之后,会更加清晰直观,更舒服
在IDA里的Local Types界面里右键添加自定义结构体

手动恢复结构体需要进行尝试,判断出大致正确的结构体内容,这里官方wp给出了源码,直接照着写进去

点击想要改变数据类型的变量按Y,把这里的_WORD改为Vm_code

就会变成这样

在main()函数进行同样的操作,如果没改变,就按F5刷新一下

就会变成这样

而官方源码如下:

会发现恢复结构体后,到这里我们的伪代码已经和源码长得非常相近了,非常方便阅读。
当vm_pwn越过了逆向部分,其实后面的操作就简单了,恢复指令集,利用指令对栈上进行操作,这里可以栈溢出,拿libc_base,构造rop然后case 9终止main(),执行ORW,我在脚本里写了比较详细的注释。
exp:
1 | from pwn import * |
bad_box
题目无附件,盲打,靶机存在格式化字符串漏洞,但有个坑点是当输入长度不足32个字节时,会采用write()来打印我们的输入,当输出长度超过32个字符时,才会用printf来打印我们的输入。

可以看到,我们格式化字符串的位置是第8个参数,并从一个特殊的数据判断出rbp的位置,这个特殊的数据就是0x3e0523432d0e2d00,根据canary的特征,我们可以猜测它就是canary,那么canary的下一个参数就是rbp = 0x1,在下一个参数就是放回地址0x7063ab146d90,这是main()的返回地址,因为main()的返回地址是一个libc地址,而main()就在0x401275(这里能得到靶机中的附件是未开启PIE保护的),然后利用%s来泄露出main()部分的汇编,leak.py脚本是靠ai写的
1 | from pwn import * |
将泄露出的bin文件简单编译成汇编如下
1 | ┌─[icyice@icyice-virtual-machine] - [~/Desktop/2025xinshengsai/IS/bad_box] - [10014] |
接下来需要尝试泄露出符号表,这一步需要随便打开一个程序来推测符号表在哪里,
拿ez_fmt和ez_stack的题目附件示例:


它们的符号表分别在0x550和0x480处,那这里就大概是0x400550或0x400480左右,尝试并修正一下位置,泄露出

而有了符号表以后再结合之前的靶机连接结果和main()里对应的参数设置可以尽可能还原出main()
1 | 00401275 F30F1EFA endbr64 |
我们发现符号表里有system,但是main()里并未调用,那就猜测是否存在后门函数
那就在main()附近去泄露,看看能不能找到backdoor(),这里采取从main()往前预测,即0x401275-0x100开始泄露
将泄露出来的结果编译成简单的汇编
1 | ┌─[icyice@icyice-virtual-machine] - [~/Desktop/2025xinshengsai/IS/bad_box] - [10024] |
发现这个函数只调用了一个函数,并且该函数只设置了rdi寄存器
1 | 0040125B F30F1EFA endbr64 |
想一想最简单的ret2text里的后门函数里的长相:
1 | void backdoor(){ |
猜测这就是后门函数,去看看rdi里传的参数是什么,也就是利用%s看看0x402004出的字符串

发现是/bin/sh,这就验证了我们对后门函数的猜测。
我们可以想到GOT表劫持,但如何去找GOT表地址呢?我们在main()里可以看到call 某函数@plt,而PLT表里就存有GOT表地址。
由于只有一次性的格式化字符串机会,我们选择劫持exit@got到后门函数,main()里给了exit@plt = 0x401100
1 | 0040132D BF00000000 mov edi,0x0 |
泄露出来

最后的exp:
1 | from pwn import * |








