此题为一道移动APP的逆向题,里面的坑很多,无法一一表述。该篇文章也是看了大牛们的wp后,加上自己的调试心得和体会杂凑而成,力求写得详尽以帮助有需要的朋友们。
1. 概览
解压apk后发现2个文件与代码相关:classes.dex和libexecute_table.so。
JEB反编译apk的结果如下,有3个native函数,它们都为so中的函数
base64解密字符串
RmFpbGVk --> Failed
U3VjY2Vzcw== --> Success
很显然,我们需要lkdakjudajndn(v0)返回1。
IDA 7.0在JNI_OnLoad下断点,开4个cmd分别调试运行apk(注:jdb那条指令中的port需要根据实际情况进行替换)
root@android:/data/local/tmp # ./android_server_nonpie
adb forward tcp:23946 tcp:23946
adb shell am start -D -ncom.rorschach.executetable/com.rorschach.executetable.MainActivity
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8602
奇怪的事情发生了,设置了断点却无法断下来?
很可能作者在JNI_OnLoad做了手脚,且此处的代码特别怪异
2. 详细分析
010editor查看so文件是否被更改,发现很多未定义的section_table
修改so节表,将节表的数目和偏移都修改为0
按照上述方法修改后,系统调用都识别出来了。
2.1 初始化函数分析
根据linker源码,so的执行顺序为:
.preinit_array->->.init->->.init array->->JNI_Onload->->java_com_XXX; 但 so 是不会执行.preinit_array 的, 可以忽略。而.init以及.init_array一般会作为壳的入口地方
运行readelf –d libexecute_table.so
可以看到INIT_ARRAY在0x34cd0处,直接丢到IDA查看,可以看到相关的初始化函数列表,为了方便观察交叉引用,我给每个函数重命了名。
初看觉得每个函数都很复杂,只注意到了init_array4_67BC开头处new了一段空间并且存入了几个关键函数:sys_open、sscanf、sys_read、sys_openat、sys_mprotect
定义function_table结构可以更好看出逻辑流程
在sys_openat和sys_mprotect处下断点
从sys_openat处的断点,可以发现sys_openat会打开两个文件/proc/self/status(sub_B8BC)、/proc/self/maps(sub_9390)
其中,打开/proc/self/maps是用来获取libexecute_table.so的基址,以便修改JNI_OnLoad的符号表
打开/proc/self/status是为了读取TracerPid用于反调试,如果TracerPid>0表示存在反调试,TracerPid<=0为正常程序。要patch一下这个函数,使其反调试失效。我patch的方式是将判断逻辑改反,由于对arm的汇编指令不熟,费了很大劲才找到网站http://armconverter.com,可以将arm指令转换为机器码。将patch后的so文件替换原so文件,并重新签名apk。
patch so:直接修改逻辑让tracePid>0做正常操作,将BLE.w改成BGT.w。
通过TracerPid定位到反调试进程sub_B8BC,依据是否处于调试状态决定0x36098的取值。如果处于调试状态是其值为 0xBD9813BA,否则为 0x2333AE83。后续lkdakjudajndn函数的计算过程将用到0x36098数值。
从sys_mprotect处的断点,发现2个函数调用了sys_mprotect:sub_86E0、sub_833C
sub_86E0用来修改符号表,将原始JNI_OnLoad地址=0x8205修改为0xA261。
[0x3208+4]=0x8205 --> [0x3208+4]=0xA261
sub_833C解密lkdakjudajndn函数。用前一个字节xor后一个字节来解密,第一个字节与0x66异或,总长度400,起始为0xAC98+150
当然了,这些逻辑也是我再看完大牛的wp才弄清楚的。其实在怎样找到真实JNI_OnLoad这个问题上,有个更为简便的方法。
2.2 寻找JNI_OnLoad加载地址
利用 https://bbs.pediy.com/thread-216701.htm 上的程序,伪造一个JavaVM(vmCode.c)来加载so,并趁机获得so加载基址和JNI_OnLoad函数地址(ida中直接显示的是错误的,应该是.init和.init_array中对JNI_OnLoad地址进行了修改所致)
#include
#include
#include
#include
typedef unsigned int u4;
int main()
{
JavaVM* vm;
JNIEnv* env;
jint res;
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString ="-Djava.class.path=.";
vm_args.version=0x00010002;
vm_args.options=options;
vm_args.nOptions =1;
vm_args.ignoreUnrecognized=JNI_TRUE;
printf("[+] dlopenlibdvm.so\n");
void *handle =dlopen("/system/lib/libdvm.so", RTLD_LAZY);//RTLD_LAZY RTLD_NOW
if(!handle){
printf("[-] dlopenlibdvm.so failed!!\n");
return 0;
}
//这里我先创建一个java虚拟机。因为JNI_ONload函数参数第一个参数为JavaVM。
typedef int (*JNI_CreateJavaVM_Type)(JavaVM**,JNIEnv**, void*);
JNI_CreateJavaVM_TypeJNI_CreateJavaVM_Func = (JNI_CreateJavaVM_Type)dlsym(handle,"JNI_CreateJavaVM");
if(!JNI_CreateJavaVM_Func){
printf("[-] dlsymfailed\n");
return 0;
}
res=JNI_CreateJavaVM_Func(&vm,&env,&vm_args);
void*si=dlopen("/data/local/tmp/libexecute_table.so",RTLD_LAZY);
if(si == NULL){
printf("[-] dlopenerr!\n");
return 0;
}
typedef jint (*FUN)(JavaVM*vm,void* res);
FUN func_onload=(FUN)dlsym(si,"JNI_OnLoad");
u4load=(u4)(dlsym(si,"JNI_OnLoad"));
load&=~0xfff;
while(*(u4*)load!=0x464C457F)load-=0x1000;
printf("%x%x\n",func_onload,load);
if(func_onload==NULL)
return 0;
func_onload(vm,NULL);
return 0;
}
Android.mk写成如下形式:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vmCode
LOCAL_SRC_FILES := vmCode.c
include $(BUILD_EXECUTABLE)
将代码用ndk-build编译成可执行文件vmCode,然后在adb中执行
得出加载so基址与JNI_Load相差A261
我们找到正确JNI_OnLoad地址后在IDA中将其第一个参数的类型定义为JavaVM*,找到其中GetEnv的调用,将其第二个参数的类型定义为JNIEnv*,顺路找到了调用RegisterNatives的地址。(可能需要右键“Force call type”来显示全所有参数)
然后将上述程序调试运行,运行到RegisterNatives处为止(注意由于这里FindClass返回值为空,需要手工过掉一个跳转),成功得到lkdakjudajndn函数的注册地址为libexecute_table.so的基址+0xac98。
2.3 定位lkdakjudajndn函数的真实地址
网上看到很多朋友不知道怎样确定lkdakjudajndn函数的偏移地址0xac98?其实很简单,RegistreNatives函数需要4个参数:env、className、gMethods、numMethods,我们需要关注第3个参数,即类型为JNINativeMethod的gMethods参数。
static int registerNatives(JNIEnv* env, const char* className,
JNINativeMethod*gMethods, int numMethods)
JNINativeMethod,定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。
说白了,JNINativeMethod包含3个指针。第3个指针就指向处理函数的真实地址。下面截图说明具体过程:
在真正的JNI_OnLoad的RegisterNatives直接下断点,根据R2的数值得出注册函数的地址。
从图中可以看出处理函数的地址为0x5D756C99,在IDA中定位到此,显示地址为0x5D756C98
而so的基址为0x5D74C000
因此,得出处理函数与基址的偏移为0x5D756C98-0x5D74C000=0xAC98
2.4 lkdakjudajndn函数的处理逻辑
lkdakjudajndn无法F5,如果此时在此处直接下断点,将会遇到错误。
当时我也是在0xAC98处直接下断点运行,每次运行到0xAD30程序崩溃退出。
后来经过一番折腾,才知道需要在sub_833c进行mprotect解密后,再在此处下断点调试,此时才能F5出程序逻辑。
现在我也不太明白,按理说JNI_OnLoad就调用了sub_833C,执行完JNI_OnLoad才会执行lkdakjudajndn函数,为啥直接执行lkdakjudajndn会有可能造成没有执行sub_833C呢?哪位朋友知道的话,麻烦告诉我一声哈。
F5的sub_AC98的函数如下,没有全部贴出
lkdakjudajndn函数的逻辑流程为:
1、将输入key的jstring对象转换成 cstring对象。
2、key长度必须大于等于10,小于等于20。
3、将输入key置换到最后一位,变成key_change1。
4、索引 0x7FC2 ,key_change2[i]
= 0x7FC2[ key_change1[i]],得到 key_change2。
5、将 key_change2按照字节进行低4位与高四位的置换生成key_change3。
6、将 key_change3以4字节形式与 0x36098内容进行异或,当处于调试状态是其值为 0xBD9813BA,否则为 0x2333AE83。得到 key_change4,从这里可知key长度为0x10。
7、将 key_change4相邻2字节交换,得到key_change5 ,即key_change5[2i] = key_change4[2i+1] key_change5[2i + 1] = key_change4[2i]。
8、再次索引7FC2 ,key_change6[i]
= 7FC2[ key_change5 [i]],得到 key_change6。
9、将 key_change6的第一字符置换到最后位置,得到 key_change7。
10、将 key_change7的所有字符的高4位对字符串“A3Cw6Gb0OZWPU52s”进行索引,组成 key_change8_highString, 将 key_change7所有字符的低4位对字符串
“A3Cw6Gb0OZWPU52s”进行索引,并进行倒序。组成 key_change8_lowString, 然后将key_change8_highString与 key_change8_lowString进行拼接生成 key_change8。
11、生成字符串“3ww3U53wOAWG333wwPZ56GGw0PO02OUW”。
12、将 key_change8 与“3ww3U53wOAWG333wwPZ56GGw0PO02OUW”比较,相等返回1,否则返回其他。
0x7FC2存放了置换表,可以发现这一处表被分析成了arm指令,也就是说,题意就是可执行的表(Executable table)。
unsigned char table[0x100]= {
0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 0x06, 0x21, 0x09, 0xF3, 0x5C,0x95, 0xAE, 0x66, 0x12,
0x8F, 0x85, 0xC8, 0x5A, 0xBF, 0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5,0x8B, 0xA4, 0xC5, 0xC7,
0xEA, 0xF6, 0x79, 0x1E, 0x3C, 0xBA, 0x97, 0x4E, 0x38, 0x60, 0x08, 0xDD,0xFA, 0xB3, 0xDE, 0x77,
0x81, 0x41, 0x19, 0xF4, 0x52, 0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9,0xE7, 0x91, 0xE9, 0x54,
0x82, 0xAD, 0x7E, 0x11, 0x35, 0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0x0A,0x74, 0x2F, 0xE2, 0x17,
0x98, 0x0C, 0x70, 0x92, 0x47, 0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D,0x07, 0x72, 0x25, 0x04,
0xB7, 0xC9, 0xCE, 0x0E, 0x9E, 0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF,0x39, 0xF0, 0xBB, 0xBD,
0x46, 0x32, 0xE6, 0x9F, 0x4F, 0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB,0x20, 0xD2, 0x0B, 0xA5,
0xEE, 0xE1, 0xA9, 0x2B, 0x84, 0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD,0xB6, 0xFC, 0x55, 0x7C,
0x5F, 0xF8, 0x4C, 0x65, 0x2C, 0x30, 0xEF, 0x48, 0xD7, 0x0D, 0x0F, 0x1A,0x5E, 0xC0, 0x3A, 0x57,
0x6A, 0x31, 0x00, 0xF1, 0x59, 0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E,0x26, 0x1D, 0x13, 0x15,
0x89, 0x5D, 0xDA, 0x61, 0xD1, 0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49,0xEC, 0xE3, 0xD0, 0x34,
0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B, 0xB2, 0x4A, 0xDF, 0xAC, 0x96,0xDC, 0xE8, 0xA0, 0xF9,
0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE, 0x87, 0x28, 0xCC, 0x99, 0xE5,0x45, 0x58, 0x94, 0x23,
0x22, 0xFB, 0x02, 0x01, 0x03, 0x8A, 0x7B, 0xB5, 0x1C, 0xA7, 0x44, 0xCD,0xA2, 0x51, 0x8E, 0x3F,
0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D, 0xA6, 0x05, 0x2D, 0xA8, 0x80,0x6D, 0xB4, 0x76, 0x5B,
};
3. 计算sn
下面是VC代码
#include "stdafx.h"
#include
unsigned char g_indexKey[0x100] =
{ 0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 0x06, 0x21, 0x09, 0xF3,0x5C, 0x95, 0xAE, 0x66, 0x12,
0x8F, 0x85, 0xC8, 0x5A, 0xBF,0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5, 0x8B, 0xA4, 0xC5, 0xC7,
0xEA, 0xF6, 0x79, 0x1E, 0x3C,0xBA, 0x97, 0x4E, 0x38, 0x60, 0x08, 0xDD, 0xFA, 0xB3, 0xDE, 0x77,
0x81, 0x41, 0x19, 0xF4, 0x52,0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9, 0xE7, 0x91, 0xE9, 0x54,
0x82, 0xAD, 0x7E, 0x11, 0x35,0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0x0A, 0x74, 0x2F, 0xE2, 0x17,
0x98, 0x0C, 0x70, 0x92, 0x47,0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D, 0x07, 0x72, 0x25, 0x04,
0xB7, 0xC9, 0xCE, 0x0E, 0x9E,0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF, 0x39, 0xF0, 0xBB, 0xBD,
0x46, 0x32, 0xE6, 0x9F, 0x4F,0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB, 0x20, 0xD2, 0x0B, 0xA5,
0xEE, 0xE1, 0xA9, 0x2B, 0x84,0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD, 0xB6, 0xFC, 0x55, 0x7C,
0x5F, 0xF8, 0x4C, 0x65, 0x2C,0x30, 0xEF, 0x48, 0xD7, 0x0D, 0x0F, 0x1A, 0x5E, 0xC0, 0x3A, 0x57,
0x6A, 0x31, 0x00, 0xF1, 0x59,0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E, 0x26, 0x1D, 0x13, 0x15,
0x89, 0x5D, 0xDA, 0x61, 0xD1,0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49, 0xEC, 0xE3, 0xD0, 0x34, 0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B,0xB2, 0x4A, 0xDF, 0xAC, 0x96, 0xDC, 0xE8, 0xA0, 0xF9, 0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE,0x87, 0x28, 0xCC, 0x99, 0xE5, 0x45, 0x58, 0x94, 0x23, 0x22, 0xFB, 0x02, 0x01, 0x03, 0x8A, 0x7B,0xB5, 0x1C, 0xA7, 0x44, 0xCD, 0xA2, 0x51, 0x8E, 0x3F, 0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D,0xA6, 0x05, 0x2D, 0xA8, 0x80, 0x6D, 0xB4, 0x76, 0x5B,
};
bool GetKey(unsigned char *lastKey, unsigned int keyLen)
{
unsigned int i, j;
unsigned char temp;
unsigned char lowByte = 0;
unsigned char highByte = 0;
unsigned char* highKey =(unsigned char*)"3ww3U53wOAWG333w";
unsigned char* lowKey =(unsigned char*)"wPZ56GGw0PO02OUW";
unsigned char* constKey =(unsigned char*)"A3Cw6Gb0OZWPU52s";
memset(lastKey, 0, keyLen);
//索引constKey
for (i = 0; i < keyLen;i++)
{
for (j = 0; j < keyLen; j++)
{
if (highKey[i] ==constKey[j])
break;
}
if (j >= keyLen)
return false;
highByte = j << 4;
for (j = 0; j < keyLen; j++)
{
if (lowKey[i] ==constKey[j])
break;
}
if (j >= keyLen)
return false;
lowByte = j;
lastKey[i] |= highByte;
lastKey[keyLen - 1 - i]|= lowByte;
}
//最后一个字节置换到第一个位置
temp = lastKey[keyLen-1];
for (i = keyLen-1; i >0;i--)
{
lastKey[i] = lastKey[i -1];
}
lastKey[i] = temp;
//索引g_indexKey
for (i = 0; i < keyLen;i++)
{
for (j = 0; j < 256;j++)
{
if (lastKey[i] ==g_indexKey[j])
break;
}
if (j >= 256)
return false;
lastKey[i] = j;
}
//相邻字节交换
for (i = 0; i <(keyLen>>1); i++)
{
temp = lastKey[i * 2];
lastKey[i * 2] =lastKey[i * 2 + 1];
lastKey[i * 2 + 1] =temp;
}
//与0x2333AE83异或
unsigned int eorConstKey =0x2333AE83;
unsigned int* p = (unsignedint*)lastKey;
for (i = 0; i < (keyLen>> 2); i++)
{
p[i] = p[i] ^eorConstKey;
}
//高4位与低4位交换
for (i = 0; i < keyLen;i++)
{
highByte = lastKey[i]>> 4;
lowByte = lastKey[i]& 0xf;
lastKey[i] = (lowByte<< 4) | highByte;
}
//索引g_indexKey
for (i = 0; i < keyLen;i++)
{
for (j = 0; j < 256;j++)
{
if (lastKey[i] ==g_indexKey[j])
break;
}
if (j >= 256)
return false;
lastKey[i] = j;
}
//最后一个字节置换到第一个位置
temp = lastKey[keyLen - 1];
for (i = keyLen - 1; i >0;i--)
{
lastKey[i] = lastKey[i -1];
}
lastKey[i] = temp;
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
unsigned char flag[0x10];
unsigned int i;
memset(flag, 0, 0x10);
if (GetKey(flag, 0x10))
{
for(i=0;i<0x10; i++)
printf("%c",flag[i]);
}
return 0;
}
sn = "C0ngRa7U1AtIoN2U"
4. 总结
掌握了.so的执行顺序:.init->->.init array->->JNI_Onload->->java_com_XXX
做题过程中还有2个疑问:
疑问1:在.init_array处下断点看到的指令不正确,也不能调试。是不是某些函数中途将其加密了呢?
疑问2:输入正确的sn,如果运行在已经patch过的apk上会显示Failed(调试lkdakjudajndn函数,返回值为1),而运行在原始apk则显示Success。是不是patch时破坏了某处逻辑?