• 2020网鼎杯 青龙组 rev01 writeup


    简介

    青龙组(第一场比赛)的逆向题一共四道,两道PE两道android。rev01场上没有人做出来,这里场下补了这道题,分享一下解题思路。

    jadx打开apk文件,可以看到验证的逻辑很清晰,app输入框输入flag,首先验证长度和格式,之后调用native函数checkFlag进行验证。

    解题流程

    JNI_onLoad动态注册

    IDA 打开libcm1.so,并没有找到checkFlag函数,只有一个JNI_onLoad函数。进一步查询之后发现这是native函数动态注册的方法。

    JNI_onLoad动态注册的实践方法参考下面的代码,最终调用(*env)->RegisterNatives函数实现动态注册,该函数的参数JNINativeMethod结构体数组指示了注册函数的函数名、函数参数类型和函数地址。我们只要得到这个结构体数组就能够确定动态注册的函数了。

    //代码出处 https://blog.csdn.net/hk9259/article/details/43309361
    JNIEXPORT jstring JNICALL native_hello(JNIEnv *env, jclass clazz)
    {
    	//动态注册的native函数
    }
    
    // Java和JNI函数的绑定表
    static JNINativeMethod method_table[] = {
        { "HelloLoad", "()Ljava/lang/String;", (void*)native_hello },
    };
    
    // 注册native方法到java中
    static int registerNativeMethods(JNIEnv* env, const char* className,
            JNINativeMethod* gMethods, int numMethods)
    {
        //...
        if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
            return JNI_FALSE;
        }
        //...
    }
    
    int register_ndk_load(JNIEnv *env)
    {
        // 调用注册方法
        return registerNativeMethods(env, JNIREG_CLASS,
                method_table, NELEM(method_table));
    }
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
    {
        //...
        register_ndk_load(env);
    	//...
    }
    

    但是这道题的native函数加入了代码混淆,看起来像是一种控制流平坦化混淆,导致很难分析JNI_onLoad函数。

    JNI_onLoad的部分汇编代码,这部分实现了控制流平坦化的分发器,BR是ARM汇编的跳转指令。

    静态分析很难下手的情况下,考虑用动态的方法去做,JNI_onLoad动态注册的相关研究很多,其中有人开发了基于frida的hook脚本,直接获得动态注册的函数名和地址。脚本地址在这里:https://github.com/lasting-yang/frida_hook_libart。脚本的原理是hook libart.so的RegisterNatives函数,从而截获JNINativeMethod结构体。

    Frida动态调试环境搭建

    首先需要准备一台已root的android设备和一台调试主机。

    在调试PC上,Frida作者推荐使用pip进行安装,输入下面的命令

    pip install frida-tools

    安装完成后,检测frida是否安装成功。

    λ frida --version
    12.7.11

    之后需要在android设备上运行对应版本的frida-server,在frida的github release页面有很多很多的版本(这里吐槽一下开发frida的老哥,实在是太勤劳了),找到对应frida版本和设备的frida-server下载。例如,我的frida是12.7.11,设备是arm64位的Nexus 6p,那么需要下载frida-server-12.7.11-android-arm64.xz。

    解压frida-server,使用android的adb上传到设备上,以root权限运行。

    adb push ./frida_server_arm64 /data/local/tmp

    adb shell

    su

    cd /data/local/tmp

    ./frida-server-arm64

    之后还需要用adb做把frida用的端口转出,执行下面两条命令

    adb forward tcp:27042 tcp:27042

    adb forward tcp:27043 tcp:27043

    frida_hook_libart

    安装配置好Frida、frida-server和frida_hook_libart脚本之后,在主机执行

    frida -U --no-pause -f com.ichunqiu.rev01 -l hook_RegisterNatives.js

    这样我们在设备上运行rev01,主机的frida就成功的输出了checkFlag的函数地址,可以看到下图的offset: 0x1004c就是checkFlag函数的偏移。

    这里还有一个坑,网上的资料大部分都讲registerNatives函数在libdvm.so中,可是新版的android已经没有libdvm了,现在这些函数都在libart当中。

    checkFlag函数分析

    IDA反编译checkFlag之后,发现和JNI_onLoad一样加了代码混淆,确实是绕不过去了。静态分析时,跳转代码的地址很难确定,这里搭建了IDA+android设备的远程调试环境。将IDA的android_server64拷贝到设备目录下,以root权限运行,之后把端口转出就可以调试了。

    接下来就需要动态调试了,主要是在关键代码下断点,追踪程序对输入字符串的处理。此外还要考虑到程序的控制流平坦化,最好用笔记下函数的调用关系和控制流平坦化后代码块的调用关系,不然很容易跟丢函数。

    base58在调试的时候主要是通过发现0x3a这个关键的数和offset_2B9E0数组来判断;RC4函数的初始化过程比较明显,调试的时候发现初始化了一个从0x00-0xFF的数组,基本就确定是RC4了,再去查看RC4初始化函数的参数就能RC4的密钥。最后的字符串对比逻辑也比较容易找到。

    checkFlag函数的伪代码如下,忽略的很多细节上的操作:

    checkFlag(input_string):
    	vector v = base58_decode(input_string)
    	s[256]	//rc4数组
    	k[16]	//rc4密钥
    	
    	for i 0->16:
    		k[i] = offset_2b9cb[i] ^ offset_2b9af[i]
    	plain = string(v)		//把vector中存储的数据以字符串的形式放在plain中
    	rc4_init(s, k)
    	rc4_encrypt(plain)
    	cipher = offset_48830	//做最终对比的结果值
    	if plain == cipher:
    		return 1			//通过校验
    	else:
    		return 0
    

    那么最后编写python脚本就得到了flag

    from Crypto.Cipher import ARC4
    
    tbl = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 
      0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 
      0x0E, 0x0F, 0x10, 0xFF, 0x11, 0x12, 0x13, 0x14, 0x15, 0xFF, 
      0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 
      0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x21, 0x22, 0x23, 
      0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0xFF, 0x2C, 
      0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 
      0x37, 0x38, 0x39, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
    
    key = 'F2 B5 A4 0D FE A8 3A 4E B4 7A AB A1 E6 C9 ED 77'.replace(' ', '').decode('hex')
    res = '34 93 C5 DD DA 0F BB 94 37 BB D6 DE EA F3 53 41 56 C9 5F 42 E7 F6'.replace(' ', '').decode('hex')
    
    cipher = ARC4.new(key)
    
    c = cipher.encrypt(res)
    
    print c.encode('hex')
    
    
    n = int(c.encode('hex'), 16)
    print n
    
    flag = ''
    
    def reverse_tbl(x):
        for i in xrange(0xff):
            if tbl[i] == x:
                return chr(i)
        return -1
    
    def base58_encode(hexstr_input):
        n = int(hexstr_input, 16)
        res = ''
        while True:
            if n == 0:
                break
            t = n % 0x3a
            n /= 0x3a
            res = reverse_tbl(t) + res
        
        return res
    
    def base58_decode(b58_str):
        res = 0
        for i in xrange(len(b58_str)):
            tmp = tbl[ord(b58_str[i])]
            res *= 0x3a
            res += tmp
        
        return hex(res)
    
    s = base58_encode(c.encode('hex'))
    print s
    print base58_decode(s)
    

    总结

    这道题的主要难点在动态调试ARM64平台的混淆代码,没有处理的脚本只能手动调试。ARM64的汇编代码很复杂,这里被坑的很深,比如下面的两条汇编,如果不查文档的话可能理解为ADD和OR,意思就完全错了。

    MADD

    EOR

    除了最开始的JNI_onLoad用脚本比较快速解决之外,主要还是靠人肉逆向做题。如果有什么好的解决ARM代码混淆的思路欢迎一起讨论。

    参考文献

    [1] 抖音火山视频的Native注册混淆函数获取方法 http://www.520monkey.com/archives/1289

    [2] Android逆向新手答疑解惑篇——JNI与动态注册 https://bbs.pediy.com/thread-224672.htm

    [3] ARM64 OLLVM反混淆 https://bbs.pediy.com/thread-252321.htm

    [4] ARM汇编代码的官方手册 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0802b

    [5] Android JNI动态注册Native 方法 https://blog.csdn.net/hk9259/article/details/43309361

  • 相关阅读:
    前缀判断 蓝桥杯
    dedecms 网站内容静态化和动态化的切换
    dedecms 频道标签 channel.lib.php的分析
    JavaScript通过闭包解决只能取得包含函数中任何变量最后一个值的问题
    JavaScript闭包 取for循环i 【转】
    JavaScript装饰模式
    JavaScript闭包意义谈
    JavaScriptjs闭包测试
    JavaScript闭包的作用谈(转)
    Zend Engine 简介
  • 原文地址:https://www.cnblogs.com/helica/p/13174980.html
Copyright © 2020-2023  润新知