├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── install.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | \#*\# 4 | apk/ 5 | pdfs/ 6 | filep/ 7 | sources/ 8 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jonathan Metzman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AndroFuzz 2 | 3 | A simple file format fuzzer for android. Used by me to fuzz pdf readers, but should work for any file format. 4 | 5 | Because of a (probably incorrect) design choice, AndroFuzz does not create any files itself but must rely on other file format fuzzers, such as SPIKEfile to create the files. Instead of implementing this capability, I made AndroFuzz able to fuzz a number of readers at once. 6 | 7 | This is the current mode AndroFuzz uses to work: 8 | 9 | 1. Files generated by fuzzer (not AndroFuzz) 10 | 11 | 2. AndroFuzz pushes files to Android emulator 12 | 13 | 3. AndroFuzz pushes the first apk of one of the apps we are testing to the emulator and installs it 14 | 15 | 4. The app loads the files and is fuzzed. AndroFuzz monitors logcat for buffer overflows and will alert the user if any occur 16 | 17 | 5. Once all the files are tested on the app, the app is uninstalled and the process, starting at step 3 is started for the next app 18 | 19 | Currently AndroFuzz is incomplete and unready for real fuzzing since it requires users to manually generate the fuzzing files using some other program. 20 | Additionally, the above model in which not many files are tested on many apps, is probably unlikely to cause many interesting crashes. 21 | 22 | AndroFuzz will be given a more sane, traditional model some time in the near future. 23 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 1. Add file generation (or mutation) to AndroFuzz itself 2 | 2. Make delivery and reporting more configurable 3 | 3. Figure out whether ability to test many apps is even useful, and possibly remove it -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import os 2 | from itertools import izip 3 | from push_files import popen_wait, kill_app 4 | 5 | class PackageInstaller: 6 | def __init__(self, path='apk'): 7 | apks = os.listdir(path) 8 | self.package_dirs = {apk: package_path(apk) for apk in apks} 9 | self.__most_recently_installed = None 10 | 11 | def install_applications(self): 12 | apks = self.package_dirs.keys() 13 | apps = [apk.split('.apk')[0] for apk in apks] 14 | for apk, app in izip(apks, apps): 15 | apk_path = self.package_dirs.pop(apk) 16 | install_app(apk_path) 17 | self.__most_recently_installed = app 18 | yield app 19 | 20 | def uninstall(self, program): 21 | uninstall_app(program) 22 | return program 23 | 24 | def uninstall_most_recent(self): 25 | app = self.__most_recently_installed 26 | if app is not None: 27 | uninstall_app(app) 28 | else: 29 | return None 30 | 31 | def package_path(apk, path='apk'): 32 | return '/'.join([os.getcwd(), path, apk]) 33 | 34 | def install_app(app_path): 35 | cmd = ['adb', '-e', 'install', app_path] 36 | return popen_wait(cmd) 37 | 38 | def uninstall_app(app): 39 | kill_app(app) 40 | uninstall_cmd = ['adb', '-e', 'shell', 'pm', 'uninstall', app] 41 | return popen_wait(uninstall_cmd) 42 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import json 4 | import re 5 | import pexpect 6 | import glob 7 | from install import PackageInstaller 8 | 9 | class AndroidLogger: 10 | ''' 11 | Can collect logs about apps from an android device/emulator 12 | ''' 13 | def __init__(self, log_dir='logs'): 14 | self.logs = {} 15 | log_cmd = 'adb -e logcat' 16 | self.logfile = log_dir + '/logfile' 17 | # self.add_log_dir(log_dir) 18 | self.child = pexpect.spawn(log_cmd) 19 | try: 20 | self.child.read_nonblocking(timeout=0) 21 | except pexpect.TIMEOUT: 22 | pass 23 | 24 | def add_log_dir(self, log_dir): # Bug, what if file with name log_dir exists? 25 | if log_dir[-1] != '/': 26 | log_dir += '/' 27 | if os.path.isdir(log_dir): 28 | self.log_dir = log_dir 29 | return log_dir 30 | else: 31 | os.makedirs(log_dir) 32 | return self.add_log_dir(log_dir) 33 | 34 | def __read(self): 35 | ''' 36 | Read logs from adb 37 | ''' 38 | try: 39 | self.child.expect('\r\r\n', timeout=5) 40 | log_output = self.child.before 41 | except pexpect.TIMEOUT: 42 | log_output = None 43 | return log_output 44 | 45 | def write_app_logs(self, program): 46 | ''' 47 | Write logs of currently tested app to disk and remove it from 48 | the logs attribute 49 | ''' 50 | # logs_dict = self.pop_program_logs(program) 51 | # # self.logfile.write(json.dumps(logs_dict)) 52 | # if check_segfault(logs_dict): 53 | # self.logfile.write(program +'\n\n\n') 54 | # self.logfile.write(unicode(logs_dict)) 55 | # self.logfile.close() 56 | return self.logfile 57 | 58 | def add_app(self, app): 59 | ''' 60 | Allows for the collection of log data from an app 61 | ''' 62 | # if self.logfile: 63 | # self.logfile.close() 64 | # self.logfile = None 65 | self.logs[app] = {} 66 | # log_path = self.log_dir + '/' + app + '.log' 67 | # self.logfile = open(log_path, 'a+') 68 | return self.logs[app], self 69 | 70 | def get_logs(self): 71 | ''' 72 | Returns the adb logs created during the fuzzing of an app 73 | ''' 74 | return self.logs 75 | 76 | def pop_program_logs(self, program): 77 | ''' 78 | Returns the logs of an app and removes them from 79 | this objects logs dictionary 80 | ''' 81 | return self.logs.pop(program, {}) 82 | 83 | def add_logs(self, app, file): 84 | ''' 85 | Add the each line of logs to the log dictionary of app 86 | ''' 87 | log_lines = self._get_logs(app, file) 88 | # self.check_segfault(log_lines, app, file) 89 | self.logs[app][file] = log_lines 90 | # file_log = {file: '\n'.join(log_lines)} 91 | if check_segfault(self.logs[app]): 92 | print "segf" 93 | self.logfile.write(file + '\t' + app + '\n') 94 | self.logfile.flush() 95 | self.logs[app][file] = '' 96 | # self.logfile.write(unicode(self.logs[app])) 97 | return self.logs[app][file] 98 | 99 | def _get_logs(self, app, file): 100 | ''' 101 | Keep getting logs from the android device as long as they are being produced 102 | ''' 103 | logs = '' 104 | while True: 105 | log_line = self.__read() 106 | if not log_line: 107 | break 108 | logs = '\n'.join([logs, log_line]) 109 | return logs 110 | 111 | def push_files(remote_path='/mnt/sdcard/Download', local_path='pdfs', adb='adb'): 112 | ''' 113 | Push files in the local_path directory to the remote_path directory on the android device 114 | ''' 115 | cmd = [adb, 'push', local_path, remote_path] 116 | popen_wait(cmd) 117 | files=os.listdir(local_path) 118 | remote_files = [remote_path + '/' + file for file in files] 119 | return remote_files 120 | 121 | def stop_app(application, process=None): 122 | ''' 123 | Start running an app 124 | ''' 125 | if process: 126 | process.kill() 127 | home_screen() # Can't kill apps in foreground 128 | stop_cmd = ['adb', 'shell', 'am', 'force-stop', application] 129 | return popen_wait(stop_cmd) 130 | 131 | def popen_wait(cmd, message=None): 132 | ''' 133 | Start a process using a Popen event and wait for it to terminate 134 | ''' 135 | p = subprocess.Popen(cmd) 136 | ret_val = p.wait() 137 | if message: 138 | print(message) 139 | return ret_val 140 | 141 | def open_file(file, intent, mimetype): 142 | ''' 143 | Create a Popen object that opens a file with an app and returns the Popen object 144 | ''' 145 | data = 'file://' + file 146 | open_cmd = ['adb', 'shell', 'am', 'start', '-W', '-a', intent, '-d', data, '-t', mimetype] 147 | p = subprocess.Popen(open_cmd) 148 | return p 149 | 150 | def open_files(files, intent='android.intent.action.VIEW', mimetype='application/pdf', log_dir='logs'): 151 | logger = AndroidLogger(log_dir) 152 | package_installer = PackageInstaller() 153 | applications = package_installer.install_applications() 154 | app_logfiles = [] 155 | for application in applications: 156 | logger.add_app(application) 157 | stop_app(application) 158 | for file in files: 159 | p = open_file(file, intent, mimetype) 160 | logger.add_logs(application, file) 161 | stop_app(application, process=p) 162 | application = package_installer.uninstall(application) 163 | return app_logfiles 164 | 165 | def home_screen(): 166 | ''' 167 | Send the press home button event to the android device 168 | ''' 169 | return send_key_event('3') 170 | 171 | def send_key_event(event_num): 172 | ''' 173 | Send an arbitrary key event to an android device 174 | ''' 175 | cmd = ['adb', 'shell', 'input', 'keyevent', event_num] 176 | return popen_wait(cmd) 177 | 178 | def power_button(): 179 | ''' 180 | Send the press power button event to the android device 181 | ''' 182 | return send_key_event('26') 183 | 184 | def kill_app(app): 185 | ''' 186 | Kills an app process 187 | ''' 188 | stop_app(app) 189 | kill_cmd = ['adb', 'shell', 'am', 'kill', app] 190 | return popen_wait(kill_cmd) 191 | 192 | def adb_cmd(cmd): 193 | ''' 194 | run the adb cmd using adb on an android device 195 | ''' 196 | return ['adb'] + cmd 197 | 198 | def adb_shell_cmd(cmd): 199 | ''' 200 | run the shell command on the android device 201 | ''' 202 | return adb_cmd(['shell']) + cmd 203 | 204 | def cleanup(files): 205 | ''' 206 | Remove the files from the android device 207 | ''' 208 | return popen_wait(adb_shell_cmd(['rm'] + files)) 209 | 210 | def check_segfault(log_dict): 211 | # regex = r'F/libc \( \d*\): Fatal signal \d{2,8} .*terminated by signal \(11\)'# r'Fatal signal 11(.|\n)*Process \d{2,8} terminated by signal' 212 | # regex = ur'F/libc \( \d*\): Fatal signal \d{2,8} .*terminated by signal \(11\)' 213 | # match = re.search(regex, log) 214 | # if match: 215 | # return match.group() 216 | # else: 217 | # return None 218 | segfault_caused = False 219 | files = log_dict.keys() 220 | for file in files: 221 | if 'Fatal signal 11' in log_dict[file]: 222 | print "Segfault caused by " + file 223 | segfault_caused = True 224 | return segfault_caused 225 | 226 | 227 | def fuzz(log_dir='logs'): 228 | ''' 229 | Push the files to the android device, install the apps and start fuzzing 230 | ''' 231 | files = push_files() 232 | logs = open_files(files, log_dir=log_dir) 233 | return logs, files 234 | 235 | # def examine_logs(log_dir): 236 | # if log_dir[-1] != '/': # should make a decorator 237 | # log_dir += '/' 238 | # logs = glob.glob(log_dir + '*.log') 239 | # for log_file in logs: 240 | # log_file_fp = open(log_file) 241 | # log_file_contents = log_file_fp.read() 242 | # log_file_fp.close() 243 | # check_segfault(json.loads(log_file_contents)) 244 | # # segfault_logs = check_segfault(log_file_contents) 245 | # # if segfault_logs: 246 | # # segfault_logs_fp = json.loads(open(logfile + '.segfault', 'w+')) 247 | # # segfault_logs_fp.write(segfault_logs) 248 | 249 | 250 | def main(): 251 | log_dir = 'logs' 252 | _, fuzz_files = fuzz(log_dir=log_dir) 253 | print "Done Fuzzing" 254 | # examine_logs(log_dir) 255 | print "Done logging" 256 | cleanup(fuzz_files) 257 | 258 | if __name__=='__main__': 259 | main() 260 | --------------------------------------------------------------------------------