图灵社区按

TEAP是什么?TEAP是Turingbook Early Access Program的简称,即早期试读,它公布的是图灵在途新书未经编辑的内容。一本书的翻译周期约为3到6个月,如果在翻译过程中,译者就能与读者进行沟通和交流,对整本书的翻译品质是有帮助的。通过TEAP,读者可以提前阅读将来才能出版的内容,译者也能收获宝贵的反馈意见,改进翻译,提高质量。

本书原名为《A Bug Hunter's Diary》,中文暂定名为《捉虫日记》。
本篇选自第8章第一节。
任何意见、建议,都非常感谢能联系我,我的微博@loveisbug

铃音大屠杀

2009年3月21日,星期六
亲爱的日记,

上周好友借给我一台越狱过的第一代iPhone,我很兴奋。自从Apple预告了iPhone,我就想试试看能否在这个设备里找到bug,但是上周之前我一台都没碰过。

8.1 探寻漏洞

终于,我有了一台iPhone可以把玩,我想找找bug。但是从哪儿开始呢?我先列了一张清单,把已安装的应用和库中看起来最可能含有bug的都列了出来。移动版Safari浏览器、移动版Mail应用和音频库排在列表的前几位。我觉得音频库是最有希望的目标,因为这些库要做大量解析工作,并且在手机上广泛使用,因此我打算先在它们身上试试运气。

我通过执行以下步骤来查找iPhone音频库的bug:

以下步骤中我使用的平台是第一代iPhone,固件版本2.2.1(5H11)。

  • 第一步:研究iPhone的音频性能。
  • 第二步:创建一个简单的fuzz程序fuzz这个手机。

注意我通过Cydia在iPhone上安装了所有必需的工具——譬如Bash、OpenSSH,以及GNU调试器。

第一步:研究iPhone的音频性能

脱胎于iPod的iPhone,是一款拥有强大音频功能的设备。手机上三个现成的框架提供了不同级别的声音功能:Core Audio框架,Celestial框架和Audio Toolbox框架。另外,iPhone运行一个音频守护进程mediaserverd,这个进程收集所有应用的声音输出,并且管理诸如音量和铃声开关变化这样的事件。

第二步:创建一个简单的fuzz程序fuzz这个手机

iPhone不同框架的音频系统看上去有点复杂,因此我决定创建一个简单的fuzz程序,从搜寻明显的bug开始。这个fuzz程序完成以下操作:

  1. 在Linux主机上:通过改动一个目标样例文件准备测试用例。
  2. 在Linux主机上:通过一个web服务器伺服这些测试用例。
  3. 在iPhone上:在移动版Safari中打开这些测试用例。
  4. 在iPhone上:监视mediaserverd的出错状况。
  5. 在iPhone上:发现错误事件时记录结果。
  6. 重复以上步骤。

在Linux主机上创建了下面这个简单的、基于文件改动的fuzz程序来准备测试用例:

01 #include <stdio.h>
02 #include <sys/types.h>
03 #include <sys/mman.h>
04 #include <fcntl.h>
05 #include <stdlib.h>
06 #include <unistd.h>
07
08 int
09 main (int argc, char *argv[])
10 {
11         int fd = 0;
12         char * p = NULL;
13         char * name = NULL;
14         unsigned int file_size = 0;
15         unsigned int file_offset = 0;
16         unsigned int file_value = 0;
17
18         if (argc < 2) {
19             printf ("[-] Error: not enough arguments\n");
20             return (1);
21         } else {
22             file_size = atol (argv[1]);
23             file_offset = atol (argv[2]);
24             file_value = atol (argv[3]);
25             name = argv[4];
26         }
27
28         // open file
29         fd = open (name, O_RDWR);
30         if (fd < 0) {
31             perror ("open");
32             exit (1);
33         }
34
35         // mmap file
36         p = mmap (0, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
37         if ((int) p == -1) {
38             perror ("mmap");
39             close (fd);
40             exit (1);
41         }
42
43         // mutate file
44         printf ("[+] file offset: 0x%08x (value: 0x%08x)\n", file_offset, file_value);
45         fflush (stdout);
46         p[file_offset] = file_value;
47
48         close (fd);
49         munmap (p, file_size);
50
51         return (0);
52 }

代码清单8-1:在Linux主机上写的准备测试用例的代码(fuzz.c

清单8-1的fuzz程序接收4个参数:目标样例文件的大小、文件修改处的偏移量、写到文件偏移位置处的单字节值,以及目标文件的文件名。写完fuzz程序后把它编译一下:

linux$ **gcc -o fuzz fuzz.c**

然后我开始fuzz AAC格式(Advanced Audio Coding)的文件,这是iPhone上使用的默认音频格式。我选择iPhone的标准铃音,Alarm.m4r,作为目标样例文件:

linux$ **cp Alarm.m4r testcase.m4r**

在终端输入以下命令行,得到测试用例文件的大小:

linux$ **du -b testcase.m4r**
415959 testcase.m4r

下面的命令行选项指示fuzz程序用0xff(十进制255)替换文件中偏移位置为4的字节:

linux$ **./fuzz 415959 4 255 testcase.m4r**
[+] file offset: 0x00000004 (value: 0x000000ff)

然后用xxd验证结果:

linux$ **xxd Alarm.m4r | head -1**
0000000: 0000 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....

linux$ **xxd testcase.m4r | head -1**
0000000: 0000 0020 ff74 7970 4d34 4120 0000 0000 ... .typM4A ....

输出显示文件偏移位置为4(文件偏移从0开始计数)的值被预期值(0xff)替换。接下来,我创建一个bash脚本来自动完成文件的改动:

01 #!/bin/bash
02
03 # file size
04 filesize=415959
05
06 # file offset
07 off=0
08
09 # number of files
10 num=4
11
12 # fuzz value
13 val=255
14
15 # name counter
16 cnt=0
17
18 while [ $cnt -lt $num ]
19 do
20         cp ./Alarm.m4r ./file$cnt.m4a
21         ./fuzz $filesize $off $val ./file$cnt.m4a
22         let "off+=1"
23         let "cnt+=1"
24 done

代码清单8-2:自动改动文件的bash脚本(go.sh

这个脚本,只是把清单8-1中所示fuzz程序包装了一下,它自动生成目标文件Alarm.m4r的4个测试用例(见第20行)。从文件偏移位置0开始(见第7行),目标文件的前4个字节(见第10行)分别被替换为0xff(见第13行)。执行后,脚本产生以下输出:

linux$ **./go.sh**
[+] file offset: 0x00000000 (value: 0x000000ff)
[+] file offset: 0x00000001 (value: 0x000000ff)
[+] file offset: 0x00000002 (value: 0x000000ff)
[+] file offset: 0x00000003 (value: 0x000000ff)

然后验证生成的测试用例:

linux$ **xxd file0.m4a | head -1**
0000000: ff00 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....

linux$ **xxd file1.m4a | head -1**
0000000: 00ff 0020 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....

linux$ **xxd file2.m4a | head -1**
0000000: 0000 ff20 6674 7970 4d34 4120 0000 0000 ... ftypM4A ....

linux$ **xxd file3.m4a | head -1**
0000000: 0000 00ff 6674 7970 4d34 4120 0000 0000 ....ftypM4A ....

如输出所示,fuzz程序运行后修改了每个测试用例文件中相应位置的字节,跟预期一致。有一个重要事实我还没有提到,清单8-2中的脚本把警告铃音文件的扩展名由.m4r改为.m4a(见第20行)。这是必要的,因为移动版Safari不支持iPhone手机铃音使用的.m4r文件扩展名。

我把改动过和没改过的警告铃音文件拷贝到Linux主机上安装的web服务器Apache的网站根目录下。把未改动过的原始警告铃音扩展名从.m4r改为.m4a,然后让移动版Safari指向它的URL。

如图8-1所示,原始目标文件Alarm.m4a在手机中的移动版Safari上顺利播放。然后我让浏览器指向第一个改动过的测试用例文件的URL,file0.m4a。

图8-2显示移动版Safari打开了这个改动过的文件,但是不能正确解析。

enter image description here
图8-1:用移动版Safari播放未改过的文件Alarm.m4a

enter image description here
图8-2:播放改动过的测试用例文件(file0.m4a

到目前为止完成了什么呢?我可以通过改动音频文件来准备测试用例,启动移动版Safari,并让它加载测试用例。此时,我想找到一个方法,在监视mediaserverd出错时,让移动版Safari自动一个接一个地打开测试用例文件。我写了这个Bash小脚本在手机上完成该工作:

01 #!/bin/bash
02
03 fuzzhost=192.168.99.103
04
05 echo [+] =================================
06 echo [+] Start fuzzing
07 echo [+]
08 echo -n "[+] Cleanup: "
09 killall MobileSafari
10 killall mediaserverd
11 sleep 5
12 echo
13
14 origpid=`ps -u mobile -o pid,command | grep /usr/sbin/mediaserverd | cut -c 0-5`
15 echo [+] Original PID of /usr/sbin/mediaserverd: $origpid
16
17 currpid=$origpid
18 let cnt=0
19 let i=0
20
21 while [ $cnt -le 1000 ];
22 do
23         if [ $i -eq 10 ];
24         then
25             echo -n "[+] Restarting mediaserverd.. "
26             killall mediaserverd
27             sleep 4
28             origpid=`ps -u mobile -o pid,command | grep /usr/sbin/ →
mediaserverd | cut -c 0-5`
29             currpid=$origpid
30             sleep 10
31             echo "done"
32             echo [+] New mediaserverd PID: $origpid
33             i=0
34         fi
35         echo
36         echo [+] =================================
37         echo [+] Current file: http://$fuzzhost/file$cnt.m4a
38         openURL http://$fuzzhost/file$cnt.m4a
39         sleep 30
40         currpid=`ps -u mobile -o pid,command | grep /usr/sbin/mediaserverd | → cut     -c 0-5`
41         echo [+] Current PID of /usr/sbin/mediaserverd: $currpid
42         if [ $currpid -ne $origpid ];
43         then
44             echo [+] POTENTIAL BUG FOUND! File: file$cnt.m4a
45             openURL http://$fuzzhost/BUG_FOUND_file$cnt.m4a
46             origpid=$currpid
47             sleep 5
48         fi
49         ((cnt++))
50         ((i++))
51         killall MobileSafari
52 done
53
54 killall MobileSafari

代码清单8-3:监视mediaserverd出错时自动打开测试用例文件的代码(audiofuzzer.sh

清单8-3中的Bash脚本是这样工作的:

  • 第3行显示伺服测试用例的web服务器的IP地址。
  • 第9行和第10行重启mediaserverd,并且杀掉所有在运行的移动版Safari进程实例以创造一个干净的环境。
  • 第14行拷贝音频守护进程mediaserverd的进程ID给变量origpid。
  • 第21行包含针对每个测试用例都要执行的主循环。
  • 第23至34行每10个测试用例重启一次mediaserverd。对iPhone的fuzzing可能是枯燥乏味的,因为包括mediaserverd在内的一些组件很容易挂起。
  • 第38行用openURL工具启动web服务器上独立的测试用例。
  • 第40行拷贝音频守护进程mediaserverd的当前进程ID给变量currpid。
  • 第42行比较保存的mediaserverd进程ID(见第14行)和当前守护进程ID。处理一个测试用例时,当mediaserverd遇到一个错误并重启,这两个进程ID便不一样。这个结果记录到手机终端(见第44行)。这个脚本也会发送一个GET请求给web服务器,包含“BUG_FOUND”字符串和导致mediaserverd崩溃的文件名(见第45行)。
  • 第51行在每次测试用例运行之后杀掉当前移动版Safari实例。

实现这个小脚本之后,我从文件偏移位置0开始,生成了1000个改动过的Alarm.m4r铃音文件,把它们拷贝到web服务器的根目录,然后在iPhone上运行audiofuzzer.sh脚本。有时手机会由于内存泄露而崩溃。每一次崩溃,我必须重启手机,从web服务器的访问日志中提取最后处理的测试用例文件名,调整代码清单8-3的第18行,然后继续fuzzing。Fuzzing iPhone就是这样痛苦……但这是值得的!除了内存泄露导致手机挂起之外,我还发现了很多崩溃是由内存数据损坏造成的。

下篇:捉虫日记之“铃音大屠杀”(2)