详解变形金刚

2019-04-04 约 4903 字 预计阅读 10 分钟

声明:本文 【详解变形金刚】 由作者 Badrer 于 2019-04-02 09:52:00 首发 先知社区 曾经 浏览数 2638 次

感谢 Badrer 的辛苦付出!

前言

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的话,手机最好是买个原生的吧。

步骤

大体介绍一下做此题的流程。

  1. 找到主要判断逻辑
  2. 找到关键的eq函数
  3. 分析、识别算法
  4. 写出解密脚本

步骤并不难,每一步都有许多方法可以达到目的,同时需要处理一些细节的地方。

寻找判断逻辑

方法一

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是因为看到了base64Table

这里简要的说一下加密过程。

0. 生成密钥。

大致过程是先去除-,然后倒序。
这里生成的密钥是固定的,所以没有必要深入的分析,我是通过动态调试识别出这部分功能。

1. 初始化S—Box

这里是用dword_CC0E33E8中的数据作为初始数据。

2. 根据密钥生成临时数据K

3. 依据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最基本的solibc.so,通过libc加载linker,然后通过linkercall_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-27ddms改为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_CC0DA0CAbyte_CC0DA0D0指向的都是乱码数据,只有最后一个地址是正确的,但当我们动态调试的时候,这两个指针却指向了eq(Ljava/lang/String;)Z这是为什么呢?

还记得so的加载流程吗?

如果不记得请在此阅读一遍这篇参考文章

那就是.init_array段。

此处对应的就是datadiv_decode5009363700628197108函数

大概功能就是对so的部分数据进行解密。

我们可以直接在此函数下断,对解密部分代码进行调试,有时候反调试就会在这里设置。

如果有必要其实也可以在linkercall_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

关键词:[‘安全技术’, ‘移动安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now