有空找些CrackMe来让脑子转一转还是挺有意思的。
这道题据说是一道Android逆向工程师的面试题。给的APK是一个服务端程序,要求写一个客户端来解密来自服务端的数据。
把APK拖到JEB,直接看MainActivity,如图:
MainActivity启动后,就会创建一个子线程去做一些事,而且这里加载了一个动态库,估计线程做的事情是在Native层实现的。
如上图,果然,子线程调用了MainActivity的Native函数init()。
用IDA打开so库,找到对应的jni函数Java_com_example_crack0_MainActivity_init() 的实现,如图:
该函数主要做的事情如下:
-
创建socket,并绑定监听本地8080端口(代码中没有指定,默认是8080,可在adb shell
中使用netstat -ano 命令进行确认);
- 读取客户端发来的数据,将读取到的数据通过xxx()函数进行加密;
- 将加密后的数据发送给客户端。
这里要注意一个地方,就是上图中xxx()函数调用的第三个参数,传入的是v6的这个整型变量的地址。另外,可以看到,v6、v7、v8和v9都是很有规律的数,就是一串连续的16进制数,但看起来xxx()函数只用到了v6,这貌似不太对,我猜想这4个整型变量其实是连续的...于是我将代码视图跳转到ARM汇编视图,发现:
双击跳转到unk_2364地址处查看,如图:
果然这串十六进制数是紧挨着的,也就是说,这16个字节的数都会参与到加密。我猜想这串数应该是用作加解密的密钥。
好,下面来分析xxx()函数的实现,如图:
可以看到,xxx()函数是先对原始数据缓冲区srcData的每个字节进行取反,然后再对原始数据的字节长度进行各种算术运算,从而得到加密后数据的长度,以及后续do-while循环所需要的循环次数,同时还将一个地址值(srcDataLen的地址)放到缓冲区srcData中,。后面这个do-while循环就是再次对经过初步处理的原始数据进行加密,真正关键的加密实现在xxxx()函数中,如图:
从xxxx()函数的实现中,可以发现TEA的显著特征,那就是0xC6EF3720,和 -0x61C88647(即0x9E3779B9)。搜索一下TEA的C语言实现,如下图所示【详见链接】:
仔细对比一下该图的encrypt()和上面题目中的xxxx()函数,就能确定这个xxxx()函数就是对传入的数据进行了TEA加密。
识别出算法后,就可以通过对比TEA加密和解密实现的不同,来写出对应的解密函数xxxx_decrypt()了:
#define _DWORD unsigned int
#define HIDWORD(x) (*((_DWORD *)&(x) + 1))
#define LODWORD(x) (*((_DWORD *)&(x)))
//key="\x67\x45\x23\x01\xEF\xCD\xAB\x89\x98\xBA\xDC\xFE\x10\x32\x54\x76"
//TEA解密算法
long long xxxx_decrypt(int a1, int a2)
{
int v2;
int v3;
int v4;
unsigned int v5;
unsigned int v6;
int v7;
long long v9;
v2 = *(int *)(a2 + 8); //v2=0xFEDCBA98
v3 = *(int *)a2; //v3=0x1234567
v4 = *(int *)(a2 + 4); //v4=0x89ABCDEF
v5 = *(int *)a1;
v6 = *(int *)(a1 + 4);
HIDWORD(v9) = *(int *)(a2 + 12);
v7 = 0xC6EF3720;
LODWORD(v9) = v2; //v9=0x76543210FEDCBA98
do
{
v6 -= ((v5 >> 5) + HIDWORD(v9)) ^ (16 * v5 + v2) ^ (v5 + v7);
v5 -= ((v6 >> 5) + v4) ^ (16 * v6 + v3) ^ (v6 + v7);
v7 += 0x61C88647;
} while (v7 != 0);
*(int *)a1 = v5;
*(int *)(a1 + 4) = v6;
return v9;
}
再回头看xxx()函数再到xxxx()函数的这个过程,首先xxxx函数的参数1是待加密数据的缓冲区首地址,参数2是key的首地址。且xxxx()函数每一次的调用,参数1所传入的缓冲区首地址,是原始缓冲区首地址+(i*8)个字节的偏移,这里的 i 表示循环计数。而循环次数 = 加密后数据的字节数 / 8,这个可以通过分析xxx()函数那几行代码得知,很容易的。
最后我写了一个客户端APK来解密服务端的数据,关键代码如下:
#define _DWORD unsigned int
#define HIDWORD(x) (*((_DWORD *)&(x) + 1))
#define LODWORD(x) (*((_DWORD *)&(x)))
//key="\x67\x45\x23\x01\xEF\xCD\xAB\x89\x98\xBA\xDC\xFE\x10\x32\x54\x76"
//TEA解密算法
long long xxxx_decrypt(int a1, int a2)
{
int v2;
int v3;
int v4;
unsigned int v5;
unsigned int v6;
int v7;
long long v9;
v2 = *(int *)(a2 + 8); //v2=0xFEDCBA98
v3 = *(int *)a2; //v3=0x1234567
v4 = *(int *)(a2 + 4); //v4=0x89ABCDEF
v5 = *(int *)a1;
v6 = *(int *)(a1 + 4);
HIDWORD(v9) = *(int *)(a2 + 12);
v7 = 0xC6EF3720;
LODWORD(v9) = v2; //v9=0x76543210FEDCBA98
do
{
v6 -= ((v5 >> 5) + HIDWORD(v9)) ^ (16 * v5 + v2) ^ (v5 + v7);
v5 -= ((v6 >> 5) + v4) ^ (16 * v6 + v3) ^ (v6 + v7);
v7 += 0x61C88647;
} while (v7 != 0);
*(int *)a1 = v5;
*(int *)(a1 + 4) = v6;
return v9;
}
JNIEXPORT void Java_me_falcon_exam02_1crack_MainActivity_decryptData(JNIEnv *env, jobject obj, jbyteArray dataArr)
{
int dataBytesLen = (*env)->GetArrayLength(env, dataArr);
char key[] = "\x67\x45\x23\x01\xEF\xCD\xAB\x89\x98\xBA\xDC\xFE\x10\x32\x54\x76";
jbyte *dataBytes = (*env)->GetByteArrayElements(env, dataArr, NULL);
int nCnt = dataBytesLen / 8;
for (int i = 0; i < nCnt; i++) {
xxxx_decrypt(dataBytes + i * 8, key);
}
char srcDataBuf[1024] = {};
for (int i = 0; dataBytes[i] != 0; i++) {
srcDataBuf[i] = ~dataBytes[i];
}
LOGD("[From server] After decrypt, srcData=%s, srcDataLen=%d", srcDataBuf, strlen(srcDataBuf));
(*env)->ReleaseByteArrayElements(env, dataArr, dataBytes, 0);
}
相关的代码、APK等有兴趣可通过下面的地址获取:
链接:https://pan.baidu.com/s/1pufbdPb6y1fnEm3W6lnXRg 密码:9jqs