JADX安全性研究
枫糖甜酒 发表于 北京 技术文章 1983浏览 · 2024-01-28 14:03

研究了apktool - https://xz.aliyun.com/t/13354,怎么能不研究一下jadx~

本文会分析JADX的历史漏洞,并对其安全性进行研究

历史漏洞

找了一下历史漏洞

其中CVE-2022-39259和CVE-2022-0219是JADX本身的问题
果不其然JADX也存在过XXE漏洞(CVE-2022-0219) - https://huntr.com/bounties/0d093863-29e8-4dd7-a885-64f76d50bf5e/
回到修复前的版本,查看XML解析部分

发现其实XmlSecurity在这个时候已经实现了,但是在实际解析的时候没有调用 orz

所以修复是通过 https://github.com/Haxatron/jadx/commit/c6a78c0d6dc990a4a0f8962d51823aa6ca3aefd2
调用了自己实现的XmlSecurity,安全限制的代码为

public static DocumentBuilderFactory getSecureDbf() throws ParserConfigurationException {
    synchronized (XmlSecurity.class) {
        if (secureDbf == null) {
            secureDbf = DocumentBuilderFactory.newInstance();
            secureDbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            secureDbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            secureDbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            secureDbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            secureDbf.setFeature("http://apache.org/xml/features/dom/create-entity-ref-nodes", false);
            secureDbf.setXIncludeAware(false);
            secureDbf.setExpandEntityReferences(false);
        }
    }
    return secureDbf;
}

其中包含了禁用外部DTD加载、外部通用实体等操作
并且需要解析XML的地方也调用的是安全方法,没有再自定义实现其他的解析XML的方法

看起来这部分确实修补完善了

路径穿越任意文件覆盖寻找

那JADX上是否会存在路径穿越任意文件覆盖漏洞呢 就像APKTool一样,搭建好本地的环境之后进行动态调试
构造了恶意APK发现名字变成了

全局搜索调用

可以看到调用点是 getResAlias 方法

private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) {
    String name;
    if (constField == null || constField.getTopParentClass().isSynthetic()) {
        name = origKeyName;
    } else {
        name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
    }
    Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name);
    if (matcher.matches()) {
        return name;
    }
    // Making sure origKeyName compliant with resource file name rules
    String cleanedResName = cleanName(matcher);
    String newResName = String.format("res_0x%08x", resRef);
    if (cleanedResName.isEmpty()) {
        return newResName;
    }
    // autogenerate key name, appended with cleaned origKeyName to be human-friendly
    return newResName + "_" + cleanedResName.toLowerCase();
}

我们构造的恶意payload会在这个地方被过滤

过滤规则为

private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");

解释一下这个模式的含义:
[\w\d_]+: 这部分表示匹配一个或多个字符,其中包括以下内容:

  • \w: 表示任何字母、数字或下划线。相当于字符类\w。
  • \d: 表示任何数字。相当于字符类\d。
  • _: 表示下划线字符本身。

因此,整个模式表示匹配由字母、数字或下划线组成的一个或多个字符的字符串。在Java中,这个正则表达式通常用于验证字符串是否符合特定的命名规则,例如变量名或键名。
返回修正后的结果

所以这部分的目录穿越是被限制了,往回看调用该方法的地方

private String getResName(String typeName, int resRef, String origKeyName) {
    if (this.useRawResName) {
        return origKeyName;
    }
    String renamedKey = resStorage.getRename(resRef);
    if (renamedKey != null) {
        return renamedKey;
    }
    // styles might contain dots in name, search for alias only for resources names
    if (typeName.equals("style")) {
        return origKeyName;
    }
    FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
    String resAlias = getResAlias(resRef, origKeyName, constField);
    resStorage.addRename(resRef, resAlias);
    if (constField != null) {
        constField.rename(resAlias);
        constField.add(AFlag.DONT_RENAME);
    }
    return resAlias;
}

除了 getResAlias 方法外,还有三个地方会直接返回 origKeyName,我们分别看一下这三个地方是否存在漏洞

useRawResName属性


当 this.useRawResName 为true的时候,就会直接返回原始的名字,所以我们查看调用
第一处是初始化ResTableParser,调用默认传入的是false

第二处调用传入是 true,但是看起来是一个测试方法

public static void main(String[] args) throws IOException {
    .......
    for (Path resFile : inputPaths) {
        LOG.info("Processing {}", resFile);
        ResTableParser resTableParser = new ResTableParser(root, true);

所以这里的利用基本上是没办法了,除非有人在集成jadx的时候自作聪明的传入了true

renamedKey重命名


当这个name对应的resRef在resStorage已经存在的时候 就会直接返回它之前的名字
这个方法为

public String getRename(int id) {
    return renames.get(id);
}

renames是一个Map

private final Map<Integer, String> renames = new HashMap<>();

什么情况下resStorage会存在resRef

对于每一个元素获取了resAlias之后 都会以put 的方式存储到 renames里面去,getResAlias方法就是前面我们提到的过滤方法,所以这里即便重复,也是获取的过滤后的名称,无法实现路径穿越

style类型


这里的注释说因为类型为style的文件可能文件名会包含 点 ,所以不需要过滤就直接返回,那我们可以控制style文件来实现目录穿越吗

控制type为style的文件内容


这里的name到时候就会直接被return ,问题在于这里的name即便我们控制了 也无法造成实质上的危害

我们修改这部分

现在这部分变成了

使用JDAX,可以看到因为我们控制的不是整个文件的名字 而是style加载的name,所以无法实现攻击

将raw文件的类型改为style(鸡肋的安全问题)

这部分需要分析resources.arsc文件结构
resources.arsc是Android编译后生成的产物,是一个二进制文件,主要用来建立资源映射关系,其内部结构的定义在ResourceTypes.h文件中有详细的描述,文件的详细结构图已经有人画好了

这里的二进制文件我们使用010editor进行分析,可以下载一个AndroidResource.bt模板方便查看结构

resources.arsc在文件中的分布采用的是小端存储的分布方式,文件的每一个部分都由一个chunk结构打头,通过chunk中的type可以知道这一个chunk是什么类型,从而进行不同的解析。
对应关系在 ResourceTypes.h https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h
比如一开始的 0002

对应的就是RES_TABLE_TYPE

这里010editor的模板也为我们解析了

找到我们想要修改的attack_file的类型(这里的文件类型和它的文件内容所在的区块不是在一起的,但是会存在映射关系,如果没有010editor的模板,手动去找很难找到,模板寻找的原理是通过起始位置和偏移量去计算的)

id值会对应其类型 id为12也就是0c,0c就是指raw类型

跟AndroidStudio的APK分析器得到的ID是对应的
将这部分对应修改为 0E,0E就是指style类型,最后得到

修改之后重新打包,这个时候debug也可以看到jadx把我们当作了style

构造一个 ../../hfile,JADX解析之后变成了

但是这个时候好像是保存在内存里面,找不到这个文件,所以我选直接保存

保存之后变成了

激动的手颤抖的心~
和Red256一起开起了香槟(事实证明不应该半场开香槟~
尝试进行更大的路径穿越,因为在对代码debug调试的过程中发现jadx是会先运行到temp目录下生成一个临时文件夹 路径大概是这样的

C:\Users\用户名\AppData\Local\Temp\jadx-instance-5750059918677153939

按照我们前面的理论,虽然这里是临时文件夹 但是如果能够正常穿越出来,那么就能在这个文件夹被删除之前让恶意文件逃逸出来,实现任意文件的覆盖
所以构造一个 ..........\hiiiiiiii,让它刚好能穿越到Temp目录下 来逃脱被删除的命运
结果试了好几次不行,最后发现导出的时候会报错,nmd

完整报错为

ERROR: Invalid resource name or path traversal attack detected: E:\temp\apktool sec\test\resources\res\style..........\hiiiiiiiiiiiii

搜索代码里面的检测方法为

具体的检测代码为

private static boolean isInSubDirectoryInternal(File baseDir, File file) {
    File current = file;
    while (true) {
        if (current == null) {
            return false;
        }
        if (current.equals(baseDir)) {
            return true;
        }
        current = current.getParentFile();
    }
}

将需要保存的路径跟根路径进行对比,如果不相同 则获取保存路径的父目录 继续对比,直到相同 或者目录为null
Java是强类型语言 这里的equals没啥绕过的可能性,前面的baseDir也无法自定义,一眼为寄
所以最后这个就是一个只能够覆盖resources文件夹下的任意文件的鸡肋问题,报告给skylot后在JADX的最新版本上已经对该问题进行了修复
https://github.com/skylot/jadx/commit/d86449a8ea26381d0ce6fafaed7deb7542dfd70b

参考链接

0 条评论
某人
表情
可输入 255