moectf 2025 pwn wp
文章中主要提供解题思路,详细WP见github仓库
syslock
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
关键源码
i = input();
if ( i > 4 )
lose();
write(1, "Input your password\n", 0x14u);
read(0, (char *)&s + i, 0xCu);
if ( i != 59 )
lose();
cheat();
i在收到一个输入之后,会判断这个值是否大于4,然后又在s+i位置写入,只要i==59就能进入cheat()函数。我们观察bss段,发现s和i只隔了0x20,刚好是在s的反方向可以通过第一个绕过。所以我们输入-32,并将i的值修改为59,就能够进入cheat()。

cheat()是一个栈溢出。然后思考怎么利用,这题没有bin/sh的字符串,思考为ret2libc,但是题目又没有给libc,说明有其他的解法。仔细观察上面的s字符串,写入的时候限制长度为0xC,我们完全可以将/bin/sh写入,构造ret2systcall
- 把系统调用(64位为3B,59其实也提醒了我们)的编号存入RAX
- 把函数参数存入其它通用寄存器(第一 、 二 、 三参数分别在 rdi 、rsi 、rdx中储存)bin/sh的地址就存在
0x404084 - 触发中断(int 0x80,syscall)
使用ROPgadget寻找,发现正好有送参数的找到之后,将参数送进去即可



xdulacker

这道题目开了PIE,但是没有栈溢出保护。
# main
init(argc, argv, envp);
menu();
while ( 1 )
{
while ( 1 )
{
putchar(62);
__isoc99_scanf("%d", &opt);
if ( opt != 1 )
break;
pull();
}
if ( opt == 2 )
{
photo();
}
else
{
if ( opt != 3 )
exit(0);
laker();
}
}
菜单会给你三个选项,然后第三个函数lacker是一个栈溢出,然后还有后门函数,所以考虑利用栈溢出来返回到后门。
ssize_t laker()
{
_BYTE s1[48]; // [rsp+0h] [rbp-30h] BYREF
if ( memcmp(s1, "xdulaker", 8u) )
{
puts("You are not him.");
exit(0);
}
puts("welcome,xdulaker");
return read(0, s1, 0x100u);
}
这个函数用了未初始化的栈空间。而photo函数也是对栈空间进行写,所以可以利用未清理的栈空间来对这个s1赋值。计算一下偏移是32,所以就调用photo将特定值赋值,然后这时候不会被清理,再次调用laker就可以绕过验证了。
同时因为开了PIE,所以要计算后门函数的地址,正好pull函数给我泄露了地址,计算一下偏移就可以得到后门函数的地址。为了避免栈对齐,可以跳过push rbp跳转到后门
int photo()
{
_BYTE buf[80]; // [rsp+0h] [rbp-50h] BYREF
puts("Hey,what's your name?!");
read(0, buf, 0x40u);
return puts("I will teach you a lesson.");
}
int pull()
{
return printf("Thanks,I'll give you a gift:%p\n", &opt);
}
ezlibc

代码很简单,main函数里面泄露了read函数的地址,vuln是一个简单的栈溢出,我们利用泄露的地址构造下payload即可。
int __fastcall main(int argc, const char **argv, const char **envp)
{
setbuf(stdout, 0);
printf("What is this?\nHow can I use %p without a backdoor? Damn!\n", &read);
vuln();
puts("Something happening");
return 0;
}
ssize_t vuln()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
return read(0, buf, 0x60u);
}
看了一下好像不太对。官方题解下面是这么说的。简单来说在动态连接这个过程中,最开始泄露的地址并非是read的真实地址,而且是plt中的指令地址。可以参考一篇博客Linux延迟绑定机制过程。
这个题,在最开始的时候,我给你打印了read在got中的地址,这个题的初衷是希望大家对于动态链接和延迟绑定能有一个更好的理解,最开始got中read的表项填写的是read函数plt中的一条指令的地址,当第一次调用read之后,完成了地址解析,此时got中的地址就变成了read函数在libc中的真实地址,此时就可以完成libc地址的泄露,很多同学在做的时候,出现的一个坑是,他们知道要重新执行一次这个printf,但是他们返回错地方了,在printf调用之前,有好几条指令都是在完成参数的装载,而他们返回到了装载指令之后,导致参数出现问题,出现了地址泄露不稳定等一系列的问题,希望大家注意这一点。
理解这个延迟绑定的过程之后,可以知道最开始打印的read函数的地址是,0x1060这个地址。根据这个就可以算出偏移,再返回到main函数再次泄露read地址。从而泄露libc的地址。
注意第一次执行返回地址时候有一个坑。

没有找到rdi,还可以在libc里面找。

