前言:这个apk是在我报名的某个培训班上课的实战题目,感觉挺难的,我也搞了好久,写出来是为了能够增加自己的影响,下次遇到这种类似的有一个分析的方向。
下面我对整个静态分析做一个整理,主要是增强一下我的印象。
刚开始分析这个的时候,胡乱的动态调试,发现啥也找不到,一脸懵逼,最终还是要靠把静态分析啃熟悉了,才能动态调试。不然会给你绕晕,
所以静态分析是特别重要!
先看一下apk文件, 。直接提示密码错误。
1.先对apk进行一个反编译。jadx-gui-0.9.0 打开目标apk,查看安卓配置文件,androidMainifest 配置清单文件找到程序入口界面。
看到我指向的一个就是程序的入口界面。直接在class 文件里面找到对应的类目。贴出来代码 关键代码,设置了一个按钮监听,如果点击则直接调用
[Asm] 纯文本查看 复制代码 public void click(View v) {///设置一个按钮监听事件
if (this.editText.getText().toString().equals("")) { //获取输入框的内容,判断是否为空
Toast.makeText(this, "Please Enter Your PassWord!", 0).show();//为空就弹出,Please Enter Your PassWord
} else {
NI.greywolf(this, this.editText.getText().toString());//如果,不为空,则调用ni.greywolf 方法,参数1 this 参数2 我们输入的密码
}
}
crlt +点击鼠标查看,这个方法的调用位置,
[Asm] 纯文本查看 复制代码 public class NI {
public static native void greywolf(Context context, String str); //通过查看greywolf 方法,是一个native修饰的,所以可以断定,这个方法在java层实现,并且是static 静态关键词修饰的。
}
在分析so的时候,一定要吧java层跟so层参数对应关系,对上去才能分析,不然都是白搭, 我也会尽量把我分析的过程写清楚,帮助大家搞懂,我也才能收获更多嘛。
到这里java层就分析完毕了,下面就要进入so层分析,我是用的IDA 7.0
《总结下,在java层,设置一个按钮监听事件,然后调用greywolf 方法,判断输入的注册码是否正确。 》
来到ida ,输出函数界面,ctrl+f 搜索java_ 没有任何结果,那就断定这个apk是采用的JNIload 动态注册
接着在输出函数界面,搜索JNI_ONLoAd ,
双击进入
[Asm] 纯文本查看 复制代码 ext:00014B58 PUSH {R4-R6,LR} ; JNIload 函数开始位置。
.text:00014B5A SUB SP, SP, #8
.text:00014B5C ADD R5, SP, #0x18+var_14 ; 以上,都是堆栈平衡,做准备工作,我们不分析这段代码
.text:00014B5E MOV R4, R0 此处调用了一个 j_j__Z2ADv 这里面就是apk反调试逻辑,
.text:00014B60 BL j_j__Z2ADv ; 看到一个这段代码,我们进入分析。
直接F5,
看伪代码
为了,验证我们的猜想,我们直接进入 j_j__Z2ADv
[Asm] 纯文本查看 复制代码 v0 = j_getpid(); // 获取一个进程pid
v1 = j_sprintf(&v40, "/proc/%d/status");
if ( !j_fork(v1) ) //这个就是程序自己fork一个子进程,然后自己附加,让我们ida 没办法附加。
[Asm] 纯文本查看 复制代码 j_std::basic_ifstream<char,std::char_traits<char>>::~basic_ifstream(&v62);
if ( !v3 )
{
v16 = j_fopen(&v40, "r");
do
{
if ( !j_fgets(&v38, 128, v16) )
goto LABEL_3;
}
while ( j_strncmp(&v38, "TracerPid", 9) );
v17 = j_atoi(&v39);
j_fclose(v16);
if ( !v17 )
{
LABEL_3:
j_sleep(1);
continue;
}
}
这里是获取 TracerPid 的值,也是一个反调试逻辑,这里如何过掉反调试,只需要 nop掉这个调用代码处,就过条这个反调试了。
我们来看看完整的汇编代码,
这里有一个关键就是注册函数
,程序逻辑我这里一遍
getenv
findclass
第三个是我们重点需要关注的代码。
注册函数逻辑。
[Asm] 纯文本查看 复制代码 .text:00014B92 MOVS R0, #0x35C
.text:00014B96 LDR R2, [R5]
.text:00014B98 LDR R4, [R2,R0]
.text:00014B9A LDR R0, =(_GLOBAL_OFFSET_TABLE_ - 0x14BA0)
.text:00014B9C ADD R0, PC ; _GLOBAL_OFFSET_TABLE_
.text:00014B9E LDR R2, =(dword_58058 - 0x57678)
.text:00014BA0 ADDS R2, R2, R0 ; dword_58058
.text:00014BA2 MOVS R3, #1
.text:00014BA4 MOV R0, R5
.text:00014BA6 BLX R4
到这里,我们的jniload 函数最关键位置,注册逻辑,
学习过ndk开发的都应该知道, 注册函数一共有四个参数,分别对应的是
1 ENV
2 CLASS
3 注册方法结构体,
4 需要注册函数数量。
这个时候,这几个参数我们理清楚了,下面我们就应该知道,反汇编的参数传递规则
第一个就是,参数 对应的寄存器,按照 R0 R1 R2 R3 顺序
那这时候对应起来的就是
R0 = ENV 上面有个getenv 获取出来的
R1 =CLASS 有个findclass
R2 =方法体 ADDS R2, R2, R0 ; dword_58058
R4 = 注册方法的个数 MOVS R3, #1
{这时候说点题外话,就是那要是有4个以上寄存器我们应该怎么办,就是把多余的参数存放在堆栈里面 然后调用。}、
这里面最关键的就是 方法体了,我们点击
2
3
3
这时候我们就进入了,程序逻辑实现的位置。
F5 查看伪代码。
这时候也到关键位置了,理清楚程序的整个逻辑。
伪代码上。
[Asm] 纯文本查看 复制代码 int (*sub_146A4())()
{
int (*result)(); // r0
dword_58058 = j_wolf_de("6C7A5CA7B2DC4B9C");
dword_5805C = j_wolf_de("234458B0A1C1489300D2572AA3D004E057970FFF6FC1318CF5F6135E6D062813D2642446BD540E79927E12CD4199");
result = bc;
dword_58060 = bc;
return result;
}
看到里面有个bc 方法,我们点击进入
[Asm] 纯文本查看 复制代码 int __fastcall bc(JNIEnv *a1, jclass a2, int a3, String str)//这里就很关键了,参数分别对应,env ,class ,javc层一个参数this,第二个参数,就是我们输入的字符串,这时候我们就要盯着这个str 看看,参数走向是如何走,很容易走偏,
{
String v4; // r6
int v5; // r4
JNIEnv *v6; // r5
int v7; // r0
int v8; // r1
int v10; // r0
v4 = str;
v5 = a3;
v6 = a1;
v7 = j_jstringTostring(a1, str);///第一个参数 ,env 第二个参数是我们输入的密码。转换一下
if ( j_dc(v6, v5, v7) )
return j_jk(v6, v8, v5, v4); //走这里,跟着传入的参数走
v10 = j_wolf_de("5B694AADB2DC559E44B84637A2D61F");
return j_st(v6, v5, v10);
*******************************************
[Asm] 纯文本查看 复制代码 int __fastcall jk(int a1, int a2, int a3, int a4)
{
int v4; // r4
int v5; // r5
int v6; // r0
int result; // r0
int v8; // r0
v4 = a3;
v5 = a1;
v6 = j_jstringTostring(a1, a4);
result = j_dc(v5, v4, v6); ///走这里。
if ( result )
{
v8 = j_wolf_de("486757B9B7D2538F089C402CA2CA12AF77D029B071D42787F6A22D502C193A1CCC6C2D49E629");
result = j_st(v5, v4, v8);
}
return result;
}
到这里。
[Asm] 纯文本查看 复制代码 int __fastcall dc(int a1, int a2, int a3)
{
int v3; // r4
int v4; // r5
int v5; // r6
int v6; // r1
int result; // r0
v3 = a3;
v4 = a2;
v5 = a1;
v6 = j_dh(a1, a2);
result = 0;
if ( v6 )
result = j_db(v5, v4, v3); //走这里...
return result;
} }
**************
[Asm] 纯文本查看 复制代码 int __fastcall db(int a1, int a2, int a3)
{
int v3; // r4
int v4; // r5
int v5; // r6
int v7; // r0
v3 = a3;
v4 = a2;
v5 = a1;
if ( j_dh(a1, a2) )
return j_ds(v5, v4, v3); 走这里
v7 = j_getpid();
j_kill(v7, 9);
return 0;
}
//******************
[Asm] 纯文本查看 复制代码 int __fastcall ds(int a1, int a2, int a3)
{
int v3; // r4
int v4; // r0
std::__node_alloc *v5; // r5
std::__node_alloc *v6; // r6
unsigned int v7; // r2
signed int v8; // r4
int v9; // r0
int result; // r0
char v11; // [sp+8h] [bp-8h]
char v12; // [sp+Ch] [bp-4h]
int v13; // [sp+10h] [bp+0h]
int v14; // [sp+20h] [bp+10h]
std::__node_alloc *v15; // [sp+24h] [bp+14h]
int v16; // [sp+28h] [bp+18h]
int v17; // [sp+38h] [bp+28h]
std::__node_alloc *v18; // [sp+3Ch] [bp+2Ch]
int v19; // [sp+40h] [bp+30h]
v3 = a3;
if ( j_dh(a1, a2) )
{
v4 = j_wolf_de("636D55B2AA8609CB");
sub_15184(&v16, v4, &v12);///整个程序就走到了这个位置! 这里返回出来的就是正确密码hello5.1
sub_15184(&v13, v3, &v11);
v5 = v18;
v6 = v15;
v7 = v17 - v18;
v8 = 0;
if ( v17 - v18 == v14 - v15 )
{
v8 = 1;
if ( j_memcmp(v18, v15) )
v8 = 0;
}
if ( v6 != &v13 && v6 )
{
j_std::__node_alloc::deallocate(v6, (v13 - v6), v7);
v5 = v18;
}
if ( v5 != &v16 && v5 )
j_std::__node_alloc::deallocate(v5, (v16 - v5), v7);
}
else
{
v9 = j_getpid();
j_kill(v9, 9);
v8 = 0;
}
result = _stack_chk_guard - v19;
if ( _stack_chk_guard == v19 )
result = v8;
return result;
}
程序终于给走完了,最终密码是 hello5.1 ,这个需要在动态调试的时候,在调用
sub_15184(&v16, v4, &v12); 程序返回的是密码。
这个加密是采用rc4 加密算法 ,可以在wolf_de 里面可以跟踪到
[Asm] 纯文本查看 复制代码 if ( j_RC4(v5, v8 >> 1, v2, v9, v7, &v11) )
{
v7[v11] = 0;
v4 = v7;
}
但是咱们呢,也不纠结是怎么得到的,只要我们能够把正确密码得到就算成功,
好了教程就到这里了,写得不好大家多多包涵,毕竟我也是刚学习安卓逆向没得多久,这段时间肺炎闹得厉害,打游戏呢,也打懵逼了,发个帖子记忆一下
有什么错误,欢迎大家指正,
链接:https://share.weiyun.com/5cr3Nm3 密码:v9q94t
|