安卓逆向的初步研究--从某恋app入手
主题:安卓app中的关键登录逻辑分析
目标:des算法分析,.so文件分析
样本:某恋v5.0.1 app
代码:main函数自实现,其它函数提取自app中的安卓无关代码
作者:by GKLBB
参考:Bu弃 https://www.chinapyg.com/forum.php?mod=viewthread&tid=119242&highlight=DES
无名Android逆向 系列视频
资源:
链接:https://pan.baidu.com/s/14FzEJt0uegp9XQhYr5iQoA
提取码:rrc0
test文件里是加密器,依据分析app逻辑用java写出
1.依据线索定位关键函数
抓包找到线索
在dex中搜索线索
com.a.a.a.b. s
switch
紧跟着加密代码
JSONObject v1_1 = new JSONObject(); //创建一个构建JSON字符串的对象
v1_1.put("xxxx", xxxx); //往里面加入key/value形式的键值对
v1_1.put("xxxx", xxxx);
v0 = com.a.a.a.f.a.a(v1_1.toString()).getBytes(); //com.a.a.a.f.a.a(v1_1.toString()) 就是具体的加密逻辑了
com.a.a.a.f.a.a
Jni.getInstance().encryptString(arg1);
com.Jni.encryptString
v4.append(this.getEncryptString(v2, true)); //调用this.getEncryptString(v2, true)加盐
DESencryption.getEncString(v4.toString(), this.getEncryptString("a", true).substring(0, 8));//getEncryptString就是libjni.so中的一个方法,这里有个细节容易被忽略,v2是输入v2字符串格式化json后hex转换后的结果,如果直接传入“a”其结果是a+盐
最终我们找到了两个关键函数加密用的getEncString,加盐用的getEncryptString
2.分析关键函数
分析getEncryptString
用ida生成的c伪码
int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNINativeInterface *a2, int a3, int a4)
{
_JNIEnv *v4; // r5@1
int v5; // r4@1
const char *v6; // r7@1
size_t v7; // r4@1
_JNIEnv *v8; // r0@2
char *v9; // r1@2
jstring (__cdecl *v10)(JNIEnv *, const char *); // r3@2
int result; // r0@6
char *s; // [sp+0h] [bp-828h]@1
int v13; // [sp+4h] [bp-824h]@1
char dest; // [sp+Ch] [bp-81Ch]@3
int v15; // [sp+80Ch] [bp-1Ch]@1
v4 = a1;
v5 = a3;
v13 = a4;
v15 = _stack_chk_guard;
g_env = a1;
s = (char *)initAddStr();//// 初始化一个字符串,即就是盐本身。
v6 = (const char *)jstringTostring(v4, v5);// 调用JNI的方法,把Java中的String变成C中的char *,即就是输入的v5转char×
v7 = strlen(s);// 求出初始化字符串的长度
if ( strlen(v6) + v7 <= 0x7FF )// 转成C的inputStr的长度和s的长度<0x7ff(2047)如果小于则拼上s.否则不拼
{
memset(&dest, 0, 0x7FFu);// 往dest这个地址填充0x7FF个0
strcat(&dest, v6);// 这里是把v6的值,也就是inputStr转成Char的值赋给dest
if ( v13 )// 第2个传参也就是a4 inputBool为true的时候,就在后面跟上初始值s。否则不跟。也就是说要想加入盐必需加入后的长度不能超过2047且为true时。
strcat(&dest, s);//加盐!!!!!!!!!!!!!!!!!!!!!!!1111
v8 = v4;// v8 = JNIEnv
v9 = &dest;
v10 = v4->functions->NewStringUTF;// v10 = NewStringUTF:把C的char* 转换成Java中的String
}
else//无盐直接转换string回去
{
v8 = v4;
v9 = (char *)v6;
v10 = v4->functions->NewStringUTF;//// v10 = NewStringUTF:把C的char* 转换成Java中的String
}
result = ((int (__fastcall *)(_JNIEnv *, char *))v10)(v8, v9);// 反转结果为string,调用NewStringUTF方法,把v10转换成String
if ( v15 != _stack_chk_guard )
_stack_chk_fail(result);
return result;
}
//总结一下,就是如果加盐后的长度不能超过2047且为true时,加盐,否则不加直接返回
分析initAddStr
用ida生成的c伪码
int initAddStr()
{
int v0; // r0@2
int v1; // r2@2
if ( !isInit ) //这里是如果初始化一次了,就不需要再执行了。也就是这里只会执行一次。
{
v0 = initInflect((int)jniStr); //反射到java层,参数就是“/key=i im lianai”
key = jstringTostring((int)g_env, v0, v1); //调用方法,把java的String变成C语言中的char*
isInit = 1;
}
return key;
}
//总结一下,这段是包含/key=i im lianai中间代码,跳转到initInflect
分析initInflect
用ida生成的c伪码
int __fastcall initInflect(int a1)
{
_JNIEnv *env; // r5@1
int v2; // r0@1
bool v3; // zf@1
int v4; // r7@1
const struct JNINativeInterface *v5; // r0@1
int (__fastcall *v6)(int *, const char *); // r3@3
const char *v7; // r1@3
int v8; // r0@2
const struct JNINativeInterface *v9; // r3@4
int v10; // r4@4
int v12; // [sp+Ch] [bp-1Ch]@1
v12 = a1;//保存入参
env = (_JNIEnv *)g_env;
//找出类ID
v2 = (*(int (__fastcall **)(_DWORD *, const char *))(*g_env + 24))(g_env, "com/Reflect");//clzID
v4 = v2=clzID;
v3 = v2 == 0;
v5 = env->functions;
if ( v3 )//clzID存在,自行下面代码段
{
v6 = (int (__fastcall *)(int *, const char *))env->functions;->NewStringUTF;
v7 = "jclass";
return v6((int *)env, v7);
}
//找出方法ID
v8 = ((int (__fastcall *)(_JNIEnv *, int, const char *, const char *))env->functions;->GetStaticMethodID)(
env,
clzID,
"func",
"(ILjava/lang/String;)Ljava/lang/String;");
v9 = env->functions;
v10 = v8=MethodID;
if ( !v8 )
{
v6 = (int (__fastcall *)(int *, const char *))env->functions;->NewStringUTF;
v7 = "method";
return v6((int *)env, v7);
}
//调用类.方法
((void (__fastcall *)(_JNIEnv *, int))env->functions;->NewStringUTF)(env, v12);//v12就是本函数入参/key=i im lianai,把v12转string,但是这里没有接收者,是因为伪代码不可信
return _JNIEnv::CallStaticObjectMethod(env, clzID, MethodID, 10);
}
//总结一下,,这段是真正的反射中间代码,调用java层的com.Reflect类中的func方法,并返回结果。
分析com.Reflect类!!!!!!!!!!
public static String func(int arg2, String arg3) {//这个就是IDA中调用的方法
return Reflect.encode(String.valueOf(arg3) + " alien"); //这里调用了此类中的encode方法,传入的是我们传递过来的参数“/key=i im lianai” 加上此类提供的一个静态字符串" alien"<注意前面有个空格>,综合起来也就是“/key=i im lianai alien”!!!!!!!!!!!!!!!!1
//总结一下,/key=i im lianai后加入alien,生成最终盐key=i im lianai alien返回
3.解密加密了的字符串
des是对称加密算法,因此密文:已知,算法:DES/CBC/PKCS5Padding, 密码:a2F6B657,IV:010230405060708,就可以解出明文,注意解出来的值还要转换HEX字符串然后每两位一组当作HEX字符串转换为accii,这是因为加密时ascii->hex->byte 解码就要byte->hex->ascii
附录:
分析前要清楚r0 r1 r2 r3四个寄存器的保存的是什么,保存的是少于4个的入参,超过4个用堆保存。
汇编无非就是操作寄存器并通过寄存器操作内存的过程。
分析指令关键看调用代码bl blx 跳转指令B
在所arm指令前先说说smali代码,smali是给dalvik vm使用的代码,而dvm类似于jvm,所以smali类似于在dalvik上跑的伪汇编代码
比如
java代码 a a1=b(123);
java代码之所以属于高级语言,容易读取,是因为到处用到了人可以识别的命名,并且把能合并参数的合并 能省略类型、包名的省略,再加上封装 所以得到了易读取的java
转换成smali代码(#为注解)
const-string v0,"123"#v0=123
invoke-virtual {v0} L包名/包名/包名/类名;->b(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
我们并没有看见a1这个名字是给人看的,我们看见的都是v0等等的虚拟寄存器,为什么叫虚拟寄存器因为只有虚拟机才能识别
转换成arm汇编(;为注解)
mov r0 123;r0=123
bl b();跳转到b并将r0传入
mov r1 r0 ;将返回值保存在r1中
在汇编代码里都是隐性的传入参数 隐性的返回,用到更多的寄存器和内存堆栈和地址,可读性更差
最终完整分析Java_com_jni_Jni_getEncryptString函数
.text:000010A4 ; =============== S U B R O U T I N E =======================================
.text:000010A4
.text:000010A4 ; r0 env
.text:000010A4 ; r1 obj
.text:000010A4 ; r2 input
.text:000010A4 ; r3 bool
.text:000010A4 ; r4 - r7 为结局变量声明,就是说后面要用临时用到这些个寄存器
.text:000010A4
.text:000010A4 ; int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNINativeInterface *a2, int a3, int a4)
.text:000010A4 EXPORT Java_com_jni_Jni_getEncryptString
.text:000010A4 Java_com_jni_Jni_getEncryptString
.text:000010A4
.text:000010A4 s = -0x828
.text:000010A4 var_824 = -0x824
.text:000010A4 dest = -0x81C
.text:000010A4
.text:000010A4 PUSH {R4-R7,LR}
.text:000010A6 LDR R6, =(__stack_chk_guard_ptr - 0x10B0) ; 栈保护代码忽略
.text:000010A8 LDR R4, =0xFFFFF7EC
.text:000010AA MOVS R5, R0 ; 保存env
.text:000010AC ADD R6, PC ; __stack_chk_guard_ptr
.text:000010AE LDR R6, [R6] ; __stack_chk_guard
.text:000010B0 ADD SP, R4
.text:000010B2 MOVS R4, R2 ; 保存input
.text:000010B4 LDR R2, =0x80C ; 80c
.text:000010B6 STR R3, [SP,#0x828+var_824] ; 存bool
.text:000010B8 LDR R3, [R6]
.text:000010BA ADD R2, SP ; sp+80c
.text:000010BC STR R3, [R2] ; sp+80c=栈保护
.text:000010BE LDR R3, =(g_env_ptr - 0x10C4)
.text:000010C0 ADD R3, PC ; g_env_ptr
.text:000010C2 LDR R3, [R3] ; g_env
.text:000010C4 STR R0, [R3] ; g_env = env 把env放进[r3]目的不知道
.text:000010C6 BL initAddStr ; 调用iniAddStr
.text:000010C6 ; nop
.text:000010CA MOVS R1, R4 ; 取出input
.text:000010CC STR R0, [SP,#0x828+s] ; 保存上一个函数的返回值salt到栈s,s=salt
.text:000010CE MOVS R0, R5 ; 取出env
.text:000010D0 BL jstringTostring ; r0 env
.text:000010D0 ; r1 =r4=r2=input
.text:000010D0 ; jstringTostring(env,input)
.text:000010D4 MOVS R7, R0 ; 保存*input
.text:000010D6 LDR R0, [SP,#0x828+s] ; s
.text:000010D8 BLX strlen ; r0 salt
.text:000010D8 ; 求盐长
.text:000010DC MOVS R4, R0 ; 存盐长
.text:000010DE MOVS R0, R7 ; s
.text:000010E0 BLX strlen ; r0 *input
.text:000010E0 ; 求入长
.text:000010E0 ; 将7ff存到堆为什么不存在寄存器里目的是方便后面调用
.text:000010E4 LDR R2, =0x7FF ; n
.text:000010E6 ADDS R0, R0, R4 ; 盐长加入长
.text:000010E8 MOVS R4, R6
.text:000010EA MOVS R6, #0x29C ; 偏移量668,即就是NewStringUTF
.text:000010EE CMP R0, R2 ; 总长比0x7ff
.text:000010F0 BLS loc_10FC ; <=则跳转
.text:000010F2 LDR R3, [R5]
.text:000010F4 MOVS R0, R5 ; env
.text:000010F6 MOVS R1, R7 ; *input
.text:000010F8 LDR R3, [R3,R6]
.text:000010FA B loc_1122 ; 跳转到NewStringUTF
.text:000010FC ; ---------------------------------------------------------------------------
.text:000010FC
.text:000010FC loc_10FC ; CODE XREF: Java_com_jni_Jni_getEncryptString+4Cj
.text:000010FC MOVS R1, #0 ; c
.text:000010FE ADD R0, SP, #0x828+dest ; s
.text:00001100 BLX memset ; r0 &dest
.text:00001100 ; r1 0
.text:00001100 ; r2 0x7ff
.text:00001100 ; 上一个函数的返回值不用所以直接r0=&dest
.text:00001104 ADD R0, SP, #0x828+dest ; dest
.text:00001106 MOVS R1, R7 ; src
.text:00001108 BLX strcat ; r0 &dest
.text:00001108 ; r1 *input
.text:0000110C LDR R3, [SP,#0x828+var_824] ; 取bool
.text:0000110E CMP R3, #0
.text:00001110 BEQ loc_111A ; 真则跳转
.text:00001112 ADD R0, SP, #0x828+dest ; dest
.text:00001114 LDR R1, [SP,#0x828+s] ; src
.text:00001116 BLX strcat ; 加入盐
.text:0000111A
.text:0000111A loc_111A ; CODE XREF: Java_com_jni_Jni_getEncryptString+6Cj
.text:0000111A LDR R3, [R5]
.text:0000111C MOVS R0, R5 ; env
.text:0000111E ADD R1, SP, #0x828+dest
.text:00001120 LDR R3, [R3,R6]
.text:00001122
.text:00001122 loc_1122 ; CODE XREF: Java_com_jni_Jni_getEncryptString+56j
.text:00001122 BLX R3 ; 跳转到NewStringUTF
.text:00001124 LDR R3, =0x80C
.text:00001126 ADD R3, SP
.text:00001128 LDR R2, [R3]
.text:0000112A LDR R3, [R4]
.text:0000112C CMP R2, R3 ; v15 = _stack_chk_guard
.text:0000112E BEQ loc_1134
.text:00001130 BLX __stack_chk_fail
.text:00001134 ; ---------------------------------------------------------------------------
.text:00001134
.text:00001134 loc_1134 ; CODE XREF: Java_com_jni_Jni_getEncryptString+8Aj
.text:00001134 LDR R3, =0x814
.text:00001136 ADD SP, R3
.text:00001138 POP {R4-R7,PC}
.text:00001138 ; End of function Java_com_jni_Jni_getEncryptString
.text:00001138
.text:00001138 ; ---------------------------------------------------------------------------
.text:0000113A ALIGN 4
.text:0000113C off_113C DCD __stack_chk_guard_ptr - 0x10B0
.text:0000113C ; DATA XREF: Java_com_jni_Jni_getEncryptString+2r
.text:00001140 dword_1140 DCD 0xFFFFF7EC ; DATA XREF: Java_com_jni_Jni_getEncryptString+4r
.text:00001144 dword_1144 DCD 0x80C ; DATA XREF: Java_com_jni_Jni_getEncryptString+10r
.text:00001144 ; Java_com_jni_Jni_getEncryptString+80r
.text:00001148 off_1148 DCD g_env_ptr - 0x10C4 ; DATA XREF: Java_com_jni_Jni_getEncryptString+1Ar
.text:0000114C ; size_t n
.text:0000114C n DCD 0x7FF ; DATA XREF: Java_com_jni_Jni_getEncryptString+40r
.text:00001150 dword_1150 DCD 0x814 ; DATA XREF: Java_com_jni_Jni_getEncryptString:loc_1134r
.text:00001154 CODE32