零、前言
这道题包含的知识感觉还挺多的,基本上是解出一步卡一步,然后不停的百度,看文章分析,才慢慢的摸索出来,首先是第一次接触到纯native开发的app这种东西,知道了其AXML大概的写法,以及SO文件中的入口为android_main
,其次是第一次接触到将dex文件写入SO中,在运行时由SO进行释放的概念。最后分析释放出的dex文件时,新接触到twofish加密算法。一环套装一环,是比较有价值的一道题。(但对于我们这种初学者来说也是相当不友好的一道题…)
一、jadx分析apk
先用jadx打开apk文件可以看到,源代码中没有java代码,AXML中显示android:hasCode="false"
,且下面有个NativeActivity
。不明觉厉,寻思着应该和so有关系,随用ida pro打开libnative.so文件开始分析。
二、IDA PRO分析SO
打开so查看exports,目前比较能看到函数名的就一个android_main
,和一个onCreate
函数,打开oncreate发现里面的东西全都看不懂(= _=),随看一下android_main
函数。
在android_main
函数中看到一堆log文件,寻思着有戏,于是分析该函数
从能看得懂的地方开始分析,可以知道,这个程序的规则是10秒晃动手机100下,然后接下一个循环,循环内应该是主要代码了,随开始分析循环。(整体代码注释请看四、SO代码)
首先跳到循环的底部,查看挑战成功和失败的逻辑,根据成功的逻辑可以知道,成功后会先解压v3,然后保存到filename中进行加载,最后再删除掉,因此v3中的数据就是压缩后的dex的数据。随跟踪v3的处理流程。
来到文件的开头,可以看到给变量V3分配了空间,且进行了数据拷贝,在此可以得出数据的大小为248336,数据所在的区域为0x7004->0x7004+0x3CA10这一块区域。
使用memoryDUmp插件,将该内存的数据dump出来,保存为out.dump,继续跟进v3的处理,查看有没有进一步操作
看到这两个地方对v3进行了操作,说明有数据加密算法,所以得从加密处向上寻找逻辑
所有逻辑集中在第145行中的while循环中,循环前面内容应该是判断角度,不是重点,不用深究。继续向下由log的提示可以推出v10为摇晃次数,依次再向上就可以推出v6是开始时间,v14是每摇晃一次后新的摇晃次数
根据逻辑,只要摇晃角度大于15度,则最终都会进入LABLE_31进行处理,这一段也是加密的核心内容,根据程序一步一步完善注释更改变量名,最终可以得出该加密逻辑为,将数据分为10部分,当摇晃次数达到(9,19,29,39,49,59,69,79,89)的时候对应每一部分的数据与摇晃次数做异或运算,当到达89的时候对最后一部分的数据与89做异或运算。
由此加密函数分析完毕,可以写解密脚本了,pyton解密脚本如下(一定注意,解密后还有个解压缩的过程,否则文件无法使用):
import zlib
with open("./blog/out.dump", 'rb') as f:
data = f.read()
data_len = len(data)
decode_bytes = [0]*data_len
for i in range(0, 9):
per_seg_len = int(len(data) // 10)
print(i*per_seg_len, (i+1)*per_seg_len)
print((i *10 + 9))
start = i*per_seg_len
while start < (i+1)*per_seg_len:
decode_bytes[start] = data[start] ^ (i *10 + 9)
start += 1
per_seg_len = int(len(data) // 10)
start = 9*per_seg_len
print(9*per_seg_len, data_len)
while start < data_len:
decode_bytes[start] = data[start] ^ 89
start += 1
with open("./blog/decode.dex", 'wb') as f:
f.write(zlib.decompress(bytes(decode_bytes)))
三、jadx分析decode.dex文件
直接将dex拖入jadx会发现很多资源找不到,会以数字的的方式标注,这是因为我们只有dex文件,没有资源文件
可以反编译原apk文件,将其中的resources.arsc
文件和dex文件一起拖入jadx,则可以使jadx自动解析
MainActivity主要设置了监听事件,监听事件由a类完成,其中取了输入字符串edit_text
和固定字符串R.string.two_fish
的值,然后使用MainActivity.b函数做运算,得到的返回值与MainActivity.m
这一串byteArray做对比,于是乎分析MainActivity.b函数。
MainActiviy.b调用了b类,把输入的字符串分组,每组16个字节,调用b.a()
,传入这16个字节与R.string.two_fish
的值进行加密,加密后输出一个byteArray。这个array应该等于MainActivity.m
可知,b类是一个加密函数,查看b类的算法。。。。算了,看不懂= =,看了网上大佬的解体思路大概是,根据传入的加密字符串R.string.two_fish
中含有关键字towfish,猜测是用了twofish加密算法,而twofish加密算法,是对成加密算法,根据key,把原始数据进行加密,并输出base64编码。在资源文件中可以找到对应的key应该是I have a male fish and a female fish.
所以,MainActivity.m
的byteArray应该就是加密后输出的base64编码,将其转化为base64后为:iE3y2hEF1izgbVUfGKWQrUCtgFQFop7iEkbmRwWdwsZ1HdQGcPxRVAkWzV/eDC9N
,解密python脚本为
import base64
a = [-120, 77, -14, -38, 17, 5, -42, 44, -32, 109, 85, 31, 24, -91, -112, -83, 64, -83, -128, 84, 5, -94, -98, -30, 18, 70, -26, 71, 5, -99, -62, -58, 117, 29, -44, 6, 112, -4, 81, 84, 9, 22, -51, 95, -34, 12, 47, 77]
a = [i&255 for i in a]
b = base64.b64encode(bytes(a))
print(b)
通过在线网站进行解密后即可得到flag:qwb{TH3y_Io<e_EACh_OTh3r_FOrEUER}
四、SO代码加注释
int __fastcall android_main(_DWORD *a1)
{
void *v2; // r5
char *v3; // r10
int v4; // r2
int v5; // r1
time_t v6_start_time; // r8
int *v7; // r0
int v8; // r3
int v9; // r6
int v10_shake_times; // r4
float v11; // s0
int v12; // r0
int v13; // r5
int v14_new_shake_times; // r5
int v15_data_index; // r3
void *v16; // r0
int v17_per_data; // r2
int v18_next_data; // r1
char *v19_start_index; // r3
time_t v20; // r5
int v21; // r8
Bytef *dest; // [sp+8h] [bp-158h]
int v24; // [sp+10h] [bp-150h] BYREF
char v25[4]; // [sp+14h] [bp-14Ch] BYREF
int v26[13]; // [sp+18h] [bp-148h] BYREF
uLongf destLen; // [sp+4Ch] [bp-114h] BYREF
char v28[8]; // [sp+50h] [bp-110h] BYREF
int v29; // [sp+58h] [bp-108h]
float v30_angle; // [sp+70h] [bp-F0h]
char name[4]; // [sp+B8h] [bp-A8h] BYREF
int v32; // [sp+BCh] [bp-A4h]
int v33; // [sp+C0h] [bp-A0h]
int v34; // [sp+C4h] [bp-9Ch]
int v35; // [sp+C8h] [bp-98h]
int v36; // [sp+CCh] [bp-94h]
int v37; // [sp+D0h] [bp-90h]
int v38; // [sp+D4h] [bp-8Ch]
int v39; // [sp+D8h] [bp-88h]
int v40; // [sp+DCh] [bp-84h]
int v41; // [sp+E0h] [bp-80h]
__int16 v42; // [sp+E4h] [bp-7Ch]
char v43; // [sp+E6h] [bp-7Ah]
char filename[4]; // [sp+E8h] [bp-78h] BYREF
int v45; // [sp+ECh] [bp-74h]
int v46; // [sp+F0h] [bp-70h]
int v47; // [sp+F4h] [bp-6Ch]
int v48; // [sp+F8h] [bp-68h]
int v49; // [sp+FCh] [bp-64h]
int v50; // [sp+100h] [bp-60h]
int v51; // [sp+104h] [bp-5Ch]
int v52; // [sp+108h] [bp-58h]
int v53; // [sp+10Ch] [bp-54h]
int v54; // [sp+110h] [bp-50h]
int v55; // [sp+114h] [bp-4Ch]
int v56; // [sp+118h] [bp-48h]
char v57; // [sp+11Ch] [bp-44h]
int v58; // [sp+124h] [bp-3Ch]
destLen = 0x100000;
dest = (Bytef *)malloc(0x100000u);
v2 = off_43A18; // off_43A18是一个偏移量,该偏移量储存的值为unk_3CA10,v2是一个指针,所以此处存的应该是unk_3CA10的地址
v3 = (char *)malloc((size_t)off_43A18); // 给v3分配off_43A18个字节,即0X3CA10,248336个字节的地址
qmemcpy(v3, &unk_7004, (size_t)v2); // 从unk_7004的地址处即0x7004开始拷贝v2个字节(0x3CA10即248336个字节)到v3中,由此可推出数据所在区域为0x7004->0x7004+0x3CA10处
*(_DWORD *)filename = -1651995345;
v45 = -2003974520;
v46 = -1966700387;
v47 = -2000190330;
v48 = -2071422265;
v49 = -947092071;
v50 = -1920499569;
v51 = -1936879484;
v52 = -2138061167;
v53 = -962950011;
v54 = -1702328950;
v55 = -946172774;
v56 = -376337267;
v57 = 0;
*(_DWORD *)name = -1651995194;
v32 = -2003974520;
v33 = -1966700387;
v34 = -2000190330;
v35 = -2071422265;
v36 = -947092071;
v37 = -1920499569;
v38 = -1936879484;
v39 = -2138061167;
v40 = -962950011;
v41 = -1853059706;
v43 = 0;
v4 = 1;
v42 = -5690;
do
filename[v4++] ^= 0xE9u;
while ( v4 != 53 );
v5 = 1;
name[0] = 47;
do
name[v5++] ^= 0xE9u;
while ( v5 != 47 );
j_app_dummy();
memset(v26, 0, sizeof(v26));
*a1 = v26;
a1[1] = sub_29B8;
a1[2] = sub_2B90;
v26[0] = (int)a1;
v26[1] = ASensorManager_getInstance();
v26[2] = ASensorManager_getDefaultSensor(v26[1], 1);
v6_start_time = 0;
v26[3] = ASensorManager_createEventQueue(v26[1], a1[7], 3, 0, 0);
v7 = (int *)a1[5];
if ( v7 )
{
v8 = v7[1];
v9 = v7[2];
v26[10] = *v7;
v26[11] = v8;
v26[12] = v9;
}
_android_log_print(4, "FindMyDex", "Can you shake your phone 100 times in 10 seconds?");
v10_shake_times = 0;
do
{
while ( 1 )
{
v12 = 0;
if ( !v26[4] )
v12 = -1;
v13 = ALooper_pollAll(v12, 0, v25, &v24);
if ( v13 >= 0 )
break;
if ( v26[4] )
{
v11 = *(float *)&v26[10] + 0.01;
if ( (float)(*(float *)&v26[10] + 0.01) > 1.0 )
v11 = 0.0;
*(float *)&v26[10] = v11;
sub_2C14(v26);
}
} // 第一个while循环结束
if ( v24 )
(*(void (__fastcall **)(_DWORD *))(v24 + 8))(a1);
if ( v13 == 3 && v26[2] )
{
while ( 1 )
{
do
{
if ( ASensorEventQueue_getEvents(v26[3], v28, 1) < 1 )
goto LABEL_51;
}
while ( v29 != 1 );
if ( (v10_shake_times & 1) != 0 )
{
if ( v30_angle >= -15.0 ) // 15.0看着像是个角度,而且摇晃应该有角度判定,所以猜测V30为摇晃角度angle
{
LABEL_30:
v14_new_shake_times = v10_shake_times;
goto LABEL_31;
}
if ( v10_shake_times == 1 ) // 当摇晃次数为1的时候初始化开始时间
v6_start_time = time(0); // 由此推出v6为摇晃开始时间,start_time
v14_new_shake_times = v10_shake_times + 1;// 由此推出V14为摇晃后的次数,暂定为new_shake_times
}
else
{
if ( v30_angle <= 15.0 )
goto LABEL_30;
v14_new_shake_times = v10_shake_times + 1;
if ( v10_shake_times >= 0 )
_android_log_print(4, "FindMyDex", "Oh yeah~ You Got it~ %d times to go~", 99 - v10_shake_times);// 由此可推出v10为摇晃的次数,shake_times
}
LABEL_31:
v10_shake_times = v14_new_shake_times; // 更新摇晃次数
if ( (unsigned int)(v14_new_shake_times - 1) <= 0x58 )// 如果摇晃次数<=89,(0x58是16进制,转换为十进制为88)
{
v10_shake_times = v14_new_shake_times;// 更新摇晃次数
v15_data_index = v14_new_shake_times / 10;// V15为int,所以次数是取摇晃次数除以10的整数部分,也就是取值为(0,1,2,3,4,5,6,7,8)
if ( v14_new_shake_times % 10 == 9 ) // 判断摇晃测试模10是否等于9,也就是摇晃测试的取值为(9,19,29,39,49,59,69,79,89)
{
v16 = off_43A18; // off_43A18是一个偏移量,该偏移量储存的值为unk_3CA10,v16是一个指针,所以此处存的应该是unk_3CA10的地址
v17_per_data = (int)off_43A18 / 10; // 将unk_3CA10的地址,也就是0x3CA10转化为int十进制并除以10,即数据长度除以10,把数据10等分,每份数据的长度per_data
v18_next_data = (v15_data_index + 1) * ((int)off_43A18 / 10);// v15的取值+1,后乘以per_data,由v15的取值可以得出,v18为数据编号+1的数据的起始位置,v15为每份数据的编号
if ( (int)off_43A18 / 10 * v15_data_index < v18_next_data )// 判断这份数据的起始位置是否小于下一份数据的其实位置(感觉这个条件是恒为真的。。。)
{
v19_start_index = &v3[v17_per_data * v15_data_index];// v15_data_index*v14_per_data为当前数据编号下数据的开始位置,因此,v19指向的是当前数据编号开始的位置的地址
do
{
--v17_per_data;
*v19_start_index++ ^= v14_new_shake_times;// 数据的每一位,与对应摇晃次数做异或运算
}
while ( v17_per_data ); // 该循环即对数据的每一位与对应的摇晃次数做或运算
}
if ( v14_new_shake_times == 89 )
{
while ( v18_next_data < (int)v16 )// 当摇晃次数等于89时,下一段数据就为最后一段数据,判断最后数据的开始位置是否小于数据总长度,即开始位置有没有达到数据的尾部
v3[v18_next_data++] ^= 0x59u; // 每一位与无符号的0x59u即89做异或运算
}
v10_shake_times = v14_new_shake_times + 1;
}
}
if ( v14_new_shake_times == 100 )
{
if ( time(0) - v6_start_time > 9 )
{
_android_log_print(4, "FindMyDex", "OH~ You are too slow. Please try again");// 挑战失败
qmemcpy(v3, &unk_7004, (size_t)off_43A18);
v10_shake_times = 0;
}
else // 挑战成功的逻辑
{
v20 = v6_start_time;
if ( uncompress(dest, &destLen, (const Bytef *)v3, (uLong)off_43A18) )// 解压数据,将v3的内容解压到dest缓冲区中,长度为off_43A18所在的数据,即unk_3CA10的地址,即0x3CA10
_android_log_print(5, "FindMyDex", "Dangerous operation detected.");// 解压失败则报错
v21 = open(filename, 577, 511); // 打开文件
if ( !v21 ) // 打开失败则报错
_android_log_print(5, "FindMyDex", "Something wrong with the permission.");
write(v21, dest, destLen); // 将解压后的文件写入filename中
close(v21); // 写入后关闭文件
free(dest); // 释放缓存
free(v3); // 释放缓存
if ( access(name, 0) && mkdir(name, 0x1FFu) )// 如果权限有问题则报错
_android_log_print(5, "FindMyDex", "Something wrong with the permission..");
sub_2368(a1);
remove(filename); // 删除文件
_android_log_print(4, "FindMyDex", "Congratulations!! You made it!");
sub_2250(a1);
v10_shake_times = 0x80000000;
v6_start_time = v20;
}
}
}
}
LABEL_51:
;
}
while ( !a1[15] );
sub_2BDA(v26);
return _stack_chk_guard - v58;
}