详解变形金刚
前言 Android逆向,最近一次写android逆向相关的文章已经是一年前了。。
难受
本题虽然简单,但是也值得深挖一下。在做题的时候我较为详细的记录了,在此我也会比较详细的讲讲,主要面向像我这样的小白,同大家分享。
工具 需要使用到的工具:
IDA7.0
JEB.android.decompiler.3.0.0.201808031948Pro
jni.h
BDOpener.apk 或者 mprop 调试环境:
已经Root的Google Nexues 6p (android 8.0)
安装Xposed框架
我的环境是在mac下 当然有些工具不是必须的。可以使用类似的工具替换。搞android的话,手机最好是买个原生的吧。
步骤 大体介绍一下做此题的流程。
[*]找到主要判断逻辑
[*]找到关键的eq函数
[*]分析、识别算法
[*]写出解密脚本
步骤并不难,每一步都有许多方法可以达到目的,同时需要处理一些细节的地方。
寻找判断逻辑方法一 Android逆向较为常用的工具,应该是jeb了,我这用的最近泄漏的版本。
疑难解决:
用jeb时非常容易运行不了,主要是因为JDK的版本问题,这里使用JEB.android.decompiler.3.0.0.201808031948Pro需要JDK11+
通过结果可以比较清楚的看到程序的逻辑,看起来似乎只要将用户名逆序即可!?
这部分代码比较简单。但是当我尝试使用34567890931进行输入,提示error ??答案明显不对,而且从逻辑上来看没有看到提示error的代码。因此这里肯定是有猫腻。
因此猜想程序执行的应该不是此Activity。
如果做过android开发,并且眼神比较好,也比较细心的话,肯定能看出问题。
此 MainActivity 继承自 AppCompiatActivity,而Activity的基类应该是AppCompatActivity,在jeb中直接双击AppCompiatActivity便可查看该类的定义,发现原来这里才是程序开始执行的位置。
此时再来看一下代码逻辑。
获取用户输入,调用native函数eq进行判断,然后判断长度是否24位,不足则补齐,并且对输入进行AES解密,最后打印结果。
整个的关键便在于native层的eq函数。
方法二 主要思路就是根据报错信息进行字符串搜索。当然最后也需要用到jeb。
我用apktool,当然使用jeb直接搜索来的更方便一些,只是我在做的时候尝试了,也就记录了。
命令apktool d Transformers.apk,之后在本地生成该apk的文件夹,在vscode下全局搜索字符串error
之后便可以在jeb中进行定位了。
jeb下直接搜索时,使用ctrl + f,更加方便快捷。
2. 找到关键的eq函数 通过jeb脱出so文件,IDA打开,发现没有找到想要的eq函数
Native函数注册 参考文章
在Android中通过jni调用本地方法(c/c++),通常使用javah生成规范的头文件,定义一个函数实现本地方法,函数名必须使用本地方法的全类名的规范来写。
Java_ + 包名 + 类名+ 接口名
示例如下:
JNIEXPORT jstring Java_com_example_test_MainActivity_helloworld(JNIEnv *, jclass ); jni还提供RegisterNatives方法进行注册Native函数。
jclass clazz;
clazz = env->FindClass("com/example/test/MainActivity");
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods)) < 0) {
return JNI_FALSE;
}
return JNI_TRUE; RegisterNatives中第二个参数gMethods是一个二维数组,代表着这个class里的每一个native方法所对应的实现的方法。写法如下示例:
static JNINativeMethod gMethods[] = {
{"helloworld", "()Ljava/lang/String;", (void*)Jni_helloworld}}; 第三个参数代表要指定的native的数量。此时将前面在jni中声明可以改为
jstring helloworld(JNIEnv *, jclass);
寻找eq函数 了解完JNI注册Native函数的几种方法后回到此题,由于在导出表中没有找到eq函数,因此可以知道此时是通过手动注册的。
因此我们可以定位到JNI_OnLoad函数
像这里v3 + 860其实是jni的方法,为了使IDA能够识别,需要手动导入jni.h文件头,有关的操作可以参考这
然后将int a1修改为JNIEnv * a1,其他有关变量也是类似。
经过修改之后可以清晰的看到Native函数的注册过程。其中关键的就是off_CC0E5014
有以上的铺垫,我们知道这个位置存放的就是JNINativeMethod结构
并且其中第三个变量sub_CC0E1784指向的就是要注册的函数地址。
当然通过一些经验我们也可以快速的找到eq函数,比如就此题来说通过字符串{9*8ga*l!Tn?@#fj'j$\\g;;可以猜测sub_CC0E1784函数可能是目标函数或者是目标函数的一部分。
也可以挨个查看一下,根据经验去寻找,猜测的方法太多不列举了。
分析、识别算法静态分析 就像其他很多师傅说的那样,主要就是修改的RC4以及修改的base64加密。
我在假期写过关于RC4算法识别,可以参考下
RC4是因为我看到了密钥,初始化S-Box以及乱序的操作。Base64是因为看到了base64的Table。
这里简要的说一下加密过程。
0. 生成密钥。 大致过程是先去除-,然后倒序。
这里生成的密钥是固定的,所以没有必要深入的分析,我是通过动态调试识别出这部分功能。
1. 初始化S—Box 这里是用dword_CC0E33E8中的数据作为初始数据。
2. 根据密钥生成临时数据K3. 依据K将S-Box乱序4. 将S-Box经过伪随机子密码生成算法得到子密钥序列 对于RC4算法来说最后一步是将子密钥序列同明文进行异或,就此题来说以上过程都是固定的,因此我们无需关注此算法经过何种修改,只要将最后的子密钥序列dump下来即可。
在进行异或之前他还对子密钥序列做了一次交换,不过这个过程是可逆的。
而后将异或的结果进行了base64加密。
这个判断起来不难,通过byte_CC0E5050数据就可以猜到
最后将结果同byte_CC0E34E8逐字节进行比较,分析到此算是结束了。
现在理一下解题思路,先还原base64,然后同RC4的子密钥序列进行异或。
那么最后我们还需要dump出子密钥序列。这就需要动态调试一番。
题外话,其实不需要调试也完全可以解决,只要照着加密过程,自己将RC4改改,同样能得到子密钥序列,我感觉应该只有初始化S-Box不同其他应该没有变动。我做的时候是动调的。
动态调试 参考WIKI
调试是逆向中不可少的。不过有时总会因为各种环境问题导致无法调试。我觉得如果打算做android逆向的话,最好还是准备只Google手机,能少遇点坑。
调试手机apk有几种方法,我大致总结了下。
1. 动态调试Java层APK 这里通常使用jeb进行动调,动调java主要是调试smail,当然这里也可以用AS进行动调,不过太麻烦了。JEB比较简单。
将需要调试的APK拖入jeb打开,在smail下断点,确保手机打开了开发者模式。
这里我用的手机是Google Nexues 6P,不知道为啥我用小米8,JEB没显示。
为了进行调试,需要对应的APK设置debuggable=true
这里可以使用mprop工具,如果手机上安装Xposed框架,那么可以直接使用BDOpener.apk模块
具体怎么使用找教程。
2. 动态调试android原生程序 手机ROOT,处于开发者模式,打开USB调试
上传android_server 并运行
同时进行端口转发
adb push android_server /data/local/tmp
chmod 775 /data/local/tmp/android_server
adb shell
su
./data/local/tmp/android_server
adb forward tcp:23946 tcp:23946 这是为了将手机的23946端口转发到本地的23946端口上,以便IDA进行通信。
将需要调试的原生程序上传至手机,并赋予可执行权限
adb push debugnativeapp /data/local/tmp/
adb shell chmod 755 /data/local/tmp/debugnativeapp IDA选择Debugger-Run-RemoteArmLinux/Android debugger
然后在IDA中设置程序,路径,配置HostName以及Port
同时设置Deubg Option,使IDA能在 entry ,load, start处断下。
容易遇到的问题。
如果遇到 error: only position independent executables (PIE) are supported. 一般是因为 android 手机的版本大于 5,可以尝试
使用 android_server 的 PIE 版本
利用 010Editor 将可执行 ELF 文件的 header 中的 elf header 字段中的 e_type 改为 ET_DYN(3)。3.原生SO动态调试(直接在so处下断) 这是比较简单的方式,这其实和SO运行和加载的时机有关,如果需要在加载SO之前也就是.init
因为so是依附于apk运行的,所以相对来说会比较复杂。
运行android_server 并且进行端口转发
在手机上运行apk,然后在IDA中attach程序,此时IDA便会在libc中断下。这时便可以调试native层的函数。
由于此方法需要apk在运行的时候附加调试,因此如果程序有加固或者在.init_array有解密,则无法进行调试。
使用这种方法时同样也能在libc和linker处断下,但是这个断点没有什么意义,因为程序本身已经加载完毕了。
4.原生SO动态调试(.init_array 以及 JNI_OnLoad) 为了理解,我们需要对so文件的加载过程有比较清晰的了解。
参考文章
能自己阅读下linker的源码那是最好的了。
android最基本的so是libc.so,通过libc加载linker,然后通过linker的call_function加载lib库。
就像文中总结的一样,当Java层调用static。。。时,系统加载so,首先执行.init和.init_array段的代码,之后如果存在JNI_OnLoad就调用该函数。后面就需要具体问题具体分析了。
同三类似,运行android_server,进行端口转发。
su
./data/local/tmp/android_server
adb forward tcp:23946 tcp:23946 不同的是需要以调试模式启动程序。我通过aapt可以快速的获取目标apk的一些信息。(习惯命令行)
/Users/jeb/Library/Android/sdk/build-tools/27.0.3/aapt dump badging Transformers.apkadb shell am start -D -n com.zhuotong.crackme/.MainActivity 此时可以打开IDA进行附加了。
此时会断在libc,然后根据需要设置Debugger Option
我是辣么设置的。
此时在IDA中F9运行,是不会有反应的。因为此时还需要恢复app的运行。
Wiki上说打开ddms,估计那个SDK的版本比较老,我是SDK-27,ddms改为monitor
在如下路径:/Users/jeb/Library/Android/sdk/tools/monitor
此时我们需要选中目标进程,这样就相当于是将app转发到电脑的jdb的调试端口默认是8700,而后使用jdb附加。
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=8700 此时在输出窗口可以看到加载了liboo000oo.so,然后他会在Jni_OnLoad处的断点停下。
这里需要注意一个顺序,先IDA附加,然后jdb附加使app恢复运行。
此时可以看到RegisterNatives函数的四个参数,其实是三个参数,第一个代表this,其中第三个参数可以很清楚的看到函数名,函数类型,以及地址。
我们找到eq函数,然后下断。根据前面的静态分析,我们在sub_CC0D6784函数下断,然后随便输入24个字符,最后将子密钥序列dump下来即可。
最后整理一下即可写出解密脚本。
这里其实还有一个点不知道各位有木有发现。
在静态分析时RegisterNatives函数的第二个参数。
其中byte_CC0DA0CA和byte_CC0DA0D0指向的都是乱码数据,只有最后一个地址是正确的,但当我们动态调试的时候,这两个指针却指向了eq和(Ljava/lang/String;)Z这是为什么呢?
还记得so的加载流程吗?
如果不记得请在此阅读一遍这篇参考文章
那就是.init_array段。
此处对应的就是datadiv_decode5009363700628197108函数
大概功能就是对so的部分数据进行解密。
我们可以直接在此函数下断,对解密部分代码进行调试,有时候反调试就会在这里设置。
如果有必要其实也可以在linker的call_function处下断,我们可以将/system/bin/linker pull 到本地进行分析。
有兴趣的可以试试。总之在何处下断,需要对so的加载流程非常的熟悉,以及合适需要IDA附加,程序运行到哪一步都需要自己把握清楚。
写出解密脚本 有了前面的分析,解密脚本也就非常的好写。
贴一下我的代码:
table="!:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\\'"
r="\x20{9*8ga*l!Tn?@#fj'j$\\g;;"
s = ""
for i in range(6):
s += chr(ord(r)^7)
s += chr(ord(r))
s += chr(ord(r)^0xf)
s += chr(ord(r))
def mydecodeb64(enc,padding):
enc=enc.replace(padding,"")
x="".join(map(lambda x:bin(table.index(x)).zfill(6),enc))
for ap in range(8-(len(x)%8)):
x+='0'
plain=[]
for i in range((len(x))/8):
plain.append(chr(eval('0b'+x)))
return "".join(plain).replace("\x00","")
s_box =
dec_one =mydecodeb64(s,padding = ";")
print len(s_box)
v30 = 0
v28 = 0
flag = ""
for i in range(16):
v28 = (v28+1)%256
v35 = s_box
v30 = (v30+v35)%256
s_box = s_box
s_box = v35
v17 = s_box
index = (v35+v17)%256
flag+=chr(s_box^ord(dec_one))
print flag总结 抽空做了几题,自己也回顾一下。不过还是太菜了。
pizza tql
页:
[1]