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
實不相瞞的說在看到這兩個東西之前我還想過會不會就是名字放着看的,不是調用的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類吧
不知道是不是我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大法好
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))