看雪2018CTF APK-ExecuteTable

此题为一道移动APP的逆向题,里面的坑很多,无法一一表述。该篇文章也是看了大牛们的wp后,加上自己的调试心得和体会杂凑而成,力求写得详尽以帮助有需要的朋友们。

1.    概览

解压apk后发现2个文件与代码相关:classes.dex和libexecute_table.so。

JEB反编译apk的结果如下,有3个native函数,它们都为so中的函数

base64解密字符串

RmFpbGVk -->  Failed

U3VjY2Vzcw==  --> Success


很显然,我们需要lkdakjudajndn(v0)返回1。


IDA 7.0在JNI_OnLoad下断点,开4个cmd分别调试运行apk(:jdb那条指令中的port需要根据实际情况进行替换)

root@android:/data/local/tmp # ./android_server_nonpie

adb forward tcp:23946 tcp:23946

adb shell am start -D -ncom.rorschach.executetable/com.rorschach.executetable.MainActivity

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8602

奇怪的事情发生了,设置了断点却无法断下来?

很可能作者在JNI_OnLoad做了手脚,且此处的代码特别怪异

2. 详细分析

010editor查看so文件是否被更改,发现很多未定义的section_table

修改so节表,将节表的数目和偏移都修改为0

按照上述方法修改后,系统调用都识别出来了。

2.1 初始化函数分析

根据linker源码,so的执行顺序为:

.preinit_array->->.init->->.init array->->JNI_Onload->->java_com_XXX; 但 so 是不会执行.preinit_array 的, 可以忽略。而.init以及.init_array一般会作为壳的入口地方

运行readelf –d libexecute_table.so

可以看到INIT_ARRAY在0x34cd0处,直接丢到IDA查看,可以看到相关的初始化函数列表,为了方便观察交叉引用,我给每个函数重命了名。

初看觉得每个函数都很复杂,只注意到了init_array4_67BC开头处new了一段空间并且存入了几个关键函数:sys_open、sscanf、sys_read、sys_openat、sys_mprotect

定义function_table结构可以更好看出逻辑流程

在sys_openat和sys_mprotect处下断点

从sys_openat处的断点,可以发现sys_openat会打开两个文件/proc/self/status(sub_B8BC)、/proc/self/maps(sub_9390)

其中,打开/proc/self/maps是用来获取libexecute_table.so的基址,以便修改JNI_OnLoad的符号表

打开/proc/self/status是为了读取TracerPid用于反调试,如果TracerPid>0表示存在反调试,TracerPid<=0为正常程序。要patch一下这个函数,使其反调试失效。我patch的方式是将判断逻辑改反,由于对arm的汇编指令不熟,费了很大劲才找到网站http://armconverter.com,可以将arm指令转换为机器码。将patch后的so文件替换原so文件,并重新签名apk。

patch so:直接修改逻辑让tracePid>0做正常操作,将BLE.w改成BGT.w。

通过TracerPid定位到反调试进程sub_B8BC,依据是否处于调试状态决定0x36098的取值。如果处于调试状态是其值为 0xBD9813BA,否则为 0x2333AE83。后续lkdakjudajndn函数的计算过程将用到0x36098数值。

从sys_mprotect处的断点,发现2个函数调用了sys_mprotect:sub_86E0、sub_833C

sub_86E0用来修改符号表,将原始JNI_OnLoad地址=0x8205修改为0xA261。

[0x3208+4]=0x8205 --> [0x3208+4]=0xA261

sub_833C解密lkdakjudajndn函数。用前一个字节xor后一个字节来解密,第一个字节与0x66异或,总长度400,起始为0xAC98+150

当然了,这些逻辑也是我再看完大牛的wp才弄清楚的。其实在怎样找到真实JNI_OnLoad这个问题上,有个更为简便的方法。

2.2 寻找JNI_OnLoad加载地址

利用 https://bbs.pediy.com/thread-216701.htm 上的程序,伪造一个JavaVM(vmCode.c)来加载so,并趁机获得so加载基址和JNI_OnLoad函数地址(ida中直接显示的是错误的,应该是.init和.init_array中对JNI_OnLoad地址进行了修改所致)

#include

#include

#include

#include

typedef unsigned int u4;

int main()

{

    JavaVM* vm;

    JNIEnv* env;

    jint res;

    JavaVMInitArgs vm_args;

    JavaVMOption options[1];

    options[0].optionString ="-Djava.class.path=.";

    vm_args.version=0x00010002;

    vm_args.options=options;

    vm_args.nOptions =1;

   vm_args.ignoreUnrecognized=JNI_TRUE;

    printf("[+] dlopenlibdvm.so\n");

    void *handle =dlopen("/system/lib/libdvm.so", RTLD_LAZY);//RTLD_LAZY RTLD_NOW

    if(!handle){

    printf("[-] dlopenlibdvm.so failed!!\n");

    return 0;

    }

    //这里我先创建一个java虚拟机。因为JNI_ONload函数参数第一个参数为JavaVM。

    typedef int (*JNI_CreateJavaVM_Type)(JavaVM**,JNIEnv**, void*);

    JNI_CreateJavaVM_TypeJNI_CreateJavaVM_Func = (JNI_CreateJavaVM_Type)dlsym(handle,"JNI_CreateJavaVM");

    if(!JNI_CreateJavaVM_Func){

    printf("[-] dlsymfailed\n");

    return 0;

    }

   res=JNI_CreateJavaVM_Func(&vm,&env,&vm_args);

    void*si=dlopen("/data/local/tmp/libexecute_table.so",RTLD_LAZY);

    if(si == NULL){

    printf("[-] dlopenerr!\n");

    return 0;

    }

    typedef jint (*FUN)(JavaVM*vm,void* res);

    FUN func_onload=(FUN)dlsym(si,"JNI_OnLoad");

    u4load=(u4)(dlsym(si,"JNI_OnLoad"));

    load&=~0xfff;

   while(*(u4*)load!=0x464C457F)load-=0x1000;

    printf("%x%x\n",func_onload,load);

    if(func_onload==NULL)

        return 0;

    func_onload(vm,NULL);

    return 0;

}

Android.mk写成如下形式:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := vmCode

LOCAL_SRC_FILES := vmCode.c

include $(BUILD_EXECUTABLE)

将代码用ndk-build编译成可执行文件vmCode,然后在adb中执行

得出加载so基址与JNI_Load相差A261

我们找到正确JNI_OnLoad地址后在IDA中将其第一个参数的类型定义为JavaVM*,找到其中GetEnv的调用,将其第二个参数的类型定义为JNIEnv*,顺路找到了调用RegisterNatives的地址。(可能需要右键“Force call type”来显示全所有参数)

然后将上述程序调试运行,运行到RegisterNatives处为止(注意由于这里FindClass返回值为空,需要手工过掉一个跳转),成功得到lkdakjudajndn函数的注册地址为libexecute_table.so的基址+0xac98。

2.3 定位lkdakjudajndn函数的真实地址

网上看到很多朋友不知道怎样确定lkdakjudajndn函数的偏移地址0xac98?其实很简单,RegistreNatives函数需要4个参数:env、className、gMethods、numMethods,我们需要关注第3个参数,即类型为JNINativeMethod的gMethods参数。

static int registerNatives(JNIEnv* env, const char* className,

        JNINativeMethod*gMethods, int numMethods)

JNINativeMethod,定义如下:

typedef struct {

const char* name;

const char* signature;

void* fnPtr;

} JNINativeMethod;

第一个变量name是Java中函数的名字。

第二个变量signature,用字符串是描述了函数的参数和返回值

第三个变量fnPtr是函数指针,指向C函数。

说白了,JNINativeMethod包含3个指针。第3个指针就指向处理函数的真实地址。下面截图说明具体过程:

在真正的JNI_OnLoad的RegisterNatives直接下断点,根据R2的数值得出注册函数的地址。

从图中可以看出处理函数的地址为0x5D756C99,在IDA中定位到此,显示地址为0x5D756C98

而so的基址为0x5D74C000

因此,得出处理函数与基址的偏移为0x5D756C98-0x5D74C000=0xAC98

2.4 lkdakjudajndn函数的处理逻辑

lkdakjudajndn无法F5,如果此时在此处直接下断点,将会遇到错误。

当时我也是在0xAC98处直接下断点运行,每次运行到0xAD30程序崩溃退出。

后来经过一番折腾,才知道需要在sub_833c进行mprotect解密后,再在此处下断点调试,此时才能F5出程序逻辑。

现在我也不太明白,按理说JNI_OnLoad就调用了sub_833C,执行完JNI_OnLoad才会执行lkdakjudajndn函数,为啥直接执行lkdakjudajndn会有可能造成没有执行sub_833C呢?哪位朋友知道的话,麻烦告诉我一声哈。

F5的sub_AC98的函数如下,没有全部贴出

lkdakjudajndn函数的逻辑流程为:

1、将输入key的jstring对象转换成 cstring对象。

2、key长度必须大于等于10,小于等于20。

3、将输入key置换到最后一位,变成key_change1。

4、索引 0x7FC2 ,key_change2[i]

= 0x7FC2[ key_change1[i]],得到 key_change2。

5、将 key_change2按照字节进行低4位与高四位的置换生成key_change3。

6、将 key_change3以4字节形式与 0x36098内容进行异或,当处于调试状态是其值为 0xBD9813BA,否则为 0x2333AE83。得到 key_change4,从这里可知key长度为0x10。

7、将 key_change4相邻2字节交换,得到key_change5 ,即key_change5[2i] = key_change4[2i+1]   key_change5[2i + 1] = key_change4[2i]。

8、再次索引7FC2 ,key_change6[i]

= 7FC2[ key_change5 [i]],得到 key_change6。

9、将 key_change6的第一字符置换到最后位置,得到 key_change7。

10、将 key_change7的所有字符的高4位对字符串“A3Cw6Gb0OZWPU52s”进行索引,组成 key_change8_highString, 将 key_change7所有字符的低4位对字符串

“A3Cw6Gb0OZWPU52s”进行索引,并进行倒序。组成 key_change8_lowString, 然后将key_change8_highString与 key_change8_lowString进行拼接生成 key_change8。

11、生成字符串“3ww3U53wOAWG333wwPZ56GGw0PO02OUW”。

12、将 key_change8 与“3ww3U53wOAWG333wwPZ56GGw0PO02OUW”比较,相等返回1,否则返回其他。


0x7FC2存放了置换表,可以发现这一处表被分析成了arm指令,也就是说,题意就是可执行的表(Executable table)。

unsigned char table[0x100]= {

   0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 0x06, 0x21, 0x09, 0xF3, 0x5C,0x95, 0xAE, 0x66, 0x12,

   0x8F, 0x85, 0xC8, 0x5A, 0xBF, 0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5,0x8B, 0xA4, 0xC5, 0xC7,

   0xEA, 0xF6, 0x79, 0x1E, 0x3C, 0xBA, 0x97, 0x4E, 0x38, 0x60, 0x08, 0xDD,0xFA, 0xB3, 0xDE, 0x77,

   0x81, 0x41, 0x19, 0xF4, 0x52, 0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9,0xE7, 0x91, 0xE9, 0x54,

   0x82, 0xAD, 0x7E, 0x11, 0x35, 0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0x0A,0x74, 0x2F, 0xE2, 0x17,

   0x98, 0x0C, 0x70, 0x92, 0x47, 0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D,0x07, 0x72, 0x25, 0x04,

   0xB7, 0xC9, 0xCE, 0x0E, 0x9E, 0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF,0x39, 0xF0, 0xBB, 0xBD,

   0x46, 0x32, 0xE6, 0x9F, 0x4F, 0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB,0x20, 0xD2, 0x0B, 0xA5,

   0xEE, 0xE1, 0xA9, 0x2B, 0x84, 0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD,0xB6, 0xFC, 0x55, 0x7C,

   0x5F, 0xF8, 0x4C, 0x65, 0x2C, 0x30, 0xEF, 0x48, 0xD7, 0x0D, 0x0F, 0x1A,0x5E, 0xC0, 0x3A, 0x57,

   0x6A, 0x31, 0x00, 0xF1, 0x59, 0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E,0x26, 0x1D, 0x13, 0x15,

   0x89, 0x5D, 0xDA, 0x61, 0xD1, 0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49,0xEC, 0xE3, 0xD0, 0x34,

   0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B, 0xB2, 0x4A, 0xDF, 0xAC, 0x96,0xDC, 0xE8, 0xA0, 0xF9,

   0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE, 0x87, 0x28, 0xCC, 0x99, 0xE5,0x45, 0x58, 0x94, 0x23,

   0x22, 0xFB, 0x02, 0x01, 0x03, 0x8A, 0x7B, 0xB5, 0x1C, 0xA7, 0x44, 0xCD,0xA2, 0x51, 0x8E, 0x3F,

   0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D, 0xA6, 0x05, 0x2D, 0xA8, 0x80,0x6D, 0xB4, 0x76, 0x5B,

};

3. 计算sn

下面是VC代码

#include "stdafx.h"

#include

unsigned char g_indexKey[0x100] =

{ 0x40, 0x50, 0x78, 0x7A, 0x29, 0x88, 0xF7, 0x06, 0x21, 0x09, 0xF3,0x5C, 0x95, 0xAE, 0x66, 0x12,

 0x8F, 0x85, 0xC8, 0x5A, 0xBF,0x33, 0x3D, 0x86, 0x90, 0x8C, 0xED, 0xD5, 0x8B, 0xA4, 0xC5, 0xC7,

 0xEA, 0xF6, 0x79, 0x1E, 0x3C,0xBA, 0x97, 0x4E, 0x38, 0x60, 0x08, 0xDD, 0xFA, 0xB3, 0xDE, 0x77,

 0x81, 0x41, 0x19, 0xF4, 0x52,0x6B, 0xFF, 0xD8, 0x2A, 0xC2, 0xBC, 0xB9, 0xE7, 0x91, 0xE9, 0x54,

 0x82, 0xAD, 0x7E, 0x11, 0x35,0x93, 0xB0, 0xA1, 0x18, 0xC4, 0x53, 0x0A, 0x74, 0x2F, 0xE2, 0x17,

 0x98, 0x0C, 0x70, 0x92, 0x47,0x64, 0x16, 0xFE, 0x75, 0x83, 0x37, 0x8D, 0x07, 0x72, 0x25, 0x04,

 0xB7, 0xC9, 0xCE, 0x0E, 0x9E,0xEB, 0xCF, 0xB1, 0xDB, 0x71, 0x56, 0xAF, 0x39, 0xF0, 0xBB, 0xBD,

 0x46, 0x32, 0xE6, 0x9F, 0x4F,0x1B, 0x4D, 0x68, 0xF2, 0x4B, 0x2E, 0xCB, 0x20, 0xD2, 0x0B, 0xA5,

 0xEE, 0xE1, 0xA9, 0x2B, 0x84,0x14, 0x67, 0x63, 0x6F, 0x3E, 0x7F, 0xFD, 0xB6, 0xFC, 0x55, 0x7C,

 0x5F, 0xF8, 0x4C, 0x65, 0x2C,0x30, 0xEF, 0x48, 0xD7, 0x0D, 0x0F, 0x1A, 0x5E, 0xC0, 0x3A, 0x57,

 0x6A, 0x31, 0x00, 0xF1, 0x59,0x10, 0xB8, 0x9A, 0x43, 0x73, 0xA3, 0x6E, 0x26, 0x1D, 0x13, 0x15,

 0x89, 0x5D, 0xDA, 0x61, 0xD1,0x6C, 0xD3, 0xE0, 0xD9, 0x1F, 0xD4, 0x49, 0xEC, 0xE3, 0xD0, 0x34,   0x36, 0xC6, 0x24, 0xE4, 0xF5, 0xAA, 0x9B,0xB2, 0x4A, 0xDF, 0xAC, 0x96, 0xDC, 0xE8, 0xA0, 0xF9,    0xC1, 0x9C, 0xCA, 0x9D, 0x27, 0xC3, 0xBE,0x87, 0x28, 0xCC, 0x99, 0xE5, 0x45, 0x58, 0x94, 0x23,    0x22, 0xFB, 0x02, 0x01, 0x03, 0x8A, 0x7B,0xB5, 0x1C, 0xA7, 0x44, 0xCD, 0xA2, 0x51, 0x8E, 0x3F,    0x42, 0xD6, 0x69, 0xAB, 0x62, 0x3B, 0x7D,0xA6, 0x05, 0x2D, 0xA8, 0x80, 0x6D, 0xB4, 0x76, 0x5B,

};

bool GetKey(unsigned char *lastKey, unsigned int keyLen)

{

    unsigned int i, j;

    unsigned char temp;

    unsigned char lowByte = 0;

    unsigned char highByte = 0;

    unsigned char* highKey =(unsigned char*)"3ww3U53wOAWG333w";

    unsigned char* lowKey =(unsigned char*)"wPZ56GGw0PO02OUW";

    unsigned char* constKey =(unsigned char*)"A3Cw6Gb0OZWPU52s";

    memset(lastKey, 0, keyLen);

    //索引constKey

    for (i = 0; i < keyLen;i++)

    {

        for (j = 0; j < keyLen; j++)

        {

            if (highKey[i] ==constKey[j])

                break;

        }


        if (j >= keyLen)

            return false;

        highByte = j << 4;

        for (j = 0; j < keyLen; j++)

        {

            if (lowKey[i] ==constKey[j])

                break;

        }


        if (j >= keyLen)

            return false;


        lowByte = j;

        lastKey[i] |= highByte;

        lastKey[keyLen - 1 - i]|= lowByte;

    }

    //最后一个字节置换到第一个位置

    temp = lastKey[keyLen-1];

    for (i = keyLen-1; i >0;i--)

    {

        lastKey[i] = lastKey[i -1];

    }

    lastKey[i] = temp;


    //索引g_indexKey

    for (i = 0; i < keyLen;i++)

    {

        for (j = 0; j < 256;j++)

        {

            if (lastKey[i] ==g_indexKey[j])

                break;

        }


        if (j >= 256)

            return false;

        lastKey[i] = j;

    }

    //相邻字节交换

    for (i = 0; i <(keyLen>>1); i++)

    {

        temp = lastKey[i * 2];

        lastKey[i * 2] =lastKey[i * 2 + 1];

        lastKey[i * 2 + 1] =temp;

    }


    //与0x2333AE83异或

    unsigned int eorConstKey =0x2333AE83;

    unsigned int* p = (unsignedint*)lastKey;


    for (i = 0; i < (keyLen>> 2); i++)

    {

        p[i] = p[i] ^eorConstKey;

    }


    //高4位与低4位交换

    for (i = 0; i < keyLen;i++)

    {

        highByte = lastKey[i]>> 4;

        lowByte = lastKey[i]& 0xf;

        lastKey[i] = (lowByte<< 4) | highByte;

    }


    //索引g_indexKey

    for (i = 0; i < keyLen;i++)

    {

        for (j = 0; j < 256;j++)

        {

            if (lastKey[i] ==g_indexKey[j])

                break;

        }


        if (j >= 256)

            return false;


        lastKey[i] = j;

    }


    //最后一个字节置换到第一个位置

    temp = lastKey[keyLen - 1];


    for (i = keyLen - 1; i >0;i--)

    {

        lastKey[i] = lastKey[i -1];

    }


    lastKey[i] = temp;

    return true;

}


int _tmain(int argc, _TCHAR* argv[])

{

         unsigned char flag[0x10];

         unsigned int i;


         memset(flag, 0, 0x10);

         if (GetKey(flag, 0x10))

         {

                   for(i=0;i<0x10; i++)

                            printf("%c",flag[i]);

         }

         return 0;

}

sn = "C0ngRa7U1AtIoN2U"

4.  总结

掌握了.so的执行顺序:.init->->.init array->->JNI_Onload->->java_com_XXX

做题过程中还有2个疑问:

疑问1:在.init_array处下断点看到的指令不正确,也不能调试。是不是某些函数中途将其加密了呢?

疑问2:输入正确的sn,如果运行在已经patch过的apk上会显示Failed(调试lkdakjudajndn函数,返回值为1),而运行在原始apk则显示Success。是不是patch时破坏了某处逻辑?

你可能感兴趣的:(看雪2018CTF APK-ExecuteTable)