├── .gitignore ├── README.md ├── android ├── SoFixer32 ├── SoFixer64 ├── fixso32 └── fixso64 └── frida_dump ├── cmd.py ├── dump_so.js ├── dump_so.py └── log.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | logs 4 | 5 | *.so -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frida_dump 2 | 3 | 支持spawn模式,毕竟不是什么时候都能attach 4 | 5 | # Usage 6 | 7 | 1. spawn模式 8 | 9 | ```bash 10 | python -m frida_dump.dump_so --spawn -n com.hunantv.imgo.activity libexec.so 11 | ``` 12 | 13 | 2. attach模式 14 | 15 | ```bash 16 | python -m frida_dump.dump_so -n 微信 libwechatcommon.so 17 | ``` 18 | 19 | 小姿势:frida 15起attach模式应当使用`frida-ps -U`看到的名字,而不是APP包名 20 | 21 | 3. 不注入dump模式 22 | 23 | 暂时仅支持64位,需要root,不需要frida 24 | 25 | 实现逻辑: 26 | 27 | - 向目标进程发送SIGSTOP将其挂起 28 | - 读取目标进程的maps获取到基址 29 | - 获取linker基址,计算第一个solist的地址 30 | - 读取soinfo链表的内存块 31 | - 检查base是否和目标so匹配,不匹配读取下一个soinfo,直到获取到so的大小 32 | - dump后使用elf-dump-fix修复 33 | - 向目标进程发送SIGCONT恢复运行 34 | 35 | ```bash 36 | python -m frida_dump.dump_so --shell -n com.coolapk.market libjiagu_64.so 37 | ``` 38 | 39 | # Thanks 40 | 41 | - [SoFixer](https://github.com/F8LEFT/SoFixer) 42 | - [elf-dump-fix](https://github.com/maiyao1988/elf-dump-fix) -------------------------------------------------------------------------------- /android/SoFixer32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeeFlowerX/frida_dump/c4ae4010ce9dd057abb6164112390d6239010c4b/android/SoFixer32 -------------------------------------------------------------------------------- /android/SoFixer64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeeFlowerX/frida_dump/c4ae4010ce9dd057abb6164112390d6239010c4b/android/SoFixer64 -------------------------------------------------------------------------------- /android/fixso32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeeFlowerX/frida_dump/c4ae4010ce9dd057abb6164112390d6239010c4b/android/fixso32 -------------------------------------------------------------------------------- /android/fixso64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeeFlowerX/frida_dump/c4ae4010ce9dd057abb6164112390d6239010c4b/android/fixso64 -------------------------------------------------------------------------------- /frida_dump/cmd.py: -------------------------------------------------------------------------------- 1 | class CmdArgs: 2 | 3 | def __init__(self): 4 | self.attach_name = None # type: str 5 | self.attach_pid = None # type: str 6 | self.host = None # type: str 7 | self.runtime = None # type: str 8 | self.log_level = None # type: str 9 | self.spawn = None # type: bool 10 | self.shell = None # type: bool 11 | self.sofixer = None # type: bool 12 | self.TARGET = None # type: str -------------------------------------------------------------------------------- /frida_dump/dump_so.js: -------------------------------------------------------------------------------- 1 | function log(message) { 2 | send({"log": message}); 3 | } 4 | 5 | function hook_dlopen(target_so, symbol) { 6 | let libdl = Process.getModuleByName("libdl.so") 7 | Interceptor.attach(libdl.getExportByName(symbol),{ 8 | onEnter: function(args) { 9 | this.libname = args[0].readCString(); 10 | log(`[${symbol}] ${this.libname} pid:${Process.id} tid:${Process.getCurrentThreadId()}`); 11 | this.hook = false; 12 | if (this.libname.includes(target_so)) { 13 | this.hook = true; 14 | } 15 | }, 16 | onLeave: function(retval) { 17 | log(`[${symbol}] handle:${retval}`); 18 | if(this.hook) { 19 | dump_so(target_so); 20 | }; 21 | } 22 | }); 23 | log(`[${symbol}] hook end`); 24 | } 25 | 26 | function dump_so(target_so) { 27 | log(`[dump_so] ${JSON.stringify(Process.findModuleByName("libc.so"))}`); 28 | let libso = Process.findModuleByName(target_so); 29 | if (libso == null) { 30 | log(`[dump_so] findModuleByName for ${target_so} failed!`); 31 | return; 32 | } 33 | log(`[dump_so] ${JSON.stringify(libso)}`); 34 | Memory.protect(ptr(libso.base), libso.size, 'rwx'); 35 | let buffer = ptr(libso.base).readByteArray(libso.size); 36 | send({"type": "buffer", "arch": Process.arch, "base": libso.base, "size": libso.size}, buffer); 37 | log(`[dump_so] for ${target_so} end!`); 38 | } 39 | 40 | function main(target_so) { 41 | hook_dlopen(target_so, "dlopen"); 42 | hook_dlopen(target_so, "android_dlopen_ext"); 43 | } 44 | 45 | rpc.exports = { 46 | main: main, 47 | dumpso: dump_so, 48 | } -------------------------------------------------------------------------------- /frida_dump/dump_so.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | import signal 6 | import logging 7 | import subprocess 8 | from pathlib import Path 9 | from argparse import ArgumentParser 10 | 11 | from frida_dump.cmd import CmdArgs 12 | from frida_dump.log import setup_logger 13 | 14 | __version__ = '1.0.0' 15 | project_name = 'frida_dump' 16 | logger = setup_logger('frida_dump', level='DEBUG') 17 | 18 | def use_sofixer(arch: str, save_name: str, so_name: str, base: str): 19 | exe_path = "/data/local/tmp/SoFixer" 20 | dump_path = "/data/local/tmp/" + so_name 21 | fix_path = os.path.splitext(dump_path)[0] + "_fix.so" 22 | if arch == "arm": 23 | os.system("adb push android/SoFixer32 " + exe_path) 24 | elif arch == "arm64": 25 | os.system("adb push android/SoFixer64 " + exe_path) 26 | os.system("adb shell chmod +x " + exe_path) 27 | os.system("adb push " + so_name + " " + dump_path) 28 | print("adb shell " + exe_path + " -m " + base + " -s " + dump_path + " -o " + fix_path) 29 | os.system("adb shell " + exe_path + " -m " + base + " -s " + dump_path + " -o " + fix_path) 30 | os.system("adb pull " + fix_path + " " + save_name) 31 | os.system("adb shell rm " + dump_path) 32 | os.system("adb shell rm " + fix_path) 33 | os.system("adb shell rm " + exe_path) 34 | 35 | return save_name 36 | 37 | def use_fixso(arch: str, save_name: str, so_name: str, base: str): 38 | exe_path = "/data/local/tmp/fixso" 39 | dump_path = "/data/local/tmp/" + so_name 40 | fix_path = os.path.splitext(dump_path)[0] + "_fix.so" 41 | if arch == "arm": 42 | os.system("adb push android/fixso32 " + exe_path) 43 | elif arch == "arm64": 44 | os.system("adb push android/fixso64 " + exe_path) 45 | os.system("adb shell chmod +x " + exe_path) 46 | os.system("adb push " + so_name + " " + dump_path) 47 | print("adb shell " + exe_path + " " + dump_path + " " + base + " " + fix_path) 48 | os.system("adb shell " + exe_path + " " + dump_path + " " + base + " " + fix_path) 49 | os.system("adb pull " + fix_path + " " + save_name) 50 | os.system("adb shell rm " + dump_path) 51 | os.system("adb shell rm " + fix_path) 52 | os.system("adb shell rm " + exe_path) 53 | 54 | return save_name 55 | 56 | def on_detached(reason, *args): 57 | sys.exit(f'rpc detached, reason:{reason} args:{args}, go exit') 58 | 59 | def on_message(message: dict, data: bytes, base_name: str, sofixer: bool): 60 | # print(f'recv message -> {message}') 61 | if message['type'] == 'send': 62 | if message['payload'].get('log'): 63 | logger.info(message['payload']['log']) 64 | elif message['payload'].get('type') == 'buffer': 65 | logger.info('buffer recv') 66 | dump_so = base_name + "_dump.so" 67 | Path(dump_so).write_bytes(data) 68 | arch = message['payload']["arch"] 69 | base = message['payload']["base"] 70 | size = message['payload']["size"] 71 | save_name = base_name + "_" + base + "_" + str(size) + "_fix.so" 72 | if sofixer: 73 | fix_so_name = use_sofixer(arch, save_name, dump_so, base) 74 | else: 75 | fix_so_name = use_fixso(arch, save_name, dump_so, base) 76 | logger.info(fix_so_name) 77 | else: 78 | logger.debug(message['payload']) 79 | 80 | 81 | def handle_exit(signum, frame, script: 'frida.core.Script'): 82 | script.unload() 83 | sys.exit('hit handle_exit, go exit') 84 | 85 | def run_cmd(cmd: str, show_cmd: bool = False): 86 | if show_cmd: 87 | print('[*] run_cmd:', cmd) 88 | output = subprocess.check_output(['adb', 'shell', cmd], shell=False) 89 | return output.decode('utf-8') 90 | 91 | 92 | def shell_dump(args: CmdArgs): 93 | result = run_cmd('readelf -s /system/bin/linker64 | grep __dl__ZL6solist') 94 | solist_offset = int(result.strip().split(' ')[1], base=16) 95 | 96 | if args.attach_pid is None: 97 | result = run_cmd(f'pidof {args.attach_name}') 98 | target_pid = result.strip() 99 | else: 100 | target_pid = args.attach_pid 101 | 102 | if len(target_pid.split(' ')) > 1: 103 | print(f'[-] pid more than one -> {target_pid}, please use -p/--attach-pid') 104 | exit() 105 | 106 | os.system(f'adb shell su -c "kill -SIGSTOP {target_pid}"') 107 | 108 | result = run_cmd(f'su -c "cat /proc/{target_pid}/maps | grep linker64"') 109 | result_str = result.strip().splitlines()[0].split('-')[0] 110 | linker_base = int(result_str, base=16) 111 | 112 | result = run_cmd(f'su -c "cat /proc/{target_pid}/maps | grep {args.TARGET[0]}"') 113 | result_lines = result.strip().splitlines() 114 | target_base = None 115 | target_end = None 116 | for index, line in enumerate(result_lines): 117 | base_end = line.split(' ')[0] 118 | if index == 0: 119 | target_base = int(base_end.split('-')[0], base=16) 120 | if index == len(result_lines) - 1: 121 | target_end = int(base_end.split('-')[1], base=16) 122 | if target_base is None: 123 | sys.exit('can not get target base') 124 | if target_end is None: 125 | sys.exit('can not get target end') 126 | target_size = target_end - target_base 127 | 128 | solist_addr = linker_base + solist_offset 129 | 130 | print(f'[+] solist:{solist_addr:#x} target:{target_base:#x}') 131 | 132 | result = run_cmd(f'su -c "xxd -p -l 8 -s {solist_addr:#x} /proc/{target_pid}/mem"') 133 | solist_head = int.from_bytes(bytes.fromhex(result), byteorder='little') 134 | print(f'[*] solist head:{solist_head:#x}') 135 | 136 | ptr_size = 8 137 | off_base = 0x10 138 | off_size = 0x18 139 | off_next = 0x28 140 | 141 | result = run_cmd(f'su -c "xxd -p -l 256 -s {solist_head:#x} /proc/{target_pid}/mem"') 142 | soinfo_raw = bytes.fromhex(result) 143 | soinfo_base = int.from_bytes(soinfo_raw[off_base:off_base + ptr_size], byteorder='little') 144 | soinfo_next = int.from_bytes(soinfo_raw[off_next:off_next + ptr_size], byteorder='little') 145 | print(f'[*] first soinfo base:{soinfo_base:#x} next:{soinfo_next:#x}') 146 | 147 | result = run_cmd(f'su -c "dd if=/proc/{target_pid}/mem of=/data/local/tmp/soinfo iflag=skip_bytes skip={soinfo_next:#x} bs=1K count={256}"') 148 | # print('[+] dd result:', result) 149 | 150 | os.system('adb pull /data/local/tmp/soinfo') 151 | os.system('adb shell su -c "rm /data/local/tmp/soinfo"') 152 | 153 | target_pattern = target_base.to_bytes(8, byteorder='little') 154 | soinfo_path = Path('soinfo') 155 | soinfo_raw = soinfo_path.read_bytes() 156 | soinfo_size = 0 157 | for item in re.finditer(target_pattern, soinfo_raw): 158 | offset = item.start() + ptr_size 159 | soinfo_size = int.from_bytes(soinfo_raw[offset:offset + ptr_size], byteorder='little') 160 | print('[+] soinfo_size', soinfo_size) 161 | 162 | if soinfo_size > target_size * 10: 163 | print('[*] use target_size as soinfo_size', target_size) 164 | soinfo_size = target_size 165 | 166 | # 可能有多个 一般第一个是对的 167 | break 168 | 169 | soinfo_path.unlink() 170 | 171 | if soinfo_size == 0: 172 | print('[*] use target_size as soinfo_size', target_size) 173 | soinfo_size = target_size 174 | 175 | if soinfo_size > 0: 176 | 177 | arch = 'arm64' 178 | dump_so = 'dump_so' 179 | base = hex(target_base) 180 | size = str(soinfo_size) 181 | 182 | result = run_cmd(f'su -c "dd if=/proc/{target_pid}/mem of=/data/local/tmp/{dump_so} iflag=skip_bytes skip={target_base:#x} bs={soinfo_size} count=1"') 183 | # print('[+] dd result:', result) 184 | 185 | os.system(f'adb pull /data/local/tmp/{dump_so}') 186 | os.system(f'adb shell su -c "rm /data/local/tmp/{dump_so}"') 187 | 188 | base_name = os.path.splitext(args.TARGET[0])[0] 189 | save_name = base_name + "_" + base + "_" + size + "_fix.so" 190 | if args.sofixer: 191 | fix_so_name = use_sofixer(arch, save_name, dump_so, base) 192 | else: 193 | fix_so_name = use_fixso(arch, save_name, dump_so, base) 194 | print(f'[+] {fix_so_name}') 195 | 196 | os.system(f'adb shell su -c "kill -SIGCONT {target_pid}"') 197 | 198 | def main(): 199 | # <------ 正文 ------> 200 | parser = ArgumentParser( 201 | prog='frida_dump script', 202 | usage='python -m frida_dump.dump_so [OPTION]...', 203 | description=f'version {__version__}, frida_dump server', 204 | add_help=False 205 | ) 206 | parser.add_argument('-v', '--version', action='store_true', help='print version and exit') 207 | parser.add_argument('-h', '--help', action='store_true', help='print help message and exit') 208 | parser.add_argument('-s', '--sofixer', action='store_true', help='use SoFixer, default use fixso') 209 | parser.add_argument('-f', '--spawn', action='store_true', help='spawn file') 210 | parser.add_argument('--shell', action='store_true', help='shell mode') 211 | parser.add_argument('-n', '--attach-name', help='attach to NAME') 212 | parser.add_argument('-p', '--attach-pid', help='attach to PID') 213 | parser.add_argument('-H', '--host', help='connect to remote frida-server on HOST') 214 | parser.add_argument('--runtime', default='qjs', help='only qjs know') 215 | parser.add_argument('--log-level', default='DEBUG', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], help='set log level, default is INFO') 216 | parser.add_argument('TARGET', nargs='*', help='TARGET so name string') 217 | args = parser.parse_args() # type: CmdArgs 218 | if args.help: 219 | parser.print_help() 220 | sys.exit() 221 | if args.version: 222 | parser.print_help() 223 | sys.exit() 224 | assert len(args.TARGET) > 0, 'plz set target' 225 | if args.shell: 226 | return shell_dump(args) 227 | if args.attach_name is None and args.attach_pid is None: 228 | sys.exit('set NAME or PID, plz') 229 | if args.attach_name and args.attach_pid: 230 | sys.exit('set NAME or PID only one, plz') 231 | for handler in logger.handlers: 232 | if isinstance(handler, logging.FileHandler) is False: 233 | handler.setLevel(logging.getLevelName(args.log_level)) 234 | logger.info(f'start {project_name}, current version is {__version__}') 235 | target = args.attach_name 236 | if args.attach_pid: 237 | target = args.attach_pid 238 | try: 239 | import frida 240 | if args.host: 241 | device = frida.get_device_manager().add_remote_device(args.host) 242 | else: 243 | device = frida.get_usb_device(timeout=10) 244 | if args.spawn: 245 | logger.info(f'start spawn {target}') 246 | pid = device.spawn(target) 247 | session = device.attach(pid) 248 | else: 249 | logger.info(f'start attach {target}') 250 | session = device.attach(target) 251 | except Exception as e: 252 | logger.error(f'attach to {target} failed', exc_info=e) 253 | sys.exit() 254 | 255 | logger.info(f'attach {target} success, inject script now') 256 | try: 257 | jscode = Path('frida_dump/dump_so.js').read_text(encoding='utf-8') 258 | script = session.create_script(jscode, runtime='qjs') 259 | script.load() 260 | session.on('detached', on_detached) 261 | base_name = os.path.splitext(args.TARGET[0])[0] 262 | script.on('message', lambda message, data: on_message(message, data, base_name, args.sofixer)) 263 | except Exception as e: 264 | logger.error(f'inject script failed', exc_info=e) 265 | sys.exit() 266 | rpc = script.exports 267 | if args.spawn: 268 | device.resume(pid) 269 | rpc.main(args.TARGET[0]) 270 | else: 271 | rpc.dumpso(args.TARGET[0]) 272 | # <------ 处理手动Ctrl+C退出 ------> 273 | signal.signal(signal.SIGINT, lambda signum, frame: handle_exit(signum, frame, script)) 274 | signal.signal(signal.SIGTERM, lambda signum, frame: handle_exit(signum, frame, script)) 275 | # wait 276 | sys.stdin.read() 277 | 278 | if __name__ == '__main__': 279 | main() 280 | -------------------------------------------------------------------------------- /frida_dump/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import datetime 5 | from pathlib import Path 6 | 7 | 8 | GLOBAL_LOGGERS = {} 9 | 10 | 11 | def tell_me_path(target_folder_name: str) -> Path: 12 | ''' 13 | 兼容自用脚本版与免安装可执行版本 14 | :param target_folder_name: 目录文件夹路径 15 | :returns 返回request_config路径 16 | ''' 17 | if getattr(sys, 'frozen', False): 18 | return Path(sys.executable).parent / target_folder_name 19 | else: 20 | return Path(__file__).parent.parent / target_folder_name 21 | 22 | 23 | class PackagePathFilter(logging.Filter): 24 | ''' 25 | 获取文件相对路径而不只是文件名 26 | 配合行号可以快速定位 27 | 参见 How can I include the relative path to a module in a Python logging statement? 28 | - https://stackoverflow.com/questions/52582458 29 | ''' 30 | def filter(self, record): 31 | record.relativepath = None 32 | abs_sys_paths = map(os.path.abspath, sys.path) 33 | for path in sorted(abs_sys_paths, key=len, reverse=True): 34 | if not path.endswith(os.sep): 35 | path += os.sep 36 | if record.pathname.startswith(path): 37 | record.relativepath = os.path.relpath(record.pathname, path) 38 | break 39 | return True 40 | 41 | 42 | def setup_logger(name: str, level: str = 'INFO') -> logging.Logger: 43 | ''' 44 | - 终端只输出日志等级大于等于level的日志 默认是INFO 45 | - 全部日志等级的信息都会记录到文件中 46 | ''' 47 | logger = GLOBAL_LOGGERS.get(name) 48 | if logger: 49 | return logger 50 | # 先把 logger 初始化好 51 | logger = logging.getLogger(f'{name}') 52 | GLOBAL_LOGGERS[name] = logger 53 | # 开始设置 54 | formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') 55 | log_time = datetime.datetime.now().strftime("%H%M%S") 56 | # 没有打包的时候 __file__ 就是当前文件路径 57 | # 打包之后通过 sys.executable 获取程序路径 58 | log_folder_path = tell_me_path('logs') 59 | if log_folder_path.exists() is False: 60 | log_folder_path.mkdir() 61 | ch = logging.StreamHandler() 62 | ch.addFilter(PackagePathFilter()) 63 | if level.lower() == 'info': 64 | ch.setLevel(logging.INFO) 65 | elif level.lower() == 'warning': 66 | ch.setLevel(logging.WARNING) 67 | elif level.lower() == 'error': 68 | ch.setLevel(logging.ERROR) 69 | else: 70 | ch.setLevel(logging.DEBUG) 71 | ch.setFormatter(formatter) 72 | logger.setLevel(logging.DEBUG) 73 | logger.addHandler(ch) 74 | log_file_path = log_folder_path / f'{name}-{log_time}.log' 75 | fh = logging.FileHandler(log_file_path.resolve().as_posix(), encoding='utf-8', delay=True) 76 | fh.addFilter(PackagePathFilter()) 77 | fh.setLevel(logging.DEBUG) 78 | fh.setFormatter(formatter) 79 | logger.addHandler(fh) 80 | # logger.info(f'log file -> {log_file_path.resolve().as_posix()}') 81 | return logger 82 | 83 | 84 | def test_log(): 85 | logger = setup_logger("test", level='INFO') 86 | logger.debug('this is DEBUG level') 87 | logger.info('this is INFO level') 88 | logger.warning('this is WARNING level') 89 | logger.error('this is ERROR level') 90 | 91 | 92 | # test_log() --------------------------------------------------------------------------------