Frida07 - dexdump核心源码分析

项目地址

github.com/hluwa/frida...

代码解析

项目中的核心函数是 searchDex:

javascript 复制代码
function searchDex(deepSearch) {
    var result = [];
    Process.enumerateRanges('r--').forEach(function (range) {
        try {
            ....
        } catch (e) {
        }
    });
    return result;
}

里面用了一个新的API,Process.enumerateRanges,我们看一下API介绍:

enumerates memory ranges satisfying protection given as a string of the form: rwx, where rw- means "must be at least readable and writable".

使用这个API可以在进程中搜索所有可读的内存段,我们可以直接传递 'r---' 的形式,也可以传递一个对象:{protection: '---', coalesce: true } ,coalesce 的值表示是否需要合并相同权限的内存段,默认是 false。

这个函数会返回一个数组对象,里面的元素有如下属性:

  1. base:基地址,NativePointer,可以理解为C里面的指针。
  2. size:内存块大小,in bytes
  3. protection:保护属性,string
  4. file:(如果有的话)内存映射文件:
    1. path,文件路径,string
    2. offset,文件内偏移,in bytes
    3. size,文件大小,in bytes

继续看源码:

python 复制代码
Memory.scanSync(range.base, range.size, "64 65 78 0a 30 ?? ?? 00").forEach(function (match) {
    if (range.file && range.file.path && (range.file.path.startsWith("/data/dalvik-cache/") || range.file.path.startsWith("/system/"))) {
        return;
    }

    if (verify(match.address, range, false)) {
        var dex_size = get_dex_real_size(match.address, range.base, range.base.add(range.size));
        result.push({
            "addr": match.address,
            "size": dex_size
        });
        var max_size = range.size - match.address.sub(range.base).toInt32();

        if (deepSearch && max_size != dex_size) {
            result.push({
                "addr": match.address,
                "size": max_size
            });
        }
    }
});

又用到了一个新的API,Memory.scanSync,看看文档介绍:

scan memory for occurrences of pattern in the memory range given by address and size.

就是按照 pattern 给定的模式来搜索指定范围的内存是否又匹配的。

ruby 复制代码
64 65 78 0a 30 ?? ?? 00

表示搜索的模式是 以 64 65 78 0a 30 字节开头的,中间两个字节不关心,后面跟着一个 00 的8个字节,如果有满足的则触发回调。

为啥要搜索这几个字节呢?是因为这几个字节是 dex 的文件魔数。可以看下官方文档介绍:

source.android.com/docs/core/r...

作者设置的比较宽泛,中间的两个字节表示的是 dex 的版本号,会搜索所有版本号的 dex。

文档介绍 pattern 还有一个 r2-style 的写法,但是搜了一下没看太明白,就不说了。

回调会传递一个对象,里面的属性有:

  1. onMatch: function(address, size): 扫描到一个内存块,起始地址是address ,大小size 的内存块,返回字符串 stop 表示停止扫描
  2. onError: function(reason): 扫描内存的时候出现内存访问异常的时候回调
  3. onComplete: function(): 内存扫描完毕的时候调用

再回到源码:

go 复制代码
if (range.file && range.file.path && (range.file.path.startsWith("/data/dalvik-cache/") || range.file.path.startsWith("/system/"))) {
    return;
}

系统app的dex,我们不需要。

python 复制代码
if (verify(match.address, range, false)) {
    var dex_size = get_dex_real_size(match.address, range.base, range.base.add(range.size));
    result.push({
        "addr": match.address,
        "size": dex_size
    });
    var max_size = range.size - match.address.sub(range.base).toInt32();

    if (deepSearch && max_size != dex_size) {
        result.push({
            "addr": match.address,
            "size": max_size
        });
    }
}

verify 函数是对 dex 进行校验,主要是根据自己对 dex 文件的熟悉程度来做校验。

比如 dex 文件应该至少有 0x70 个字节,因为这是 dex 文件头的大小。

比如,0x3c位置的字节必定是 0x70,因为文件头后面跟着的就是字符串。

作者还开了一个深度验证,利用maps,其实原理很简单,我们使用010editor打开一个dex:

文件头里面有一个 map_off 字段,它的值是 map_list 段在dex文件内的偏移。

我们再看 map_list 段:

这里也储存了自身的一个偏移,那么根据这两个东西,就可以认为这个是dex文件。

具体代码如下:

ini 复制代码
function verify_by_maps(dexptr, mapsptr) {
    var maps_offset = dexptr.add(0x34).readUInt();
    var maps_size = mapsptr.readUInt();

    for (var i = 0; i < maps_size; i++) {
        var item_type = mapsptr.add(4 + i * 0xC).readU16();

        if (item_type === 4096) {
            var map_offset = mapsptr.add(4 + i * 0xC + 8).readUInt();

            if (maps_offset === map_offset) {
                return true;
            }
        }
    }

    return false;
}

然后再计算 map_list 结束的位置:

kotlin 复制代码
function get_maps_end(maps, range_base, range_end) {
    var maps_size = maps.readUInt();

    if (maps_size < 2 || maps_size > 50) {
        return null;
    }

    var maps_end = maps.add(maps_size * 0xC + 4);

    if (maps_end < range_base || maps_end > range_end) {
        return null;
    }

    return maps_end;
}

最后通过减掉起始地址,就可以得到真正的文件大小了:

ini 复制代码
function get_dex_real_size(dexptr, range_base, range_end) {
    var dex_size = dexptr.add(0x20).readUInt();
    var maps_address = get_maps_address(dexptr, range_base, range_end);

    if (!maps_address) {
        return dex_size;
    }

    var maps_end = get_maps_end(maps_address, range_base, range_end);

    if (!maps_end) {
        return dex_size;
    }

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

如果开了深度搜索,匹配方式又有不同:

go 复制代码
Memory.scanSync(range.base, range.size, "70 00 00 00").forEach(function (match) {
    var dex_base = match.address.sub(0x3C);

    if (dex_base < range.base) {
        return;
    }

    if (dex_base.readCString(4) != "dex\n" && verify(dex_base, range, true)) {
        var 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
        });
        var max_size = range.size - dex_base.sub(range.base).toInt32();

        if (max_size != real_dex_size) {
            result.push({
                "addr": dex_base,
                "size": max_size
            });
        }
    }
});

70 00 00 00 是dex文件头里面字符串的偏移段。这是因为有些加固厂商会修改 dex 的魔数,所以作者选择了这种匹配方式。

可以看到,逆向的重心,除了api用的熟之外,还需要对app本身的相关知识要有足够的了解才行。

相关推荐
沿着路走到底29 分钟前
JS事件循环
java·前端·javascript
子春一21 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶1 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied3 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一23 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
羽沢314 小时前
ECharts 学习
前端·学习·echarts