【Android逆向】脱壳项目 frida-dexdump 原理分析

android,逆向,脱壳,项目,frida,dexdump,原理,分析 · 浏览次数 : 184

小编点评

该代码中关于字符串 ID 的值解析逻辑如下: 1. **`range_end`** 是 `range` 的结束位置,用于计算 `map_offset`。 2. **`map_offset`** 是 `map_size` 的偏移量。 3. **`map_address`** 是 `map_offset` 加上 `map_size` 的结果。 4. **`maps_end`** 是 `maps_address` 加上 `map_size` 的结果。 5. **`maps_end`** 是 `map_base` 的结束位置。 通过比较 **`maps_end`** 和 **`range_end`**,可以确定 `map_off` 是否与 **`maps_base`** 相匹配。如果匹配,则表示字符串 ID 是正确配置的。 此外,代码还检查了以下条件: * **`map_size`** 的值是否在 2 到 50 之间的范围。 * **`maps_base`** 的地址是否在 **`range_base`** 和 **`range_end`** 之间的范围内。 通过这些验证,可以确保字符串 ID 的格式正确,并有效地从 DEX 中获取数据。

正文

1. 项目代码地址

https://github.com/hluwa/frida-dexdump

2. 核心逻辑为

def dump(self):
        logger.info("[+] Searching...")
        st = time.time()
        ranges = self.agent.search_dex(enable_deep_search=self.enable_deep)
        et = time.time()
        logger.info("[*] Successful found {} dex, used {} time.".format(len(ranges), int(et - st)))
        logger.info("[+] Starting dump to '{}'...".format(self.output))
        idx = 1
        for dex in ranges:
            try:
                bs = self.agent.memory_dump(dex['addr'], dex['size'])
                md = md5(bs)
                if md in self.mds:
                    continue
                self.mds.add(md)
                bs = fix_header(bs)
                out_path = os.path.join(self.output, "classes{}.dex".format('%d' % idx if idx != 1 else ''))
                with open(out_path, 'wb') as out:
                    out.write(bs)
                logger.info("[+] DexMd5={}, SavePath={}, DexSize={}"
                            .format(md, out_path, hex(dex['size'])))
                idx += 1
            except Exception as e:
                logger.exception("[-] {}: {}".format(e, dex))
        logger.info("[*] All done...")

这里面有三个比较核心的方法 1 self.agent.search_dex 2 self.agent.memory_dump 3 fix_header, 分别对应着在内存中寻找dex,然后dump下来 ,最后修后dex的header

3. search_dex 分析

// /agent/src/search.ts

export function searchDex(deepSearch: boolean) {
    const result: any = [];
    Process.enumerateRanges('r--').forEach(function (range: RangeDetails) {
        try {
            Memory.scanSync(range.base, range.size, "64 65 78 0a 30 ?? ?? 00").forEach(function (match) {
                ......
                }
            });

            if (deepSearch) {
                Memory.scanSync(range.base, range.size, "70 00 00 00").forEach(function (match) {
                    const dex_base = match.address.sub(0x3C);
                    if (dex_base < range.base) {
                        return;
                    }
                    if (dex_base.readCString(4) != "dex\n" && verify(dex_base, range, true)) {
                        const real_dex_size = get_dex_real_size(dex_base, range.base, range.base.add(range.size));
                        if (!verify_ids_off(dex_base, real_dex_size)) {
                            return;
                        }
                        result.push({
                            "addr": dex_base,
                            "size": real_dex_size
                        });
                        const max_size = range.size - dex_base.sub(range.base).toInt32();
                        if (max_size != real_dex_size) {
                            result.push({
                                "addr": dex_base,
                                "size": max_size
                            });
                        }
                    }
                })
            } else {
                if (range.base.readCString(4) != "dex\n" && verify(range.base, range, true)) {
                    const real_dex_size = get_dex_real_size(range.base, range.base, range.base.add(range.size));
                    result.push({
                        "addr": range.base,
                        "size": real_dex_size
                    });
                }
            }

        } catch (e) {
        }
    });

    return result;
}

原理:

  1. Process.enumerateRanges('r--') 枚举可读的内存区块
    2.1. 非深度搜索 :Memory.scanSync(range.base, range.size, "64 65 78 0a 30 ?? ?? 00"),这里就是在搜索 DEX.035. 但是很多加壳软件会修改掉这个DEX的标志(不影响虚拟机的加载,但会影响静态分析),这里就不展开解读了
    2.2 深度搜索: Memory.scanSync(range.base, range.size, "70 00 00 00") , DEX 头的大小是 0x70 = 112,而紧挨着头的是string_ids区域,那么它的偏移必然是0x70,这个值是固定的,那么stringIdsOffset在DEX header里的值必然是"70 00 00 00",可以通过搜索它来缩小一波范围,成为怀疑对象
  2. 执行verify 来确认是不是dex
function verify(dexptr: NativePointer, range: RangeDetails, enable_verify_maps: boolean): boolean {

    if (range != null) {
        var range_end = range.base.add(range.size);
        // verify header_size
        if (dexptr.add(0x70) > range_end) {
            return false;
        }

        if (enable_verify_maps) {

            var maps_address = get_maps_address(dexptr, range.base, range_end);
            if (!maps_address) {
                return false;
            }

            var maps_end = get_maps_end(maps_address, range.base, range_end);
            if (!maps_end) {
                return false;
            }
            return verify_by_maps(dexptr, maps_address)
        } else {
            return dexptr.add(0x3C).readUInt() === 0x70;
        }
    }

    return false;

}

其他的比较简单,核心是 verify_by_maps

function verify_by_maps(dexptr: NativePointer, mapsptr: NativePointer): boolean {
    const maps_offset = dexptr.add(0x34).readUInt();
    const maps_size = mapsptr.readUInt();
    for (let i = 0; i < maps_size; i++) {
        const item_type = mapsptr.add(4 + i * 0xC).readU16();
        if (item_type === 4096) {
            const map_offset = mapsptr.add(4 + i * 0xC + 8).readUInt();
            if (maps_offset === map_offset) {
                return true;
            }
        }
    }
    return false;
}

通过 map_off 找到 DEX 的 map_list, 通过解析它,并得到type为 TYPE_MAP_LIST(4096) 的item。理论上讲,这个条目里面的索引值应该要与 map_off 一致,那么通过校验这两个地方,就可以实现一个更加精确的验证方案。

这里涉及到Dex MapItem的数据结构

  struct MapItem {
     uint16_t type_;
     uint16_t unused_;
     uint32_t size_;
     uint32_t offset_;
   };

确认完毕后,开始获取dex的大小get_dex_real_size,(内存中的 DEX Header 并不只有 magic 可以抹掉,还有另一个运行时无关但对我们至关重要的字段:file_size,也就是文件的大小)

function get_dex_real_size(dexptr: NativePointer, range_base: NativePointer, range_end: NativePointer): Number {
    const dex_size = dexptr.add(0x20).readUInt();

    const maps_address = get_maps_address(dexptr, range_base, range_end);
    if (!maps_address) {
        return dex_size;
    }

    const maps_end = get_maps_end(maps_address, range_base, range_end);
    if (!maps_end) {
        return dex_size;
    }

    return maps_end.sub(dexptr).toInt32();
}

function get_maps_address(dexptr: NativePointer, range_base: NativePointer, range_end: NativePointer): NativePointer | null {
    const maps_offset = dexptr.add(0x34).readUInt();
    if (maps_offset === 0) {
        return null;
    }

    const maps_address = dexptr.add(maps_offset);
    if (maps_address < range_base || maps_address > range_end) {
        return null;
    }

    return maps_address;
}

function get_maps_end(maps: NativePointer, range_base: NativePointer, range_end: NativePointer): NativePointer | null {
    const maps_size = maps.readUInt();
    if (maps_size < 2 || maps_size > 50) {
        return null;
    }
    const maps_end = maps.add(maps_size * 0xC + 4);
    if (maps_end < range_base || maps_end > range_end) {
        return null;
    }

    return maps_end;
}

原理就是根据range的大小,和dex mapSize中的信息,确认出maps_end的地址,因为map就是dex的结尾,
获得了maps_end的地址,减去dexptr,即为整个dex的实际大小

searchDex最后 将数据写入到result返回
                        result.push({
                            "addr": dex_base,
                            "size": real_dex_size
                        });

4. 找到dex在内存中的位置了,就执行memory_dump 将数据dump出来

5. 然后修复dex头 fix_header

def fix_header(dex_bytes):
    import struct
    dex_size = len(dex_bytes)

    if dex_bytes[:4] != b"dex\n":
        dex_bytes = b"dex\n035\x00" + dex_bytes[8:]

    if dex_size >= 0x24:
        dex_bytes = dex_bytes[:0x20] + struct.Struct("<I").pack(dex_size) + dex_bytes[0x24:]

    if dex_size >= 0x28:
        dex_bytes = dex_bytes[:0x24] + struct.Struct("<I").pack(0x70) + dex_bytes[0x28:]

    if dex_size >= 0x2C and dex_bytes[0x28:0x2C] not in [b'\x78\x56\x34\x12', b'\x12\x34\x56\x78']:
        dex_bytes = dex_bytes[:0x28] + b'\x78\x56\x34\x12' + dex_bytes[0x2C:]

    return dex_bytes

这里一共做了四件事

  1. 修复magic, dex.035。
  2. 修复filesize
  3. 修复stringIdsOffset
  4. 修复小端特征 "78 56 34 12"

与【Android逆向】脱壳项目 frida-dexdump 原理分析相似的内容:

【Android逆向】脱壳项目 frida-dexdump 原理分析

1. 项目代码地址 https://github.com/hluwa/frida-dexdump 2. 核心逻辑为 def dump(self): logger.info("[+] Searching...") st = time.time() ranges = self.agent.search_

【Android逆向】脱壳项目frida_dump 原理分析

脱dex核心文件dump_dex.js 核心函数 function dump_dex() { var libart = Process.findModuleByName("libart.so"); var addr_DefineClass = null; var symbols = libart.e

【Android逆向】制作Fart脱壳机,完成对NCSearch的脱壳操作

1. 我的手机是Pixel 1 ,下载fart对应的镜像 镜像位置具体参考大佬博客 https://www.anquanke.com/post/id/201896 2 执行 adb reboot bootloader ——重启手机到fastboot模式, 直接重启手机到fastboot模式,不用关机

【Android逆向】制作Youpk脱壳机,完成对NCSearch的脱壳操作

1. 拉去youpk 代码或镜像,自行编译构建 youpk 代码地址 https://github.com/youlor/unpacker 2. 执行 adb reboot bootloader 3. 执行 sh flash-all.sh 4. 安装NCSearch,并启动app 5. 执行adb

android 逆向笔记

壳检测工具 GDA 2. 逆向分析APP 一般流程 1. 使用自动化检测工具检测APP是否加壳,或者借助一些反编译工具依靠经验判断是否加壳 2. 如果apk加壳,则需要先对apk进行脱壳 3. 使用`jeb`, `jadx`, `apktool`等反编译工具对apk进行反编译 4. 先依据静态分析得

【Android逆向】frida 破解 jwxdxnx02.apk

apk 路径: https://pan.baidu.com/s/1cUInoi 密码:07p9 这题比较简单,主要是用于练习frida 1. 安装apk到手机 需要输入账号密码 2. 使用jdax 查看apk package hfdcxy.com.myapplication; import andr

[Android逆向]Exposed 破解 jwxdxnx02.apk

使用exposed 遇到了一些坑,这里记录一下 源码: package com.example.exposedlesson01; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.X

[Android 逆向]frida 破解 切水果大战原版.apk

1. 手机安装该apk,运行,点击右上角礼物 提示 支付失败,请稍后重试 2. apk拖入到jadx中,待加载完毕后,搜素失败,找到疑似目标类MymmPay的关键方法payResultFalse 4. adb logcat 或者androidstudio 查看该进程的日志,发现以下日志 com.mf

[Android 逆向]Xposed 破解 切水果大战原版.apk

代码 public class Main implements IXposedHookLoadPackage { boolean flag = false; @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam

【Android逆向】frida 破解 滚动的天空

1. apk 安装到手机中 2. 玩十次之后,会提示 充值 3. adb shell dumpsys window | grep mCurrentFocus 查看一些当前activity是哪一个 是 AppActivity 4. 阅读代码,感觉是unity3d做的游戏 5. apk拖入到jadx中,