上篇博文中浅析了从手机淘宝中提炼出商品搜索接口,很多人有个疑惑,x-sign 怎么来的?目前很多网友表示是通过 xposed hook 用模拟器作服务器中转的方式。下面我们通过逆向 so 文件的方式取得这个 x -sign 的算法。
找到 x -sign 的计算点
经过一系列跳转后,我们看到了 com.taobao.wireless.security.adapter.a 接口的 a 方法。
private String a(String[] arg4, String arg5, int arg6, String arg7) {return this.a.getRouter().doCommand(10401, new Object[]{arg4, arg5, Integer.valueOf(arg6), arg7});
}
在接下来的跳转链之后,我们又找到了实现 RouterComponent 接口以及 doCommand 方法的一个类:
public class a implements IRouterComponent {public a() {super();
}
public Object doCommand(int arg2, Object[] arg3) {return JNICLibrary.doCommandNative(arg2, arg3);
}
}
还有一个 JNICLibrary 类,其中声明了 doCommandNative 方法:
public class JNICLibrary {public static native Object doCommandNative(int arg0, Object[] arg1);
}
因此,我们需要在原生代码中找到 doCommandNative 方法。
混淆机器码
在 libsgmain.so 文件中包含一个原生库(libsgmain.so 实际上是一个.JAR 文件,其中实现了与加密有关的接口):libsgmainso-6.xx.x。在 IDA 中加载该库后,我们看到了一堆错误消息提示框,问题在于 section 头表无效。
通过 elf 查看工具我们可以看到
但我们并不需要这个信息,程序头表对我们而言已经足够,可以正确加载并分析 ELF 文件。因此我们可以简单删除 section 头表,将头部中对应的字段置空。
然后再次在 IDA 中打开该文件。
我们有两种方法能告诉 Java 虚拟机哪个原生库包含代码中声明的原生代码的具体实现。第一种方法就是采用 Java_package_name_ClassName_methodName 之类的名字,第二种方法是调用 RegisterNatives 函数,在加载库的时候进行注册(在 JNI_OnLoad 函数中)。对于这个案例,如果我们使用第一种方法,那么函数名应该类似于 Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative。在导出函数中我们找不到这个名字,这意味着我们需要查找 RegisterNatives。因此,我们转到 JNI_OnLoad 函数,看到如下代码:
这里代码执行了哪些逻辑?初步分析时,函数头以及函数尾都是典型的 ARM 架构。第一条指令会将函数需要使用的寄存器值 push 到栈中(这里为 R0、R1、R2 以及 LR,用来保存函数返回地址)。最后一条指令恢复已保存的寄存器值,将返回地址存到 PC 寄存器中,然后返回函数。但如果我们仔细分析,可能会注意到倒数第二条指令改变了返回地址。来计算一下代码执行后返回地址的值。该地址加载自 R1(0xB130),减去 5,然后被 mov 到 R0,再加上 0x10,最后这个值等于 0xB13B。因此,IDA 认为最终指令执行的是正常的函数返回操作,然而实际上会跳转到 0xB13B 这个地址。
这里需要注意的是,ARM 处理器有两个型号以及两组指令:ARM 以及 Thumb。地址的低位用来决定处理器会使用哪一组指令集。这里地址为 0xB13A,因此对应的是 Thumb 模式。
在这个库中,每个函数开头处都添加了类似的语句以及某些垃圾代码,这里我们不会详细分析这些内容,只要记住几乎所有函数的实际代码都离函数开头有一段距离。
由于已有代码中没有显式转换到 0xB13A,因此 IDA 无法识别该地址处的代码。同样,IDA 也没有将库中的大部分数据识别为代码,这样我们分析起来需要稍微用点技巧 因此,我们手动告诉 IDA 代码位置,然后得到如下结果:
接下来我们采用脚本来 patch 代码。(鉴于篇幅 脚本内容略)
patch 完成后,我们可以指引 IDA 找到函数的真实代码。IDA 会逐一收集所有函数代码,然后我们就可以使用 HexRays 来反编译代码。
我们已经找到加密算法和密钥,现在让我们尝试解密类名。我们得到的结果为 com/taobao/wireless/security/adapter/JNICLibrary
命令结构树
现在我们需要找到哪里调用了 RegisterNatives,这将我们指引到 doCommandNative 函数。经过一系列分析还原得出具体逻辑:
int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args)
{
int v5; // r5
struc_2 *a5; // r6
int v9; // r1
int v11; // [sp+Ch] [bp-14h]
int v12; // [sp+10h] [bp-10h]
v5 = 0;
v12 = *(_DWORD *)off_8AC00;
v11 = 0;
a5 = (struc_2 *)malloc(0x14u);
if (a5)
{
a5->field_0 = 0;
a5->field_4 = 0;
a5->field_8 = 0;
a5->field_C = 0;
v9 = command % 10000 / 100;
a5->field_0 = command / 10000;
a5->field_4 = v9;
a5->field_8 = command % 100;
a5->field_C = env;
a5->field_10 = args;
v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11);
}
free(a5);
if (!v5 && v11)
sub_7CF34(env, v11, &byte_83ED7);
return v5;
}
函数名表示这是开发者将所有函数转到原生库的统一入口点,我们的目标函数编号为 10401。
从代码中我们可以通过命令编号生成 3 个子编号:command / 10000、command % 10000 / 100 以及 command % 10(这里我们对应的是 1、4 以及 1)。这 3 个子编号、指向 JNIEnv 的指针以及传给该函数的其他参数共同组成一个结构体,以便后续使用。
这棵树会在 JNI_OnLoad 中动态创建,其中 3 个子编号共同编码了整棵树的路径。树中每个节点都包含相应函数经过异或处理后的地址,秘钥位于父节点中。
最终结果