首页
论坛
课程
招聘
[原创][新手向] 使用AFL来fuzz upx
2019-1-28 17:14 21027

[原创][新手向] 使用AFL来fuzz upx

2019-1-28 17:14
21027

目录

本文首先简单介绍一下AFL的基本使用方法,随后以upx为例记录一下使用AFL来fuzz upx的过程,最后以其中一个典型的crash做简单分析。水文一篇,希望和对fuzz感兴趣的同学一起交流。

0x01 AFL下载和安装

AFL可以对有源码和无源码的程序进行fuzz。对有源码的程序Fuzz的原理,简单来说即是在程序编译时,向汇编代码中插入自己的指令,从而在程序运行时,计算覆盖率。当把样本喂给程序来Fuzz时,如果AFL发现程序执行了新的路径,就把当前的样本保存在Queue中,基于这个新的样本来继续Fuzz。

 

下载和安装AFL:

wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
tar xvf afl-latest.tgz
cd afl-2.52b
# make && make install
sudo make && sudo make install

安装完成后,默认在/usr/local/bin/目录下,修改.bashrc加入环境变量export PATH=/usr/local/bin:$PATH后即可使用。

0x02 AFL简单使用

这里以一个demo实践一下afl的用法,首先编写一个需要fuzz的程序,让AFL去fuzz,查看fuzz的效果。源码:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void test (char *buf) {
    int n = 0;
    if(buf[0] == 'a') n++;
    if(buf[1] == 'f') n++;
    if(buf[2] == 'l') n++;
    if(buf[3] == '!') n++;

    if(n == 4) {
    printf("awesome!\n");
        raise(SIGSEGV);
    }else{
        printf("wrong!\n");
    }
}

int main(int argc, char *argv[]) {
    char buf[100];
    test(argv[1]);
    return 0;
}

编译代码:

afl-gcc afl-case1.c -o afl-case1

首先验证一下程序的功能完好:打开一个内容为afl!的文件时,程序退出了。

 

image-20181225153050613

 

接下来我们创建in和out目录,在in目录中随机生成一些数据,这些样本用作AFL的输入。

 

image-20181225153635294

 

使用AFL开始fuzz,指定输入目录in和输出目录out,@@用于替换输入的参数。

afl-fuzz -i in -o out ./afl-case1 @@

运行后,AFL的fuzz界面如下:

 

image-20181225151615247

 

我们可以看到得到了两个crash,内容见下面的图片。

 

image-20181225151734234

 

第一个afl!明显符合我们的预期,第二个经过调试得知是由于溢出导致的。(为了不影响行为流畅,分析过程请参考文末栈溢出分析。)

0x03 fuzz upx

了解了基本用法之后,选择一些实际的,易出crash的软件来fuzz。这里以upx为例。

upx 简单介绍

upx是一个不同可执行文件的打包软件,最新版本截至26 Aug 2018是3.95。

  • 官网 https://upx.github.io/
  • github repo https://github.com/upx/upx

ubuntu源中的upx通过apt-cache showpkg upx看到版本是3.9.1的,我们直接下载git中的最新版本来安装。

 

在ubuntu16.04的docker中下载最新版本的和编译安装,步骤如下:

upx编译和安装

编译和安装的脚本如下:

mkdir fuzz-upx
git clone https://github.com/upx/upx.git

# 修改Makefile中的CC和G++指定用afl来编译

cd upx
git submodule update --init --recursive # install lzma-sdk

# compile ucl  
wget http://www.oberhumer.com/opensource/ucl/download/ucl-1.03.tar.gz
tar zxvf ucl-1.03.tar.gz
cd ucl-1.03
export CC=/usr/bin/gcc
export CXX=/usr/bin/g++
./configure
sudo make
sudo make install

# 编译
j fuzz-upx
cd upx
export CC=/usr/local/bin/afl-gcc 
export CXX=/usr/local/bin/afl-g++
# export CC=/usr/local/bin/afl-clang-fast
# export CC=/usr/local/bin/afl-clang-fast++
export UPX_UCCLDIR="$PWD/ucl-1.03"# change for youself
export UPX_LZMADIR="$PWD/src/lzma-sdk"
make all

# 输出binary在 src/upx.out
j fuzz-upx
./upx/src/upx.out

export PATH=$PWD/upx/src/:$PATH

编译完成后,查看upx的版本如下:

 

image-20181225162108885

开始fuzz

在fuzz之前,为了实现更多的的代码覆盖率,我们需要收集一些不同的binary来放到afl的输入目录。AFL的样本选择对于Fuzz的效果至关重要,如果样本太大会导致AFLfuzz时速度很慢,最好小于1KB。

 

fuzz之前设置相关的参数:

echo core >/proc/sys/kernel/core_pattern
cd /sys/devices/system/cpu
echo performance | tee cpu*/cpufreq/scaling_governor

开始fuzz:

j fuzz-upx
afl-fuzz -i  afl_upx_in -o afl_upx_out  $PWD/upx/src/upx.out  -d @@

image-20181225171517993

 

不到半小时不到出了5个crash(后续还出了很多crash,但是经过分析后发现大部分类型相同):

 

image-20181225185807754

crash分析

用upx打开发现可以稳定复现:

 

image-20181225174104204

 

剩下的几个crash gdb挂上去后,发现都是同一类型的crash。因此挑一个来分析一下。运行后发现存在OOB read:

 

image-20181225190738392

 

image-20181225190806943

 

栈回溯一下看一下调用链:

 

image-20181225190624003

 

RSI的值是哪里来的呢?回溯分析一下上层的函数,发现RSI = R9+R14*4:

 

image-20181225193836696

 

继续看汇编可能有点枯燥了,好在是有源码的,调用链都有了,直接上源码分析吧。

源码分析

源码在upx/src/p_lx_elf.c文件中,crash时的调用链如下:PackLinuxElf32help1->elf_lookup->get_te32->getle32,深入的看一下PackLinuxElf32help1和elf_lookup部分的代码来看一下OOB read的原因。

 

upx程序由于需要将ELF文件压缩,因此程序会将ELF文件映射到内存空间后再解析。在PackLinuxElf32help1函数中获取了dynstr,dynsym,gashtab,hashtab的地址后,调用elf_lookup来查找JNI_OnLoad的符号地址:

void
PackLinuxElf32::PackLinuxElf32help1(InputFile *f)
{
[...]
        Elf32_Phdr const *phdr= phdri;
        for (int j = e_phnum; --j>=0; ++phdr)
        if (Elf32_Phdr::PT_DYNAMIC==get_te32(&phdr->p_type)) {
            dynseg= (Elf32_Dyn const *)(check_pt_dynamic(phdr) + file_image);
            invert_pt_dynamic(dynseg);
            break;
        }
        // elf_find_dynamic() returns 0 if 0==dynseg.
        dynstr =      (char const *)elf_find_dynamic(Elf32_Dyn::DT_STRTAB);
        dynsym = (Elf32_Sym const *)elf_find_dynamic(Elf32_Dyn::DT_SYMTAB);
        gashtab = (unsigned const *)elf_find_dynamic(Elf32_Dyn::DT_GNU_HASH);
        hashtab = (unsigned const *)elf_find_dynamic(Elf32_Dyn::DT_HASH);
        jni_onload_sym = elf_lookup("JNI_OnLoad");        //  crash here
        if (jni_onload_sym) {
            jni_onload_va = get_te32(&jni_onload_sym->st_value);
            jni_onload_va = 0;
        }
    }

在elf_lookup函数源码如下,根据栈回溯信息可以知道crash的点在unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);。在第二个if判断分支中,n_bitmask是从gashtab中获取的,bitmask是&gashtab+0x10,因此严重怀疑n_bitmask这个从ELF的gashtab中获取的值导致的OOB read,具体调试来看一下。

Elf32_Sym const *PackLinuxElf32::elf_lookup(char const *name) const
{
    if (hashtab && dynsym && dynstr) {                                     // PackLinuxElf32::hashtab is null
        unsigned const nbucket = get_te32(&hashtab[0]);                     // mov    rsi, qword ptr [rdi + 0x330]     0x7ffff7fd8114
        unsigned const *const buckets = &hashtab[2];
        unsigned const *const chains = &buckets[nbucket];
        unsigned const m = elf_hash(name) % nbucket;
        unsigned si;
        for (si= get_te32(&buckets[m]); 0!=si; si= get_te32(&chains[si])) {
            char const *const p= get_te32(&dynsym[si].st_name) + dynstr;
            if (0==strcmp(name, p)) {
                return &dynsym[si];
            }
        }
}    // we can craft gashtab
    if (gashtab && dynsym && dynstr) {                                       // gashtab maybe wrong                    base 0x9ca030
        unsigned const n_bucket = get_te32(&gashtab[0]);                     // dymsym cmp    qword ptr [rbp + 0x340], 0  ; 0x9ca368 --> 0x7ffff7fd8114 --> 0x9307b083b580b083
        unsigned const symbias  = get_te32(&gashtab[1]);                     // dynstr cmp    qword ptr [rbp + 0x1a0], 0  ; 0x9ca1d0 --> 0x7ffff7fd2d6c --> 0x5ec4f8df00c8f8d3
        unsigned const n_bitmask = get_te32(&gashtab[2]);
        unsigned const gnu_shift = get_te32(&gashtab[3]);                       
        unsigned const *const bitmask = &gashtab[4];                         // bitmask is wrong   PackLinuxElf32::gashtab   0x7ffff7fd8114
        unsigned const *const buckets = &bitmask[n_bitmask];                 // bitmask 0x7ffff7fd8124 +((0xe9cd4b0c -1) & 0x474111f )*4

        unsigned const h = gnu_hash(name);   //JNI_OnLoad
        unsigned const hbit1 = 037& h;
        unsigned const hbit2 = 037& (h>>gnu_shift);
        unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);      // crash here!!!  hex( 0x7ffff7fd8124 +((0xe9cd4b0c -1) & 0x474111f )*4) = 0x7ffff90d8550 

        if (1& (w>>hbit1) & (w>>hbit2)) {
            unsigned bucket = get_te32(&buckets[h % n_bucket]);
            if (0!=bucket) {
                Elf32_Sym const *dsp = dynsym;
                unsigned const *const hasharr = &buckets[n_bucket];
                unsigned const *hp = &hasharr[bucket - symbias];

                dsp += bucket;
                do if (0==((h ^ get_te32(hp))>>1)) {
                    char const *const p = get_te32(&dsp->st_name) + dynstr;
                    if (0==strcmp(name, p)) {
                        return dsp;
                    }
                } while (++dsp, 0==(1u& get_te32(hp++)));
            }
        }
    }
    return 0;

}
gdb调试

在jni_onload_sym = elf_lookup("JNI_OnLoad");设置一下断点:

 

image-20181225195254318

 

跟进elf_lookup函数,gashtab的地址保存在rdi,是0x7ffff7fd8114。获取bitmask的值,保存在r9中,执行后r9的值是0x7ffff7fd8124:

 

image-20181226141132028

 

继续执行我们可以看到获取buckets时,n_bitmask的值为0xe9cd4b0c:

 

image-20181226141547194

 

n_bitmask的值之前猜测是从ELF文件中获得的,也就可能是AFL随机生成的。我们在ELF文件中搜索相应的值,可以发现映射到内存空间中的gashtab,验证了我们之前的想法。

 

image-20181226142351566

 

继续跟进,执行到源码中计算crash时的参数的地方unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);

 

image-20181226150624396

 

计算后,RSI的值就已经OOB的的了(如下图所示),如果继续执行就会发生复现crash时的越界读了,这就不重复粘图片了。

 

image-20181226151114641

 

分析出了crash的原因后,发现越界读似乎并不能达到什么效果。首先由于ELF中的gashtab可控导致bitmask可控,以当前程序为例,任意地址读的范围如下:

0x7ffff7fd8124 +((bitmask -1) & 0x474111f )*4 范围大约是 0x7ffff7fd8124 - 0x80046c0ea024

程序的虚拟地址空间如下。ELF文件被映射到图中0x7ffff7fc9000的区域,那么后面的内容,构造一下gashtab都是可以越界读的。

 

image-20181226160535160

 

在源码中,越界读之后的数据还是保存在局部变量中的,读了之后也并不能输出,更不用说越界写和控制PC寄存器了。所以只算是一个DoS吧。

 

后续我开了几十个AFL来对upx进行fuzz,收获了几百个crash,编写相关的脚本后对crash进行精简后,人工分析得到的crash大多还是不能利用,因此对于upx的fuzz暂时告一段落了。

补充

补充一下第二部分中的栈溢出分析分析过程:

gdb afl-case1 -ex "set args out/crashes/id:000001,sig:06,src:000003,op:havoc,rep:128"

运行程序后,crash在这里:

 

image-20181225152741742

 

通过栈回溯信息可以看到,程序发生了栈溢出:

 

image-20181225152817190

参考

  1. https://blog.nullable.software/2017/07/fuzzing-nginx.html
  2. 官网http://lcamtuf.coredump.cx/afl/
  3. afl入门教程,fuzz了自己写的demo https://stfpeak.github.io/2017/06/11/Finding-bugs-using-AFL/
  4. 比较入门的afl入门文章,fuzz upx源码fuzz和readelf无源码fuzz https://www.cnblogs.com/WangAoBo/p/8280352.html
  5. DT_GNU_HASH_TAB https://flapenguin.me/2017/05/10/elf-lookup-dt-gnu-hash/

2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

最后于 2019-4-5 21:35 被心许雪编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (12)
雪    币: 11841
活跃值: 活跃值 (792)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
netwind 活跃值 13 2019-1-28 17:50
2
0
感谢分享!
雪    币: 351
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
五天 活跃值 2019-1-29 10:57
3
0
雪    币: 402
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
DWwinter 活跃值 2019-1-29 11:13
4
0
雪    币: 17
活跃值: 活跃值 (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小熊ppt 活跃值 2019-3-17 20:10
5
0
楼主,问一下,如果我想fuzz一个so文件呢,只有一个二进制的so文件,如何fuzz呢
雪    币: 4345
活跃值: 活跃值 (95)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
心许雪 活跃值 3 2019-4-5 21:11
6
0
小熊ppt 楼主,问一下,如果我想fuzz一个so文件呢,只有一个二进制的so文件,如何fuzz呢
抱歉,没fuzz过so。你可以调研分享一下。
对于库函数,有源码的话可以试试libfuzzer。

最后于 2019-4-5 21:13 被心许雪编辑 ,原因:
雪    币: 8329
活跃值: 活跃值 (1304)
能力值: ( LV5,RANK:77 )
在线值:
发帖
回帖
粉丝
qux 活跃值 2019-4-10 00:01
7
0
楼主验证演示程序的功能时,应该是写错了。
因为演示程序是读参数作为输入,楼主那样的写法只是把文件名读了进去。
最后于 2019-4-14 19:36 被qux编辑 ,原因: 知道了错误原因
雪    币: 8329
活跃值: 活跃值 (1304)
能力值: ( LV5,RANK:77 )
在线值:
发帖
回帖
粉丝
qux 活跃值 2019-4-17 23:59
8
0

貌似楼主把demo的源码搞错了
通过查看https://stfpeak.github.io/2017/06/11/Finding-bugs-using-AFL/的demo源码,结合本文章的demo源码,综合出一个可以达到效果的源码版本,需要的小伙伴请自取

#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <string.h> 
#include <signal.h>


void vuln(char *buf) {
    int n = 0;
    if(buf[0] == 'a') n++;
    if(buf[1] == 'f') n++;
    if(buf[2] == 'l') n++;
    if(buf[3] == '!') n++;

    if(n == 4) {
    printf("awesome!\n");
        raise(SIGSEGV);
    }else{
        printf("wrong!\n");
    }
}


int main(int argc, char *argv[])
{
    char buf[40] = {0};
    FILE *input = NULL;
    input = fopen(argv[1], "r");
    if (input != 0)
    {
        fscanf(input, "%s", &buf);
        printf("buf is %s\n", buf);
        vuln(buf);
        fclose(input);
    }
    else
    {
        printf("bad file!");
    }
    return 0;
}
雪    币: 151
活跃值: 活跃值 (302)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
1wc 活跃值 2019-7-3 20:13
9
0
小熊ppt 楼主,问一下,如果我想fuzz一个so文件呢,只有一个二进制的so文件,如何fuzz呢
用afl的qemu模式进行fuzz即可,但是需要先对.so文件进行逆向并编写harness,可以参考这篇对CS:GO进行fuzz的文章https://phoenhex.re/2018-08-26/csgo-fuzzing-bsp。
雪    币: 8208
活跃值: 活跃值 (7023)
能力值: (RANK:570 )
在线值:
发帖
回帖
粉丝
r0Cat 活跃值 7 2019-7-9 15:02
10
0
666,感谢分享!
雪    币: 104
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
让利润奔跑 活跃值 2019-10-23 19:42
11
0
请问AFL产生的crash文件如何用GDB进行复现,找了好多博客都没有
雪    币: 4345
活跃值: 活跃值 (95)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
心许雪 活跃值 3 2020-4-19 13:02
12
0
让利润奔跑 请问AFL产生的crash文件如何用GDB进行复现,找了好多博客都没有
用gdb启动起程序,在程序运行之前设置好程序需要的参数(具体的命令搜索一下即可),之后run起来即可。
雪    币: 601
活跃值: 活跃值 (229)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
绥术 活跃值 2020-8-7 17:16
13
0

能够再说得详细点吗?‘设置好程序需要的参数(具体的命令搜索一下即可)’是什么意思?可以的话,麻烦给个具体的例子

用gdb启动起程序,在程序运行之前设置好程序需要的参数(具体的命令搜索一下即可),之后run起来即可。

最后于 2020-8-7 17:17 被绥术编辑 ,原因:
游客
登录 | 注册 方可回帖
返回