学逆向论坛

找回密码
立即注册

只需一步,快速开始

发新帖

116

积分

0

好友

25

主题

[Reverse] 详解变形金刚

发表于 2020-6-15 01:03:58 | 查看: 2384| 回复: 0

相关题目:


前言  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])) < 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.apk

详解变形金刚

详解变形金刚
adb 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[i*4])^7)
s += chr(ord(r[i*4+1]))
s += chr(ord(r[i*4+2])^0xf)
s += chr(ord(r[i*4+3]))

def mydecodeb64(enc,padding):
enc=enc.replace(padding,"")
x="".join(map(lambda x:bin(table.index(x))[2:].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[i*8:(i+1)*8])))
return "".join(plain).replace("\x00","")
s_box = [0xF0,0x37,0xE1,0x9B,0x2A,0x15,0x17,0x9F,0xD7,0x58,0x4D,0x6E,0x33,0xA0,0x39,0xAE,0x04,0xD0,0xBE,0xED,0xF8,0x66,0x5E,0x00,0xD6,0x91,0x2F,0xC3,0x10,0x4C,0xF7,0xA6,0xC1,0xEC,0x6D,0x0B,0x50,0x65,0xBB,0x34,0xFA,0xA4,0x2D,0x3B,0x23,0xA1,0x96,0xD5,0x1D,0x38,0x56,0x0A,0x5D,0x4F,0xE4,0xCC,0x24,0x0D,0x12,0x87,0x35,0x85,0x8E,0x6F,0xC6,0x13,0x9A,0xD3,0xFC,0xE7,0x08,0xAC,0xB7,0xE9,0xB0,0xE8,0x41,0xAA,0x55,0x53,0xC2,0x42,0xBC,0xE6,0x0F,0x8A,0x86,0xA8,0xCF,0x84,0xC5,0x48,0x74,0x36,0x07,0xEB,0x88,0x51,0xF6,0x7F,0x57,0x05,0x63,0x3E,0xFE,0xB8,0xC9,0xF5,0xAF,0xDF,0xEA,0x82,0x44,0xF9,0xCD,0x06,0xBA,0x30,0x47,0x40,0xDE,0xFD,0x1C,0x7C,0x11,0x5C,0x02,0x31,0x2C,0x9C,0x5F,0x46,0x27,0xC4,0x83,0x73,0x16,0x90,0x20,0x76,0x7B,0xF2,0xE3,0xF3,0x77,0x52,0x80,0x25,0x09,0x26,0x3F,0xC7,0x18,0x1B,0xA3,0xFF,0xFB,0xCB,0xA9,0x8C,0x54,0x7A,0x68,0xB4,0x70,0x4B,0xE2,0x49,0x22,0x7E,0xA5,0xB6,0x81,0x9D,0x4E,0x67,0xF1,0xA7,0x3C,0xD9,0x94,0xEF,0x32,0x6B,0x1F,0xB1,0x60,0xB9,0x64,0x59,0x01,0xB3,0x7D,0xE0,0x6C,0xAD,0x97,0x19,0xB5,0x3A,0xF4,0xD8,0x8D,0x98,0x03,0x93,0x1A,0xDC,0x1E,0x4A,0xC0,0x5A,0xE5,0xD1,0x3D,0x14,0xC8,0x79,0xBD,0x43,0xDB,0x69,0xD2,0x61,0x95,0x9E,0x21,0x45,0x89,0x2B,0xAB,0x29,0xA2,0x8B,0x2E,0xD4,0x0E,0x62,0xCA,0x28,218, 91, 114, 143, 153, 117, 238, 120, 12, 113, 191, 221, 206, 146, 106, 178]
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[v28]
v30 = (v30+v35)%256
s_box[v28] = s_box[v30]
s_box[v30] = v35
v17 = s_box[v28]
index = (v35+v17)%256
flag+=chr(s_box[index]^ord(dec_one[i]))
print flag
总结  抽空做了几题,自己也回顾一下。不过还是太菜了。
  pizza tql



温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的认可,还可以获得学币奖励,请尊重他人的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。

小黑屋|手机版|站务邮箱|学逆向论坛 ( 粤ICP备2021023307号 )|网站地图

GMT+8, 2024-4-27 00:27 , Processed in 0.114228 second(s), 41 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表