nssctf比赛的wp(只有pwn,线上赛,有使用AI)
看到这个什么新生赛过来参加一下,用用最新的方法论,找找感觉
第一题:nc签到?黑盒
只需nc的签到题,简简单单…%p%p%p%s….
这道题就是TMD老叟戏顽童!!多的不说,请看VCR:

懂我意思?真心换真心,耍我耍了半天。没招,AI大人出来吧
哦,这是一个格式化字符串漏洞。
题目提示:只需nc的签到题,简简单单…%p%p%p%s….
其实关键是在理解%p和%s是干什么的
它们是 printf 的占位符,各有分工:
%p—— 把栈上的值当作指针(地址),以十六进制打印出来。你连发一堆%p,相当于把栈从低到高一格一格 dump 出来。%s—— 把栈上的值当作一个指针,然后解引用,去那个地址把字符串读出来。
正常的printf书写:printf("%s", input);有这个漏洞的源码大概率是printf(input);
这二者的区别在于,第一个:%s是命令-我要打印字符串,input-从哪儿来的字符串;第二个:直接把input当作命令,而当我们输入%p时,%p就是命令
所以思路就简单了:利用%p看看栈里面有什么,会有一堆十六进制数。这道题目flag就直接在栈里面,被直接输出了,再通过p64()转为ASCII 字符
但是ai大人提醒,还有一个稍微复杂一点的情况,就是实际上flag不直接在栈里面,只是flag的地址在栈里面,我们就需要利用好%s去flag的地址,%1$s %2$s……直到出现flag
回看这道题,感觉试图用“真心”拿到flag的我好愚蠢
第二道:creeper?
史蒂夫弄丢了他的钻石镐,你能帮他重新做一把吗?
这道题目是我目前见过最难的,Canary found NX enabled PIE enabled,不过我今天也学了一个强大的逆向工具IDA(在文章的末尾放上了一些新学的IDA知识)
在IDA里先看看main函数

锁定sub_12A9和sub_152A
发现sub_12A9是一个初始化函数

所以重点还是在 sub_152A这个主要函数上
先读懂main函数里都发生了什么
v1 = sub_1317(),v1是sub_1317函数的返回值 |

我能发现到diamond <= 2或dword_4014 <= 1的情况下,会输出Not enough!
而只有在diamond >=3和dword_4014 >= 2时,才会输出Craft success
从而调用 sub_14CB,里面有 system("$0"),拿到 shell
我们发现题目是存在一个赢的方法,那为什么要做呢?一定是在游戏的某个策略导致无法同时满足diamond >=3和dword_4014 >= 2,那这个题目的思路就清晰了,去前面调用过的函数里面找到这个漏洞,解决它
sub_1317
首先看sub_1317

其实就是读一行输入 → 转成整数 → 返回给菜单做选择
缓冲区 s[40],输入fgets(s, n: 32, stream: stdin) == nullptr,无栈溢出
sub_144B
看sub_144B(调用sub_137B的函数)

如果dword_404C > 127,程序崩溃,
<127,开一个线程执行a1–传进来的函数sub_137B
sub_137B
看sub_137B

我们发现了什么!
在dword_4018 == 1的时候,我们多了一个钻石,但是dword_4018又被赋了0,也就是说这个函数只能执行一遍,我们就获得了一个钻石
但事情还有转机,usleep(useconds: 0x186A0u),可能是为了确保我们的输入能被机器读取到这里有一个0.1秒的停顿,也就是,sub_137B函数在这0.1秒内无法把dword_4018赋为0,而我们的主程序是没有停顿的,因为sub_144B开了一个线程调用sub_137B,如果此时我们再输入9个1,那么在这个0.1秒之后,有几个钻石要增加?10个!
注:
usleep(useconds: 0x186A0u)指的是停留0.1秒,0x186A0 = 十进制 100000,usleep 的单位是微秒
sub_13E3
同理看sub_13E3

同样的方法,我们的木棍也有了。
现在我们已经找到成功的办法了,写一个文件,使得在0.1秒内输入大量1

总结:这个漏洞叫竞态条件(Race Condition),具体是TOCTOU(检查时刻≠使用时刻)
第三题:never_arrive
又大又小,该怎么办啊喵?
canary found NX enabled PIE enabled
在IDA里看main函数

先是一些初始化操作,然后是一个if语句
关键就是这一步,只要满足(int)vuln() <= 255,就会执行system(command: "/bin/sh"),拿到shell
我们一起去看一下vuln函数做了什么

_QWORD v1[2]:一个名为 v1 的数组,里面有 2 个元素,每个元素占 8 字节(64 位)v1[1] = __readfsqword(0x28u):存储stack canary__isoc99_scanf(a1: "%lld", v1):读取用户输入
这个条件判断也很奇诡,如果v1[0] <= 255LL,程序崩溃,如果v1[0] > 255LL,返回v1[0]的值,但在主函数里就不满足(int)vuln() <= 255
Why?这不就形成一个悖论了吗?仔细寻找是否有什么地方存在漏洞。
思考一下,为什么同样的一个数,在main函数的判断里是<= 255的,而在vuln函数的判断里是> 255LL
没招,我问了一下Claude,让他给我一个停止我这个思考的最小提示:
注意两个比较的「类型/位宽」不一样。vuln 里是
v1[0] > 255LL(LL = 64 位 long long),而 main 里是(int)vuln() <= 255((int)= 32 位)。同一个数,用 64 位看和用 32 位看,可以是两个完全不同的值。
豁然开朗,我们只要输入一个数,在64位的时候> 255LL,而在低32位时<= 255
比如:0x0000000100000000,在低32位时是0x00000000。
还有一个小细节:在scanf里读取的是%lld。十进制数,直接输入0x0000000100000000是不行的
写exploit:


结束,完胜到手!
IDA知识:
- 按 N 给变量改名
- 按 X 键(交叉引用),它会列出所有用到这个变量的地方
- 按F5,把IDA View-A(汇编视图)反编译为Pseudocode(伪代码视图)。