AFL新手入门教程:使用AFL来fuzz UPX
本文首先简单介绍一下AFL的基本使用方法,随后以upx为例记录一下使用AFL来fuzz upx的过程,最后以其中一个典型的crash做简单分析。本文适合对Fuzz感兴趣的同学作为入门读物,希望和对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!
的文件内容时,程序退出了。
功能符合我们与预期后,创建in和out目录,在in目录中随机生成一些数据。
开始fuzz,指定输入目录和输出目录,@@用于替换输入的参数。
afl-fuzz -i in -o out ./afl-case1 @@
运行后,AFL的fuzz界面如下:
我们可以看到得到了两个crash:
第一个afl!明显符合我们的预期,第二个为什么会crash呢?上gdb调试一下。
gdb afl-case1 -ex "set args out/crashes/id:000001,sig:06,src:000003,op:havoc,rep:128"
运行程序后,crash在这里:
通过栈回溯信息可以看到,程序发生了栈溢出:
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
查看版本:
开始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 @@
不到半小时不到出了5个crash(后续还出了很多crash,但是经过分析后发现大部分类型相同):
crash分析
用upx打开发现可以稳定复现:
剩下的几个crash gdb挂上去后,发现都是同一类型的crash。因此挑一个来分析一下。运行后发现存在OOB read:
栈回溯一下看一下调用链:
RSI
的值是哪里来的呢?回溯分析一下上层的函数,发现RSI = R9+R14*4
:
继续看汇编可能有点枯燥了,好在是有源码的,调用链都有了,直接上源码分析吧。
源码分析
源码在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");设置一下断点:
跟进elf_lookup函数,gashtab的地址保存在rdi,是0x7ffff7fd8114。获取bitmask的值,保存在r9中,执行后r9的值是0x7ffff7fd8124:
继续执行我们可以看到获取buckets时,n_bitmask的值为0xe9cd4b0c:
n_bitmask的值之前猜测是从ELF文件中获得的,也就可能是AFL随机生成的。我们在ELF文件中搜索相应的值,可以发现映射到内存空间中的gashtab,验证了我们之前的想法。
继续跟进,执行到源码中计算crash时的参数的地方 unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);
:
计算后,RSI的值就已经OOB的的了(如下图所示),如果继续执行就会发生复现crash时的越界读了,这就不重复粘图片了。
分析出了crash的原因后,发现越界读似乎并不能达到什么效果。首先由于ELF中的gashtab可控导致bitmask可控,以当前程序为例,任意地址读的范围如下:
0x7ffff7fd8124 +((bitmask -1) & 0x474111f )*4 范围大约是 0x7ffff7fd8124 - 0x80046c0ea024
程序的虚拟地址空间如下。ELF文件被映射到图中0x7ffff7fc9000的区域,那么后面的内容,构造一下gashtab都是可以越界读的。
在源码中,越界读之后的数据还是保存在局部变量中的,读了之后也并不能输出,更不用说越界写和控制PC寄存器了。所以只算是一个DoS吧。
后续我开了几十个AFL来对upx进行fuzz,收获了几百个crash,编写相关的脚本后对crash进行精简后,人工分析得到的crash大多还是不能利用,因此对于upx的fuzz暂时告一段落了。
参考
- https://blog.nullable.software/2017/07/fuzzing-nginx.html
- 官网http://lcamtuf.coredump.cx/afl/
- afl入门教程,fuzz了自己写的demo https://stfpeak.github.io/2017/06/11/Finding-bugs-using-AFL/
- 比较入门的afl入门文章,fuzz upx源码fuzz和readelf无源码fuzz https://www.cnblogs.com/WangAoBo/p/8280352.html
- DT_GNU_HASH_TAB https://flapenguin.me/2017/05/10/elf-lookup-dt-gnu-hash/