首页
社区
课程
招聘
[原创]一种通过“快照”的方式绕过So初始化时的检测
发表于: 2022-8-26 17:51 28103

[原创]一种通过“快照”的方式绕过So初始化时的检测

2022-8-26 17:51
28103

本文的思路是很久以前就有的,但是找不到一个合适的工具“拍摄快照”一直都没有成功实现,直到看到这篇文章才成功实现,感谢该文作者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,便于调试:

有大佬能帮忙看看这些问题是什么原因么:

 
 
 
#获取lldb android 服务端
$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8/lib/linux/aarch64/lldb-server
 
#ADB导入设备、赋予权限
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
 
#pc端连接android
$ lldb
 platform select remote-android
 platform connect unix-abstract-connect:///data/local/tmp/debug.sock
#获取lldb android 服务端
$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/9.0.8/lib/linux/aarch64/lldb-server
 
#ADB导入设备、赋予权限
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
 
#pc端连接android
$ 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() # type: SBTarget
    arch_long = dump_arch_info(target)
    frame = exe_ctx.GetFrame() # type: SBFrame
    regs = dump_regs(frame)
 
    target = exe_ctx.GetTarget() # type: SBTarget
    sections = dump_memory_info(target)
 
    max_seg_size = 64 * 1024 * 1024
# dump内存
    process = exe_ctx.GetProcess() # type: SBProcess
    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}')
    # 'aarch64', 'unknown', 'linux', 'android'
    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 = {} # type: Dict[str, int]
    registers = None # type: List[SBValue]
    for registers in frame.GetRegisters():
        # - General Purpose Registers
        # - Floating Point Registers
        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,
            }
            # size 好像有负数的情况 不知道是什么情况
            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) # type: SBError
        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)
    # 开始正式dump
    for seg_info in memory_list:
        try:
            start_addr = seg_info['start'] # type: int
            end_addr = seg_info['end'] # type: int
            region_name = seg_info['name'] # type: str
            permissions = seg_info['permissions'] # type: Dict[str, bool]
 
            # 跳过不可读 之后考虑下是不是能修改权限再读
            if seg_info['permissions']['r'] is False:
                logger.warning(f'Skip dump {region_name} permissions => {permissions}')
                continue
 
            # 超过预设大小的 跳过dump
            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()
            # 读取成功的才写入本地文件 并计算md5
            # 内存里面可能很多地方是0 所以压缩写入文件 减少占用
            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:
            # 这里好像不会出现异常 因为前面有 SBError 处理了 不过还是保留
            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() # type: SBTarget
    arch_long = dump_arch_info(target)
    frame = exe_ctx.GetFrame() # type: SBFrame
    regs = dump_regs(frame)
 
    target = exe_ctx.GetTarget() # type: SBTarget
    sections = dump_memory_info(target)
 
    max_seg_size = 64 * 1024 * 1024
# dump内存
    process = exe_ctx.GetProcess() # type: SBProcess
    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}')
    # 'aarch64', 'unknown', 'linux', 'android'
    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 = {} # type: Dict[str, int]
    registers = None # type: List[SBValue]
    for registers in frame.GetRegisters():
        # - General Purpose Registers
        # - Floating Point Registers
        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,
            }
            # size 好像有负数的情况 不知道是什么情况
            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) # type: SBError
        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)
    # 开始正式dump
    for seg_info in memory_list:
        try:
            start_addr = seg_info['start'] # type: int
            end_addr = seg_info['end'] # type: int
            region_name = seg_info['name'] # type: str
            permissions = seg_info['permissions'] # type: Dict[str, bool]
 
            # 跳过不可读 之后考虑下是不是能修改权限再读
            if seg_info['permissions']['r'] is False:
                logger.warning(f'Skip dump {region_name} permissions => {permissions}')
                continue
 
            # 超过预设大小的 跳过dump
            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()
            # 读取成功的才写入本地文件 并计算md5
            # 内存里面可能很多地方是0 所以压缩写入文件 减少占用
            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}')

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 18
支持
分享
最新回复 (21)
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
近期一直纠结于怎么构造参数,看到这个思路打开了
2022-8-27 07:46
0
雪    币: 4038
活跃值: (3427)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
666
2022-8-30 11:55
0
雪    币: 435
活跃值: (2636)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
4
StriveMario 666
菜死了 哥(捕捉Mario)
2022-8-30 11:59
0
雪    币: 2290
活跃值: (2180)
能力值: (RANK:400 )
在线值:
发帖
回帖
粉丝
5
牛逼!
2022-8-30 17:29
0
雪    币: 3351
活跃值: (13993)
能力值: ( LV9,RANK:230 )
在线值:
发帖
回帖
粉丝
6
so检测unidbg刻不容缓
2022-8-30 17:30
0
雪    币: 7198
活跃值: (21960)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
7
牛逼
2022-8-30 17:59
0
雪    币: 2141
活跃值: (4522)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
unidbg检测点应该很多吧 龙哥说几百个都不止
2022-9-1 09:52
0
雪    币: 415
活跃值: (2633)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
真是个人才啊,楼主骨络清奇!!!
2022-9-2 10:39
0
雪    币: 4423
活跃值: (3088)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
10
思路很好啊,学习了
2022-9-2 11:20
0
雪    币: 14488
活跃值: (17488)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
11
感谢分享
2022-9-2 15:31
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
学习了,感谢分享
2022-9-3 20:11
0
雪    币: 105
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
666,mark
2022-9-5 18:20
0
雪    币: 27
活跃值: (363)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
kkkk
2022-9-7 10:16
0
雪    币: 2295
活跃值: (3000)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
15

大佬有没有想过用frida stalker去获取运行时的寄存器,如果用lldb 还需要过反调试 

最后于 2022-10-6 16:21 被zhuzhu_biu编辑 ,原因:
2022-10-6 16:17
1
雪    币: 158
活跃值: (1096)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
请问有没有完整代码库呀
2023-9-25 10:11
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
大佬,这个思路是不是可以用于ttd呢
2024-1-4 09:38
0
雪    币: 435
活跃值: (2636)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
18
mb_ldbucrik 大佬,这个思路是不是可以用于ttd呢
这个思路还是远远不够的,ttd有太多东西需要处理了. 在我的理解中,ttd是record all + replay all. 这个思路只是snapshot.
2024-1-9 10:26
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
FANGG3 这个思路还是远远不够的,ttd有太多东西需要处理了. 在我的理解中,ttd是record all + replay all. 这个思路只是snapshot.
好的,目前在网上也没有找到带有ttd功能的arm调试器,win的x96debug到是有这个功能
2024-1-10 09:19
0
雪    币: 29
活跃值: (1109)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
No such file or directory: 'dump/regs.json'
2024-9-20 14:19
0
雪    币: 29
活跃值: (1109)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
优质胖虎 No such file or directory: 'dump/regs.json'
jiejuele
2024-9-20 14:25
0
雪    币: 27
活跃值: (1618)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
很棒的实现啊,支持楼主
2024-9-22 22:39
0
游客
登录 | 注册 方可回帖
返回
//