本文的思路是很久以前就有的,但是找不到一个合适的工具“拍摄快照”一直都没有成功实现,直到看到这篇文章才成功实现,感谢该文作者seeflower。如果原作者认为本文章不合适,请联系我删除,谢谢啦~
每次在虚拟机中打开ida,逆向一堆JniOnLoad或者.iniarray段被混淆的代码时。我都想着有没有一种方法,免去这些初始化流程(当然这些初始化操作是厂家的重点防护对象)。
有一天,工作完之后,手头上的jnionload还没有分析完,将VMware中运行的机器设置暂停,明天再分析。突然我想到,既然VMware能通过快照这种方式保存虚拟机的运行时状态,即使关机也可以恢复。那我能不能通过对Android APP”拍摄“一份”快照“(SnapShot),正好停在我想逆向的函数前,修改入参,然后反复运行这份快照,绕过初始化过程、绕过各种检测呢?
思路有了,接下来就是寻找工具,怎么去实现了。
为了能让Unidbg成功载入快照,拍摄内容:
有了目标,现在开始实现。
第一步:装载lldb(相机)
第二步:编写dump脚本
内存段(Segment)、寄存器(Register)的lldb Dump脚本
符号表(SymTab)的lldb Python 脚本:
第三步:按下快门
将脚本导入到lldb:
command script import lldb_dumper.py
command script import lldb_symb.py
附加到目标so:
触发断点以后,通过以下命令得到快照文件:
生成的文件有:
符号表json文件:libxxxxxxx.so_sym.json
dump文件夹:./dump/*
Register json文件:./dump/regs.json
Segment json文件:./dump/segments.json
Section json文件:./dump/sections.json
Unidbg是基于Unicorn增加JNI支持的Unicorn Plus Pro Ultra(就是牛皮哦~),所以Unidbg也通过了一系列Api对寄存器和内存操作:
恢复寄存器状态:
恢复内存状态:
因为执行快照时,并不需要一些内存段,这里过滤一下:
这里内存文件已经zlib压缩,需要解压后再写入内存中。
运行快照:
有时候,会出现Memory unmap的情况,这个时候,在Segments.json,找到对应地址,将name 添加到whileList里面即可。
用unidbg 的libc 替换内存段中的libc,便于调试:
有大佬能帮忙看看这些问题是什么原因么:
$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8/lib/linux/aarch64/lldb-server
adb push lldb-server /data/local/tmp/
adb shell
cd /data/local/tmp
chmod 755 lldb-server
./lldb-server p --server --listen unix-abstract:///data/local/tmp/debug.sock
$ lldb
platform select remote-android
platform connect unix-abstract-connect:///data/local/tmp/debug.sock
$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8/lib/linux/aarch64/lldb-server
adb push lldb-server /data/local/tmp/
adb shell
cd /data/local/tmp
chmod 755 lldb-server
./lldb-server p --server --listen unix-abstract:///data/local/tmp/debug.sock
$ lldb
platform select remote-android
platform connect unix-abstract-connect:///data/local/tmp/debug.sock
from asyncio.log import logger
from datetime import datetime
import hashlib
import json
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List
import sys
import zlib
import os
if TYPE_CHECKING:
from lldb import *
import lldb
black_list = {
'startswith': ['/dev', '/system/fonts', '/dmabuf'],
'endswith': ['(deleted)', '.apk', '.odex', '.vdex', '.dex', '.jar', '.art', '.oat', '.art]'],
'includes': [],
}
dump_path = Path("./dump/")
def __lldb_init_module(debugger: 'SBDebugger', internal_dict: dict):
debugger.HandleCommand('command script add -f lldb_dumper.dumpMem dumpmem')
def dumpMem(debugger: 'SBDebugger', command: str, exe_ctx: 'SBExecutionContext', result: 'SBCommandReturnObject', internal_dict: dict):
target = exe_ctx.GetTarget()
arch_long = dump_arch_info(target)
frame = exe_ctx.GetFrame()
regs = dump_regs(frame)
target = exe_ctx.GetTarget()
sections = dump_memory_info(target)
max_seg_size = 64 * 1024 * 1024
process = exe_ctx.GetProcess()
segments = dump_memory(process, dump_path, black_list, max_seg_size)
(dump_path / "regs.json").write_text(json.dumps(regs))
(dump_path / "sections.json").write_text(json.dumps(sections))
(dump_path / "segments.json").write_text(json.dumps(segments))
def dump_arch_info(target: 'SBTarget'):
triple = target.GetTriple()
print(f'[dump_arch_info] triple => {triple}')
arch, vendor, sys, abi = triple.split('-')
if arch == 'aarch64' or arch == 'arm64':
return 'arm64le'
elif arch == 'aarch64_be':
return 'arm64be'
elif arch == 'armeb':
return 'armbe'
elif arch == 'arm':
return 'armle'
else:
return ''
def dump_regs(frame: 'SBFrame'):
regs = {}
registers = None
for registers in frame.GetRegisters():
print(f'registers name => {registers.GetName()}')
for register in registers:
register_name = register.GetName()
register.SetFormat(lldb.eFormatHex)
register_value = register.GetValue()
regs[register_name] = register_value
print(f'regs => {json.dumps(regs, ensure_ascii=False, indent=4)}')
return regs
def dump_memory_info(target: 'SBTarget'):
logger.debug('start dump_memory_info')
sections = []
for module in target.module_iter():
module: SBModule
for section in module.section_iter():
section: SBSection
module_name = module.file.GetFilename()
start, end, size, name = get_section_info(target, section)
section_info = {
'module': module_name,
'start': start,
'end': end,
'size': size,
'name': name,
}
print(f'Appending: {name}')
sections.append(section_info)
return sections
def get_section_info(tar, sec):
name = sec.name if sec.name is not None else ""
if sec.GetParent().name is not None:
name = sec.GetParent().name + "." + sec.name
module_name = sec.addr.module.file.GetFilename()
module_name = module_name if module_name is not None else ""
long_name = module_name + "." + name
return sec.GetLoadAddress(tar), (sec.GetLoadAddress(tar) + sec.size), sec.size, long_name
def dump_memory(process: 'SBProcess', dump_path: Path, black_list: Dict[str, List[str]], max_seg_size: int):
logger.debug('start dump memory')
memory_list = []
mem_info = lldb.SBMemoryRegionInfo()
start_addr = -1
next_region_addr = 0
while next_region_addr > start_addr:
err = process.GetMemoryRegionInfo(next_region_addr, mem_info)
if not err.success:
logger.warning(f'GetMemoryRegionInfo failed, {err}, break')
break
next_region_addr = mem_info.GetRegionEnd()
if next_region_addr >= sys.maxsize:
logger.info(f'next_region_addr:0x{next_region_addr:x} >= sys.maxsize, break')
break
start = mem_info.GetRegionBase()
end = mem_info.GetRegionEnd()
region_name = 'UNKNOWN'
if mem_info.IsMapped():
name = mem_info.GetName()
if name is None:
name = ''
mem_info_obj = {
'start': start,
'end': end,
'name': name,
'permissions': {
'r': mem_info.IsReadable(),
'w': mem_info.IsWritable(),
'x': mem_info.IsExecutable(),
},
'content_file': '',
}
memory_list.append(mem_info_obj)
for seg_info in memory_list:
try:
start_addr = seg_info['start']
end_addr = seg_info['end']
region_name = seg_info['name']
permissions = seg_info['permissions']
if seg_info['permissions']['r'] is False:
logger.warning(f'Skip dump {region_name} permissions => {permissions}')
continue
predicted_size = end_addr - start_addr
if predicted_size > max_seg_size:
logger.warning(f'Skip dump {region_name} size:0x{predicted_size:x}')
continue
skip_dump = False
for rule in black_list['startswith']:
if region_name.startswith(rule):
skip_dump = True
logger.warning(f'Skip dump {region_name} hit startswith rule:{rule}')
if skip_dump: continue
for rule in black_list['endswith']:
if region_name.endswith(rule):
skip_dump = True
logger.warning(f'Skip dump {region_name} hit endswith rule:{rule}')
if skip_dump: continue
for rule in black_list['includes']:
if rule in region_name:
skip_dump = True
logger.warning(f'Skip dump {region_name} hit includes rule:{rule}')
if skip_dump: continue
ts = datetime.now()
err = lldb.SBError()
seg_content = process.ReadMemory(start_addr, predicted_size, err)
tm = (datetime.now() - ts).total_seconds()
if seg_content is None:
logger.debug(f'Segment empty: @0x{start_addr:016x} {region_name} => {err}')
else:
logger.info(f'Dumping @0x{start_addr:016x} {tm:.2f}s size:0x{len(seg_content):x}: {region_name} {permissions}')
compressed_seg_content = zlib.compress(seg_content)
md5_sum = hashlib.md5(compressed_seg_content).hexdigest() + '.bin'
seg_info['content_file'] = md5_sum
(dump_path / md5_sum).write_bytes(compressed_seg_content)
except Exception as e:
logger.error(f'Exception reading segment {region_name}', exc_info=e)
return memory_list
from asyncio.log import logger
from datetime import datetime
import hashlib
import json
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List
import sys
import zlib
import os
if TYPE_CHECKING:
from lldb import *
import lldb
black_list = {
'startswith': ['/dev', '/system/fonts', '/dmabuf'],
'endswith': ['(deleted)', '.apk', '.odex', '.vdex', '.dex', '.jar', '.art', '.oat', '.art]'],
'includes': [],
}
dump_path = Path("./dump/")
def __lldb_init_module(debugger: 'SBDebugger', internal_dict: dict):
debugger.HandleCommand('command script add -f lldb_dumper.dumpMem dumpmem')
def dumpMem(debugger: 'SBDebugger', command: str, exe_ctx: 'SBExecutionContext', result: 'SBCommandReturnObject', internal_dict: dict):
target = exe_ctx.GetTarget()
arch_long = dump_arch_info(target)
frame = exe_ctx.GetFrame()
regs = dump_regs(frame)
target = exe_ctx.GetTarget()
sections = dump_memory_info(target)
max_seg_size = 64 * 1024 * 1024
process = exe_ctx.GetProcess()
segments = dump_memory(process, dump_path, black_list, max_seg_size)
(dump_path / "regs.json").write_text(json.dumps(regs))
(dump_path / "sections.json").write_text(json.dumps(sections))
(dump_path / "segments.json").write_text(json.dumps(segments))
def dump_arch_info(target: 'SBTarget'):
triple = target.GetTriple()
print(f'[dump_arch_info] triple => {triple}')
arch, vendor, sys, abi = triple.split('-')
if arch == 'aarch64' or arch == 'arm64':
return 'arm64le'
elif arch == 'aarch64_be':
return 'arm64be'
elif arch == 'armeb':
return 'armbe'
elif arch == 'arm':
return 'armle'
else:
return ''
def dump_regs(frame: 'SBFrame'):
regs = {}
registers = None
for registers in frame.GetRegisters():
print(f'registers name => {registers.GetName()}')
for register in registers:
register_name = register.GetName()
register.SetFormat(lldb.eFormatHex)
register_value = register.GetValue()
regs[register_name] = register_value
print(f'regs => {json.dumps(regs, ensure_ascii=False, indent=4)}')
return regs
def dump_memory_info(target: 'SBTarget'):
logger.debug('start dump_memory_info')
sections = []
for module in target.module_iter():
module: SBModule
for section in module.section_iter():
section: SBSection
module_name = module.file.GetFilename()
start, end, size, name = get_section_info(target, section)
section_info = {
'module': module_name,
'start': start,
'end': end,
'size': size,
'name': name,
}
print(f'Appending: {name}')
sections.append(section_info)
return sections
def get_section_info(tar, sec):
name = sec.name if sec.name is not None else ""
if sec.GetParent().name is not None:
name = sec.GetParent().name + "." + sec.name
module_name = sec.addr.module.file.GetFilename()
module_name = module_name if module_name is not None else ""
long_name = module_name + "." + name
return sec.GetLoadAddress(tar), (sec.GetLoadAddress(tar) + sec.size), sec.size, long_name
def dump_memory(process: 'SBProcess', dump_path: Path, black_list: Dict[str, List[str]], max_seg_size: int):
logger.debug('start dump memory')
memory_list = []
mem_info = lldb.SBMemoryRegionInfo()
start_addr = -1
next_region_addr = 0
while next_region_addr > start_addr:
err = process.GetMemoryRegionInfo(next_region_addr, mem_info)
if not err.success:
logger.warning(f'GetMemoryRegionInfo failed, {err}, break')
break
next_region_addr = mem_info.GetRegionEnd()
if next_region_addr >= sys.maxsize:
logger.info(f'next_region_addr:0x{next_region_addr:x} >= sys.maxsize, break')
break
start = mem_info.GetRegionBase()
end = mem_info.GetRegionEnd()
region_name = 'UNKNOWN'
if mem_info.IsMapped():
name = mem_info.GetName()
if name is None:
name = ''
mem_info_obj = {
'start': start,
'end': end,
'name': name,
'permissions': {
'r': mem_info.IsReadable(),
'w': mem_info.IsWritable(),
'x': mem_info.IsExecutable(),
},
'content_file': '',
}
memory_list.append(mem_info_obj)
for seg_info in memory_list:
try:
start_addr = seg_info['start']
end_addr = seg_info['end']
region_name = seg_info['name']
permissions = seg_info['permissions']
if seg_info['permissions']['r'] is False:
logger.warning(f'Skip dump {region_name} permissions => {permissions}')
continue
predicted_size = end_addr - start_addr
if predicted_size > max_seg_size:
logger.warning(f'Skip dump {region_name} size:0x{predicted_size:x}')
continue
skip_dump = False
for rule in black_list['startswith']:
if region_name.startswith(rule):
skip_dump = True
logger.warning(f'Skip dump {region_name} hit startswith rule:{rule}')
if skip_dump: continue
for rule in black_list['endswith']:
if region_name.endswith(rule):
skip_dump = True
logger.warning(f'Skip dump {region_name} hit endswith rule:{rule}')
if skip_dump: continue
for rule in black_list['includes']:
if rule in region_name:
skip_dump = True
logger.warning(f'Skip dump {region_name} hit includes rule:{rule}')
if skip_dump: continue
ts = datetime.now()
err = lldb.SBError()
seg_content = process.ReadMemory(start_addr, predicted_size, err)
tm = (datetime.now() - ts).total_seconds()
if seg_content is None:
logger.debug(f'Segment empty: @0x{start_addr:016x} {region_name} => {err}')
else:
logger.info(f'Dumping @0x{start_addr:016x} {tm:.2f}s size:0x{len(seg_content):x}: {region_name} {permissions}')
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!