VNCTF2022 Android cm1.apk

只有一个Android,我也就只做了个Android,题目链接: https://buuoj.cn/match/matches/81/challenges

首先定位到com.vnctf2022.cm1.MainActivity,发现里面并没有特别的地方,如果安装运行并配合反编译代码,可以很快明白起始这里就是实现了个进度条和反馈收集功能,真正的逻辑在com.vnctf2022.cm1.Main中。

具体到com.vnctf2022.cm1.Main.init_widget() 中,程序先获取了输入然后调用check函数来检查,check()函数中直接去加载了一个dex文件来获取了一个newCls2Check对象进行具体的校验。dex动态加载逻辑在loadDexClass()中,对应的dex文件就是asset目录下的ooo文件。虽然这里有native层的操作,但是都是无关紧要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// com.vnctf2022.cm1.Main
private Boolean check(String input) {
IHeihei newCls2check = this.loadDexClass();
if(newCls2check == null) {
Toast.makeText(this, "errrrrrrrorrrr what are you doing!!!!!", 0).show();
return Boolean.valueOf(false);
}
return newCls2check.hcheck(input).booleanValue() ? Boolean.valueOf(true) : Boolean.valueOf(false);
}

private void init_widget() {
this.btn1 = (Button)this.findViewById(0x7F070023); // id:btn1
this.txt1 = (TextView)this.findViewById(0x7F070094); // id:txt1
this.edit1 = (EditText)this.findViewById(0x7F070039); // id:edit1
this.txt1.setText(this.stringFromJNI2());
this.btn1.setOnClickListener(new View.OnClickListener() {
@Override // android.view.View$OnClickListener
public void onClick(View arg3) {
String input = Main.this.edit1.getText().toString();
Main.this.txt1.setText(input);
if(Main.this.check(input).booleanValue()) {
Toast.makeText(Main.this.getBaseContext(), "you are right~", 0).show();
return;
}
Toast.makeText(Main.this.getBaseContext(), "Wrong!", 0).show();
}
});
}

private IHeihei loadDexClass() {
File v0 = this.getDir("dex", 0);
String dexPath = v0.getAbsolutePath() + File.separator + "classes.dex";
File dexFile = new File(dexPath);
try {
if(!dexFile.exists()) {
dexFile.createNewFile();
FileUtils.copyFiles(this, "ooo", dexFile);// 这里的copyFile是自己实现的,并不是直接复制
}
}
catch(IOException v2_1) {
v2_1.printStackTrace();
}

DexClassLoader v2_2 = new DexClassLoader(dexPath, v0.getAbsolutePath(), null, this.getClassLoader());
try {
IHeihei clsInstance = (IHeihei)v2_2.loadClass("com.vnctf2022.cm1.Haha").newInstance();
if(clsInstance != null) {
return clsInstance;
}
Log.e("Mz1", " --- loaderr");
}
catch(Exception v0_1) {
v0_1.printStackTrace();
}
return null;
}

FileUtils.copyFiles是程序自己实现的,包含了一定的解密逻辑,所以直接用jeb来反编译ooo文件是行不通的。一种简单的办法就是把安装apk并运行,然后直接去目录下面找到对应的classes.dex文件来分析即可。

分析classes.dex,其中com.vnctf2022.cm1.Haha类包含了相关的校验逻辑,整个校验逻辑稍微复杂了一些,但是耐心梳理还是可以理清的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// classes.dex com.vnctf2022.cm1.Haha
package com.vnctf2022.cm1;

import android.content.Context;
import android.widget.Toast;

public class Haha implements IHeihei {
public String flag;

public Haha() {
this.flag = "flag{fake_flag_again}";
}

public static byte[] bencrypt(byte[] input, byte[] keys) {
return input.length == 0 ? input : Haha.toByteArray(Haha.encrypt(Haha.toIntArray(input, false), Haha.toIntArray(keys, false)), false);
}

public static int[] encrypt(int[] input, int[] keys) {
int rounds = 52 / input.length + 6;
int rnd_s = 0;
int prev_int = input[input.length - 1];
do {
rnd_s += -1640531527;
int rnd_e = rnd_s >>> 2 & 3;
int ind;
for(ind = 0; ind < input.length - 1; ++ind) {
int ind_chr = input[ind] + ((prev_int >>> 5 ^ input[ind + 1] << 2) + (input[ind + 1] >>> 3 ^ prev_int << 4) ^ (rnd_s ^ input[ind + 1]) + (keys[ind & 3 ^ rnd_e] ^ prev_int));
input[ind] = ind_chr;
prev_int = ind_chr;
}

int next_int = input[0];
int last_ind = input.length - 1;
int lc = input[last_ind] + ((prev_int >>> 5 ^ next_int << 2) + (next_int >>> 3 ^ prev_int << 4) ^ (rnd_s ^ next_int) + (keys[ind & 3 ^ rnd_e] ^ prev_int));
input[last_ind] = lc;
prev_int = lc;
--rounds;
}
while(rounds > 0);

return input;
}

@Override // com.vnctf2022.cm1.IHeihei
public Boolean hcheck(String input) {
byte[] aim = new byte[]{68, 39, -92, 108, -82, -18, 72, -55, 74, -56, 38, 11, 60, 84, 97, -40, 87, 71, 99, -82, 120, 104, 0x2F, -71, -58, -57, 0, 33, 42, 38, -44, -39, -60, 0x71, -2, 92, -75, 0x76, -77, 50, 0x87, 43, 0x20, -106};
byte[] _tmp = Haha.bencrypt(input.getBytes(), "H4pPY_VNCTF!!OvO".getBytes());
if(_tmp.length != aim.length) {
return Boolean.valueOf(false);
}

int i;
for(i = 0; i < _tmp.length; ++i) {
if(_tmp[i] != aim[i]) {
return Boolean.valueOf(false);
}
}

return Boolean.valueOf(true);
}

@Override // com.vnctf2022.cm1.IHeihei
public void hi(Context arg3) {
Toast.makeText(arg3, "anothor dex", 1).show();
}

private static byte[] toByteArray(int[] arg5, boolean arg6) { // 把intArray分拆为byteArray
int n = arg5.length << 2;
if(arg6) {
int m = arg5[arg5.length - 1];
if(m > n) {
return null;
}

n = m;
}

byte[] result = new byte[n];
int i;
for(i = 0; i < n; ++i) {
result[i] = (byte)(arg5[i >>> 2] >>> ((i & 3) << 3) & 0xFF);
}

return result;
}

private static int[] toIntArray(byte[] arg7, boolean arg8) {
int[] result;
int v0 = (arg7.length & 3) == 0 ? arg7.length >>> 2 : (arg7.length >>> 2) + 1;
if(arg8) {
result = new int[v0 + 1];
result[v0] = arg7.length;
}
else {
result = new int[v0];
}

int i;
for(i = 0; i < arg7.length; ++i) {
int v3 = i >>> 2;
result[v3] |= (arg7[i] & 0xFF) << ((i & 3) << 3); // result[i]是一个int,对应输入的每连续4个char
}

return result;
}
}

输入的string input先转换成byte[],然后送入bencrypt(byte[], byte[])进行加密,使用到的密钥是"H4pPY_VNCTF!!OvO".getBytes()

bencrypt()函数中,先把byte[] inputbyte[] keys转换为int序列。其实就是每4个byte合并成int32,然后送入encrypt(int[], int[])进行加密,然后把这个加密函数返回的int[]转换回byte[],其实就是前面转换的逆过程,把每个int32拆解为4个byte。然后比较得到的byte[]byte[] aim是否一致。

具体到encrypt()内部,是一个对input的多轮加密过程。简单来说,每轮加密时,下一轮密文next_input[i]由本轮的input[i+1]、刚刚计算得到的next_input[i-1]、keys[]和两个轮密钥rnd_s和rnd_e计算得到。(input[-1]=input[length-1], input[length]=input[0])

$next_input[i] = func(input[i+1], next_input[i-1], keys[], rnd_s, rnd_e), i : 0 \rightarrow n$

这个过程可以视作顺序的递推,是可逆的,逆操作就是倒序的逆推。

$input[i]=reverse_func(input[i+1], next_input[i-1], keys[], rnd_s, rnd_e), i : n \rightarrow 0$

涉及到的三组密钥都是固定的,与输入无关。
aim的长度为44,所以input也即flag长度为44,共计加密10轮。

(后来才知道这是个XXTEA,这个加密算法家族包括TEA、XTEA、XXTEA

解密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import java.util.Arrays;

public class vnctfcm1apk{
public static byte[] bencrypt(byte[] input, byte[] keys) {
return input.length == 0 ? input : toByteArray(encrypt(toIntArray(input, false), toIntArray(keys, false)), false);
}

public static int[] encrypt(int[] input, int[] keys) {
System.out.println("input: " + Arrays.toString(input));
System.out.println("keys : " + Arrays.toString(keys));

int rounds = 52 / input.length + 6;
int rnd_s = 0;
int prev_int = input[input.length - 1];
do {
rnd_s += -1640531527;
int rnd_e = rnd_s >>> 2 & 3;

System.out.println("[rnd_e:" + rnd_e + ", rnd_s:" + rnd_s + "]");
int ind;
for(ind = 0; ind < input.length - 1; ++ind) {
int ind_chr = input[ind] + ((prev_int >>> 5 ^ input[ind + 1] << 2) + (input[ind + 1] >>> 3 ^ prev_int << 4) ^ (rnd_s ^ input[ind + 1]) + (keys[ind & 3 ^ rnd_e] ^ prev_int));
input[ind] = ind_chr;
prev_int = ind_chr;
}

int next_int = input[0];
int last_ind = input.length - 1;
int lc = input[last_ind] + ((prev_int >>> 5 ^ next_int << 2) + (next_int >>> 3 ^ prev_int << 4) ^ (rnd_s ^ next_int) + (keys[ind & 3 ^ rnd_e] ^ prev_int));
input[last_ind] = lc;
prev_int = lc;
--rounds;
}
while(rounds > 0);

System.out.print("try encrypt: [");
for(byte a:toByteArray(input, false)){
System.out.print(a + ",");
}
System.out.println("]");

return input;
}

public static int[] decrypt(int[] aim, int[] keys){
int[] preaim = new int[aim.length];
int[] rnd_s = new int[]{-1640531527,1013904242,-626627285,2027808484,387276957,-1253254570,1401181199,-239350328,-1879881855,774553914};
int[] rnd_e = new int[]{2,0,2,1,3,1,3,2,0,2};

for(int rnd=9;rnd>=0;rnd--){

int ind = aim.length - 1;
int last_ind = aim.length - 1;
int prev_int = aim[last_ind-1];
int next_int = aim[0];
int lc = aim[last_ind];

preaim[last_ind] = (lc - ((prev_int >>> 5 ^ next_int << 2) + (next_int >>> 3 ^ prev_int << 4) ^ (rnd_s[rnd] ^ next_int) + (keys[ind & 3 ^ rnd_e[rnd]] ^ prev_int)) );

for(ind = aim.length - 1 -1; ind>=0; ind--){
if(ind==0) prev_int = preaim[aim.length-1];
else prev_int = aim[ind-1];
next_int = preaim[ind+1];
preaim[ind] = (aim[ind] - ((prev_int >>> 5 ^ next_int << 2) + (next_int >>> 3 ^ prev_int << 4) ^ (rnd_s[rnd] ^ next_int) + (keys[ind & 3 ^ rnd_e[rnd]] ^ prev_int)) );
}

aim = preaim;
}

// System.out.println("ans : " + Arrays.toString(toByteArray(aim)));
System.out.print("try decrypt: [");
for(byte a:toByteArray(aim, false)){
System.out.print(a + ",");
}
System.out.println("]");
System.out.print("try flag: ");
for(byte a:toByteArray(aim, false)){
System.out.print((char)a);
}
System.out.println("");
return aim;
}

public static void main(String[] argc){

int rounds = 7; // 52 / 44 + 6
byte[] aim_byte=new byte[]{68, 39, -92, 108, -82, -18, 72, -55, 74, -56, 38, 11, 60, 84, 97, -40, 87, 71, 99, -82, 120, 104, 0x2F, -71, -58, -57, 0, 33, 42, 38, -44, -39, -60, 0x71, -2, 92, -75, 0x76, -77, 50, (byte)0x87, 43, 0x20, -106};
int[] aim_int =new int[]{0b1101100101001000010011101000100,0b11001001010010001110111010101110,0b1011001001101100100001001010,0b11011000011000010101010000111100,0b10101110011000110100011101010111,0b10111001001011110110100001111000,0b100001000000001100011111000110,0b11011001110101000010011000101010,0b1011100111111100111000111000100,0b110010101100110111011010110101,0b10010110001000000010101110000111};

byte[] try_byte=new byte[]{86,78,67,84,70,123,57,51,101,101,55,54,56,56,45,102,50,49,54,45,52,50,99,98,45,97,53,99,50,45,49,57,49,102,102,52,101,52,49,50,98,97,125,0};

decrypt( toIntArray(aim_byte, false), toIntArray("H4pPY_VNCTF!!OvO".getBytes(), false) );

int[] ens = new int[11];
ens = encrypt( toIntArray(try_byte, false) , toIntArray("H4pPY_VNCTF!!OvO".getBytes(), false) );
System.out.println("encoded msg length = " + toByteArray(ens, false).length);
}

private static int[] toIntArray(byte[] arg7, boolean arg8) {
int[] result;
int v0 = (arg7.length & 3) == 0 ? arg7.length >>> 2 : (arg7.length >>> 2) + 1;
// System.out.println("IntArray length:" + v0);
if(arg8) {
result = new int[v0 + 1];
result[v0] = arg7.length;
}
else {
result = new int[v0];
}

int i;
for(i = 0; i < arg7.length; ++i) {
int v3 = i >>> 2;
result[v3] |= (arg7[i] & 0xFF) << ((i & 3) << 3);

}

return result;
}

private static byte[] toByteArray(int[] arg5, boolean arg6) {
int n = arg5.length << 2;
if(arg6) {
int m = arg5[arg5.length - 1];
if(m > n) {
return null;
}

n = m;
}

// System.out.println("ByteArray length:" + n);
byte[] result = new byte[n];
int i;
for(i = 0; i < n; ++i) {
result[i] = (byte)(arg5[i >>> 2] >>> ((i & 3) << 3) & 0xFF);
}

return result;
}
}

可惜时不我Die

image-20220212205239215

Welcome to my other publishing channels