攻防世界Mobile5 EasyJNI 安卓逆向CTF

EasyJNI

  最近正好在出写JNI,正好看到了一道JNI相关的较为简单明了的CTF,就一时兴起的写了,不得不说逆向工程和正向开发确实是可以互补互相加深的

JNI

  JNI(Java Native Interface)即java本地接口,众所周知,android有四层结构(也有说五层结构,即多了一个抽象层,这里不予讨论),应用层与应用接口层是用Java写的,而C/C++核心库和linux内核层由C/C++写的,既然知道了这一点,那理解JNI就很简单了,Java和C/C++肯定是不能直接互相调用的,那么应用层肯定就不能直接调用底层的东西,比如从应用层直接用Java想调用底层C/C++开发的启动相机或NFC等肯定是不能直接实现的。

  这就是JNI的作用了,它充当桥梁,向C/C++转译Java中的方法,向Java转译C/C++的函数,而NDK就是JNI开发工具。

开始解题

      首先不考虑那么多的先AndroidKiller试试

        比较简单的找到了

 

 

      既然都AK(AndroidKiller)了就索性看看JADE

        MainActivity一部分

 

 

    实不相瞒的说在看到这两个东西之前我还想过会不会就是名字放着看的,不是调用的JNI,既然看到这里了就知道了确确实实调用了JNI,那就一步一步的分析吧,首先看OnCreate()

        

 就这一句话是有用的,别的没啥用,这段代码中设置了一个点击事件,即创立了一个Button,并通过ID找到Button,然后设置SetOnClickListener设置监听,点击之后调用上图中的a()方法,传入的String是输入的String,通过a()方法返回一个布尔值

 

   那就回去看a方法就好了,先放个整体图出来吧

        

       这样就能看到这题恶心人的地方了,他的MainActivity中有a方法,但是还有一个a类,而且在a类中,还有一个静态的Byte[]也叫a,但没啥影响,降维理解一下这里吧:

       初始化一个a类locala,并把输入进去的String类型的字符串穿换成Byte[]类型组传入a类locala的a方法。看着可能绕,自己做的就还行了。

        

       那就看a类吧

          a类加密方法

   不知道是不是我jade版本的问题,看这个a数组里边的东西,明明是char型的东西,没有自动转换成字符,这就很烦了,于是我换成了JEB

 1 package com.a.easyjni;
 2 
 3 public class a {
 4     private static final char[] a;
 5 
 6     static {
 7         a.a = new char[]{'i', '5', 'j', 'L', 'W', '7', 'S', '0', 'G', 'X', '6', 'u', 'f', '1', 'c', 'v', '3', 'n', 'y', '4', 'q', '8', 'e', 's', '2', 'Q', '+', 'b', 'd', 'k', 'Y', 'g', 'K', 'O', 'I', 'T', '/', 't', 'A', 'x', 'U', 'r', 'F', 'l', 'V', 'P', 'z', 'h', 'm', 'o', 'w', '9', 'B', 'H', 'C', 'M', 'D', 'p', 'E', 'a', 'J', 'R', 'Z', 'N'};
 8     }
 9 
10     public a() {
11         super();
12     }
13 
14     public String a(byte[] arg10) {
15         int v8 = 3;
16         StringBuilder v4 = new StringBuilder();
17         int v0;
18         for(v0 = 0; v0 <= arg10.length - 1; v0 += 3) {
19             byte[] v5 = new byte[4];
20             int v3 = 0;
21             byte v2 = 0;
22             while(v3 <= 2) {
23                 if(v0 + v3 <= arg10.length - 1) {
24                     v5[v3] = ((byte)(v2 | (arg10[v0 + v3] & 0xFF) >>> v3 * 2 + 2));
25                     v2 = ((byte)(((arg10[v0 + v3] & 0xFF) << (2 - v3) * 2 + 2 & 0xFF) >>> 2));
26                 }
27                 else {
28                     v5[v3] = v2;
29                     v2 = 0x40;
30                 }
31 
32                 ++v3;
33             }
34 
35             v5[v8] = v2;
36             int v2_1;
37             for(v2_1 = 0; v2_1 <= v8; ++v2_1) {
38                 if(v5[v2_1] <= 0x3F) {
39                     v4.append(a.a[v5[v2_1]]);
40                 }
41                 else {
42                     v4.append('=');
43                 }
44             }
45         }
46 
47         return v4.toString();
48     }
49 }

   实不相瞒的说,要不是我去看别人的WP我都没有意识到这是变种的Base64加密,不过这个a作为密码表还是很明显的,那再重新理一下逻辑,输入了一个字符串Str,这个字符串在a.a中被加密成立Str1,Str1被传到了布尔型的native层的ncheck中经过检测,    并由返回的布尔值判断输出。

   那就简单了,看ncheck就好了,在ncheck中有判断需要逆向回去的字符串

      将apk解压

      

  

      so库就在lib下了

 

 

      IDA大法好

        IDA搜索

  Ctrl+F搜索ncheck就得到了想要的函数,分析一下这个函数吧,在v6之前说的都是废话,不用管

 1 v6 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
 2   if ( strlen(v6) == 32 )
 3   {
 4     v7 = 0;
 5     do
 6     {
 7       v8 = &s1[v7];
 8       s1[v7] = v6[v7 + 16];
 9       v9 = v6[v7++];
10       v8[16] = v9;
11     }
12     while ( v7 != 16 );
13     (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v5, v6);
14     v10 = 0;
15     do
16     {
17       v12 = __OFSUB__(v10, 30);
18       v11 = v10 - 30 < 0;
19       v16 = s1[v10];
20       s1[v10] = s1[v10 + 1];
21       s1[v10 + 1] = v16;
22       v10 += 2;
23     }
24     while ( v11 ^ v12 );
25     v13 = memcmp(s1, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u);
26     result = 0;
27     if ( !v13 )
28       result = 1;
29   }
30   else
31   {
32     (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v5, v6);
33     result = 0;
34   }
35   return result;

    不管别的,至少我第一眼看到的就是“MbT3sQgX039i3g==AQOoMQFPskB1Bsc7”,在这我大概就知道是Base64了,这个两个等号太明显了,而且前面的密码表很明显跟base64不一样,那就确定是base64的变种了。在这个函数中也有加密,先看一下这个加密吧,两个do...while,先看两个while,一个是v7由0变到16,一个是计算一个异或值。

    先看第一个do...while,就是将前16位与后16位交换位置,没啥好多说的

    看看第二个do..while,V12是一个判断溢出,__OFSUB__(int a,int b)经过查询之后的作用是判断a-b是否会产生溢出,即a+(-b)是否溢出,-30补码为11100010,如果要产生溢出,那最小的补码为00100000,十进制就是16,跟前面那个就有点意思了,也是16。并且在0到16期间也不可能减去30大于等于0,即V11永远为0。那么V11^V12的第一个跳出条件就是,当V10为16的时候。也能看出来作用就是两两交换位置。

    “v13 = memcmp(s1, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u)”这里,memcmp(String str1,String str2,int n)作用判断为判断str1与str2的前n位,而0x20就是32。

    从吾爱白嫖来的解密脚本:

#白嫖来的base64解密函数

base64_charset = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"

def decode(base64_str):
    base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
                    s != '=']
    resp = bytearray()
    nums = len(base64_bytes) // 4
    remain = len(base64_bytes) % 4
    integral_part = base64_bytes[0:4 * nums]

    while integral_part:
        tmp_unit = ''.join(integral_part[0:4])
        tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
        for i in tmp_unit:
            resp.append(i)
        integral_part = integral_part[4:]

    if remain:
        remain_part = ''.join(base64_bytes[nums * 4:])
        tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
        for i in tmp_unit:
            resp.append(i)

    return resp

#两两交换位置,还原so内的加密
flag = "AQOoMQFPskB1Bsc7MbT3sQgX039i3g=="
tmp=""
for i in range(len(flag)//2):
    tmp += flag[i*2+1] + flag[i*2]
print(tmp)
print(decode(tmp))

 


posted @ 2020-02-08 22:40  StarryNight_32  阅读(1085)  评论(0编辑  收藏  举报