给aapt添加支持uincode类名功能

给aapt添加支持uincode类名功能

aapt 是什么?

aapt是Android App资源打包工具。

为什么有这个需求

  1. Android的开发语言Java用uincode字符作为类名、字段名和方法名。
  2. 现实中专业开发人员很少用uincode字符作为类名、字段名和方法名。
  3. aapt在开发时没有考虑做这种支持,但其实资源文件中和AndroidManifest.xml中会引用到java类名(Activity类,自定义View类等)。
  4. 一些商业代码混淆工具利用这个特性增加逆向工程的难度。如把类名混淆为不可见unicode字符,影响人类阅读分析,但不影响虚拟机执行。
  5. 资源文件和AndroidManifest.xml中的类也需要替换为混淆后的类名。这些商业工具在混淆的时候也对资源文件和AndroidManifest.xml进行了相应处理。
  6. Android逆向人员重新打包App时,需要使用到aapt工具,但aapt无法处理这种情况。

如何解决

问题的表象

直接执行aapt会得到下面的错误提示信息

1
presets_main_layout.xml:7: error: Error parsing XML: not well-formed (invalid token)
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical" style="@style/Screen.Background"
xmlns:android="http://schemas.android.com/apk/res/android">
<include layout="@layout/view_toolbar_activity_top" />
<LinearLayout android:id="@id/linear_layout_root_view" style="@style/Screen.InnerContainer">
<include layout="@layout/view_loading_switcher_base" />
<o.軞 android:id="@id/view_flipper_main" android:layout_width="fill_parent" android:layout_height="fill_parent">
......
</o.軞>
</LinearLayout>
</LinearLayout>

下面是资源文件presets_main_layout.xml的内容

问题的根源

​ 阅读Android源码知道,aapt使用了expat XML解析库来解析资源xml文件,xmltoc.c文件里的utf8_isname2、 utf8_isname3两个函数用来判断utf-8字符是否为合法的xml元素名字字符。

我们要做的就是要patch这两个函数

下面以utf8_isname3函数为例讲解patch过程。

utf8_isname3函数内容

1
2
3
4
5
static int PTRFASTCALL
utf8_isName3(const ENCODING *UNUSED_P(enc), const char *p)
{
return UTF8_GET_NAMING3(namePages, (const unsigned char *)p);
}

UTF8_GET_NAMING3是一个宏定义。内容如下:

1
2
3
4
5
6
7
#define UTF8_GET_NAMING3(pages, byte) \
(namingBitmap[((pages)[((((byte)[0]) & 0xF) << 4) \
+ ((((byte)[1]) >> 2) & 0xF)] \
<< 3) \
+ ((((byte)[1]) & 3) << 1) \
+ ((((byte)[2]) >> 5) & 1)] \
& (1u << (((byte)[2]) & 0x1F)))

上面的代码显示utf8_isName3最终用到了两个全局变量namingBitmap和namePages。

这两个变量定义在nametab.h中。

两个全局变量的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const unsigned namingBitmap[] = {
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF,
0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF,
0x00000000, 0x04000000, 0x87FFFFFE, 0x07FFFFFE,
0x00000000, 0x00000000, 0xFF7FFFFF, 0xFF7FFFFF,
………………

static const unsigned char namePages[] = {
0x19, 0x03, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x00,
0x00, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25,
0x10, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x13,
0x26, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
………………

我们可以通过在exe中直接查找同时用到这两个变量的代码来定位函数所在位置。

在expat在解析xml时后, aapt还对类名做了一些检测:允许的类名字符为: “abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789$”

这里的判断逻辑功能存在于Resource.cpp中,Android9.0源码版本中共有4处判断。

下面是相关的代码片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const char* classIdentChars = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789$";
………………

ATTR_OKAY = -1;
…………

else if (strcmp16(block.getElementName(&len), instrumentation16.string()) == 0) {
if (validateAttr(manifestPath, finalResTable, block, RESOURCES_ANDROID_NAMESPACE,
"name", classIdentChars, true) != ATTR_OKAY) {
hasErrors = true;
}
if (validateAttr(manifestPath, finalResTable, block,
RESOURCES_ANDROID_NAMESPACE, "targetPackage",
packageIdentChars, true) != ATTR_OKAY) {
hasErrors = true;
}
}
…………
补丁办法
  1. patch utf8_isname[23]函数,直接返回1.
  1. 修改相关validateAttr调用的返回值或删除相关对validateAttr调用。

为执行文件打补丁

这里我们使用apktool2.40.jar中的aapt_64.exe(jar包的prebuilt\windows目录中)为例。

apktool对aapt添加了一些功能,提供了windows、linux和macosx三个平台的版本。

逆向工作人员使用apktool居多。

  1. 用IDA的二进制查找功能查找namePages变量

aapt_patch_1

得到地址0x56CF30

aapt_patch_2

  1. 用同样的方法查找namingBitmap。注意namingBitmap中的值是4字节整数,在变为byte序列时需要倒序。

    如我要以0x04000000, 0x87FFFFFE两个整数为特征值去查找,那么实际查找字节内容为:

    00 00 00 04 FE FF FF 87

    查找后得到地址:0x56C1F0

  2. 查找引用0x56C1F0的代码块,在附近查找对地址0x56CF30的引用。

    如我们找到下面这一处(代码短小,对两个全局变量都有引用,可对照源码分析一下,会发现对“& 0x1F”相关指令被优化掉了,Android SDK中的版本是有相关指令的)。

    aapt_patch_4

代码结尾附近恰好有mov eax, 1指令,我们把后面改变eax寄存器值得指令”shl eax, cl”和“and eax, [rcx + dx * 4]” nop掉就可以了。

对找到的其他挤出(2-3)处相应修改, expat相关的代码就patch好了。

  1. 查找字符串“_0123456789$”(这里可以使用字符串查找功能),得到地址:0x55AEDD

    aapt_patch_6

    查找相关引用:

    aapt_patch_7

跳转到对应代码:

aapt_patch_8

因为代码优化的原因,这里并不像源代码中那样做了直接比较,而是把返回值保存在esi寄存器中了。

这里我们直接删除call调用的相关指令:直接用”mov, eax, 0FFFFFFFFh”替换”call sub_41FAD0”, 都是5字节指令,完美。

对其余4处做相应修改。

至此patch完毕,保存,然后替换apktool.jar中的相应文件即可。

其他选择

除了patch,也可以直接用源码进行编译。

  1. 可编译源码。aapt-apktool_v28
  2. apktool aapt源码版本:apktool aapt