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

posted @ 2020-06-22 01:23  Helica  阅读(626)  评论(0编辑  收藏  举报