查看原文
其他

一款最简单的关于动态注册的APP分析

小白abc 看雪学院 2021-03-06
本文为看雪论坛优秀文章
看雪论坛作者ID:小白abc


题目来自看雪2w班4月第2题。


目标:找出flag。


WriteUp


拿到手,一看***加固,先脱壳,发现程序关键步骤native化了。



再打开so文件一看,动态注册。

本来想着动态调试吧,可是还得先过360加固的反调试,算了还是静态分析吧。

先看.init.array段:



可以看到一一进去查看函数伪代码,发现其实最关键的就是一个字符串动态解密的函数:

unsigned int str_decode(){ unsigned int v0; // r0 unsigned int v1; // r0 unsigned int v2; // r0 unsigned int v3; // r0 unsigned int v4; // r0 unsigned int v5; // r0 unsigned int v6; // r0 unsigned int v7; // r0 unsigned int v8; // r0 unsigned int v9; // r0 unsigned int v10; // r0 unsigned int v11; // r0 unsigned int v12; // r0 unsigned int result; // r0 unsigned int v14; // [sp+0h] [bp-38h] unsigned int v15; // [sp+4h] [bp-34h] unsigned int v16; // [sp+8h] [bp-30h] unsigned int v17; // [sp+Ch] [bp-2Ch] unsigned int v18; // [sp+10h] [bp-28h] unsigned int v19; // [sp+14h] [bp-24h] unsigned int v20; // [sp+18h] [bp-20h] unsigned int v21; // [sp+1Ch] [bp-1Ch] unsigned int v22; // [sp+20h] [bp-18h] unsigned int v23; // [sp+24h] [bp-14h] unsigned int v24; // [sp+28h] [bp-10h] unsigned int v25; // [sp+2Ch] [bp-Ch] unsigned int v26; // [sp+30h] [bp-8h] unsigned int i; // [sp+34h] [bp-4h] i = 0; do { v0 = i; ELF[i++] ^= 0x61u; // ELF } while ( v0 < 4 ); v26 = 0; do { v1 = v26; read_maps[v26++] ^= 0xBAu; // read /proc/self/maps } while ( v1 < 0x14 ); v25 = 0; do { v2 = v25; self_maps[v25++] ^= 0xE6u; // /proc/self/maps // } while ( v2 < 0xF ); v24 = 0; do { v3 = v24; frida[v24] ^= 0x51u; // frida // ++v24; } while ( v3 < 5 ); v23 = 0; do { v4 = v23; frida_so_found[v23++] ^= 0x98u; // found frida.so in memory } while ( v4 < 0x18 ); v22 = 0; do { v5 = v22; fmt[v22++] ^= 0x5Cu; // %x-%lx %4s %lx %*s %*s %s } while ( v5 < 0x19 ); v21 = 0; do { v6 = v21; end_oat[v21++] ^= 0xA6u; // .oat } while ( v6 < 4 ); v20 = 0; do { v7 = v20; found_frida[v20++] ^= 0x31u; // found frida in memory } while ( v7 < 0x15 ); v19 = 0; do { v8 = v19; course[v19++] ^= 0x86u; // course } while ( v8 < 6 ); v18 = 0; do { v9 = v18; check[v18] ^= 0x24u; // check ++v18; } while ( v9 < 5 ); v17 = 0; do { v10 = v17; sig[v17] ^= 0xFDu; // (Ljava/lang/Object;)Z ++v17; } while ( v10 < 0x15 ); v16 = 0; do { v11 = v16; MainActivity[v16] ^= 0xD1u; // com/kanxue/reflectiontest/MainActivity ++v16; } while ( v11 < 0x26 ); v15 = 0; do { v12 = v15; training[v15++] ^= 0x32u; // training } while ( v12 < 8 ); v14 = 0; do { result = v14; kanxue[v14++] ^= 0x18u; // kanxue } while ( result < 6 ); return result;}

发现都是一堆异或,没有什么难的算法。

直接IDA脚本,一一解密,从解密后的字符串可以看到,这个so存在检测frida的反调试,以及其他不知名的字符串。

再之后看.init函数:
 
 
实际上最终调用的是创建新线程执行函数sub_E80,跟进去看看:
 
 
大概就是检测frida是否注入进程,以及一些elf魔数的检查和是否存在oat文件的检查。

如果检测不通过,则kill掉进程。

接下来,看JNI_Onload函数:
 
 
就是动态注册而已,我们直接修改源码,跟踪动态注册的函数check:
 
 
可以观察到check函数地址为0xb39444c9,再通过cat /proc/13291/maps | grep native-lib命令查看libnative-lib.so的加载基地址为0xb3943000,这里的13291为进程pid。
 
 
从而确认check的函数偏移为0x14c9,找到对应函数:
 
 
可以发现这个函数做了4件事:

1. 检测输入string长度是否为20

2. 再次动态注册check函数到,新的native函数偏移为0x1234

3. 检测输入的string是否以kanxue开头

4. 调用新的check函数,即sub_1234函数,并传递输入字符串的第六位之后的字符串为参数


再次跟进sub_1234函数:


可以看到同上个函数一样,这个函数也做了四件事:

1. 检测输入string长度是否为14

2. 再次动态注册check函数到,新的native函数偏移为0x1148

3. 检测输入的string是否以training开头

4. 调用新的check函数,即sub_1148函数,并传递输入字符串的第8位之后的字符串为参数


最后再次跟进sub_1148函数:
 
 
这个函数只是检测了最后的字符串长度是否为6,并且将之与字符串course对比。
 
最终,拿到flag为kanxuetrainingcourse。

验证。
 
 
这个时候再看log,会发现确实动态注册了三次check 函数,且注册在了不同的位置。
 


后记


这个程序貌似存在一个bug,就是我第一次验证成功后,不退出程序,再次输入同样的flag,这个时候再check就不对了,而输入course就对了,估计是多次动态注册导致再次check时只走了最后sub_1148函数的原因。



-End -





看雪ID:小白abc

https://bbs.pediy.com/user-715334.htm 

*这里由看雪论坛 小白abc 原创,转载请注明来自看雪社区。


推荐文章++++

*   MacOS平台一个古老的恶意软件INIT29的分析

*   Ghidra 分析程序及个人感受

*   堆入门攻略-how2heap学习总结

*   使用frida hook插件化apk

*   WannaRen病毒逆向分析报告


好书推荐






公众号:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文” 一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存