XMan2017冬 孔雀翎writeup

周六下午听说有面向萌新的移动安全CTF,本来不想欺负小朋友,最后抱着去装个B的态度去做了一下最难的那个apk,质量不错,出题人还是很良心的。

附上题目链接:https://github.com/LeadroyaL/attachment_repo/tree/master/xman_2017/

整个题目是Java写的,没有用到一丁点native的东西,但处理的还是很好的。主要难点在于静态字符串的混淆和由此引来的反射方法难以追踪,加密方式可以通过观察规律来探索,总体来说设计的很不错。

一开始抱着弄懂每一处细节去做的,所有的字符串都被加密处理过了,但解密的方法是写在代码里的,一般位于static段,也可能是动态运行并且解密的,显然里面加了几个while-true让JEB和jadx都发生了不同程度的抽风,从一个确定的字符串为起点,生成String或String[],前者的话,就按位异或,组成解密后的字符串,循环与无规律的byte[7]进行xor;后者要复杂一点,使用substring来切开不同的部分,再分别与无规律的byte[7]进行xor,第一个substring的长度由const-int来确定,后面的长度是在字符串里紧接着的一个char的值。格式类似于给定int3,去处理这条

1
|"ABC"|\u0006|"ABCDEF"|\u0002|"AA"|

字符串这样的。

还有一种动态解密方式是给定一个Long,只有最高的几个bit有效,所以看起来是杂乱无章的,经过计算将Long换算为index,去查Object[] cache,如果类别是Object,就表示未解密,如果类别是String,表示已经解密可以直接拿去用。这部分使用的是每个String分别去xor一个固定数值的char,有多少个String就会有多少个xor_char。

嗯,写代码解密吧。。。呸,解个毛线,太恶心了不想解密,还是动态上去看吧。懒得写了,而且提取参数的话还会稍微费点时间。

观察一下,每个被静态解密的String最终都会调用到String.intern(),虽然不大明白这个的意义是什么,但搜索smali就方便多了。调试时候下这么几个断点:对于单个String的解密,断各个String.intern();对于String[]的解密,断在对数组赋值的地方;对于反射,断在获取字符串的地方。但是由于smalidea本身bug的存在,只能dump到String和String[],其他地方经常会crash。所以也懒得debug了,看一眼dump出来的数据,有个table,有个AES256_ECB_PKCS5。再浏览一下整体的逻辑,就差反射的部分没有任何信息了,调试的话崩溃太多没法搞。

随便解密几个固定的数值,感觉已经够用了,能猜到是在做什么了。

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
target="!_~&!#+(!_@&!_$*!_+(!_~(!#_(!^~)!_+&!_~((!~(!^~*!^~~!^~~!_~*!^+(!^@(!_+(!^^(!_~*(!~(!_~(!_%*!_~&!_~&!^S(!_~((#~((!~&!^%&!_%&!_+(!^~&!#+((_~(!_~(!_+(!_+;!_+(!^~(!_@&!^~:!^~(!_~(!^~(!_~*!^~;!^+(!_~&!^~(!_~&!^@&!^~(!_~(!^_(!#%(!_~&!^~&!^_&!_+&!__&!_+(!_~(!__~(^~)!_~?"
a_a_b="!(&*~;):+IJK%MFD_WHa@GQRSTNO$bcd^fghPLeijklmnABC#EopXVUqrstuvwxy?"
my_b=["test again", "congratulation! flag is XCTF{"]
str22=[0 = "1000"
1 = "1100"
2 = "AES/ECB/PKCS5Padding"
3 = "1011"
4 = "0111"
5 = "0000"
6 = "0001"
7 = "0100"
8 = "11111111"
9 = "reverse"
10 = "1101"
11 = "0011"
12 = "exception:"
13 = "AES"
14 = "imaeskeyimaeskey"
15 = "0110"
16 = "1010"
17 = "0101"
18 = "1001"
19 = "0010"
20 = "1111"
21 = "1110"]
str_e=[0 = "0000"
1 = "0001"
2 = "0010"
3 = "0011"
4 = "0100"
5 = "0101"
6 = "0110"
7 = "0111"
8 = "1000"
9 = "1001"
10 = "1010"
11 = "1011"
12 = "1100"
13 = "1101"
14 = "1110"
15 = "1111"]

观测到a.a.c.d是最主要的入口,也是由反射调进去的,而且里面看起来经过了多轮加密,大概率是要逆的东西了。上xposed搞一下,hook一下反射时候的各种参数,主要关注控制流程和传参数值、类型。 贴一下hook和log

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
final String PACADGE_NAME = "reverse";
final String TAG = "LOVE_XIXI";


@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
Log.d(TAG, loadPackageParam.packageName);
if (!loadPackageParam.packageName.contains(PACADGE_NAME)) {
return;
}
ClassLoader loader = loadPackageParam.classLoader;
Log.d(TAG, "start hook");


XposedHelpers.findAndHookMethod("a.a.c", loader, "d", String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d(TAG, "======== start run =======");
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Log.d(TAG, "result is " + param.getResult());
Log.d(TAG, "======== after run =======");
}
});

XposedHelpers.findAndHookMethod(Method.class, "invoke", Object.class, Object[].class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d(TAG, "You call " + ((Method) param.thisObject).getName() + "t" + ((Method)param.thisObject).getDeclaringClass().getName());
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
String type = param.getResult().getClass().getName();
Log.d(TAG, "And return type " + type);
if(type.equals("[B")){
byte[] res = (byte[])param.getResult();
String s = "";
for (byte b : res) {
s += "t";
s += (int) b;
}
Log.d(TAG, "And return value " + s);
}else if(type.equals("[C")){
char[] res = (char[])param.getResult();
String s = "";
for (char b : res) {
s += "t";
s += (int) b;
}
Log.d(TAG, "And return value " + s);
}else{
Log.d(TAG, "And return value " + param.getResult());
}
}
});


Log.d(TAG, "end hook");
}

Log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
01-08 23:20:47.639 7398-7398/? D/LOVE_XIXI: You call da.a.c
01-08 23:20:47.639 7398-7398/? D/LOVE_XIXI: ======== start run =======
01-08 23:20:47.640 7398-7398/? D/LOVE_XIXI: You call ba.a.c
01-08 23:20:47.657 7398-7398/? D/LOVE_XIXI: And return type [B
01-08 23:20:47.657 7398-7398/? D/LOVE_XIXI: And return value -2051-85-11778-50-71-79-66-8-3691-74-599635
01-08 23:20:47.658 7398-7398/? D/LOVE_XIXI: You call ba.a.c
01-08 23:20:47.658 7398-7398/? D/LOVE_XIXI: And return type java.lang.String
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: And return value EC33AB8B4ECEB9B1BEF8DC5BB6C56023
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: You call ba.a.c
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: And return type java.lang.String
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: And return value 11101100001100111010101110001011010011101100111010111001101100011011111011111000110111000101101110110110110001010110000000100011
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: You call aa.a.c
01-08 23:20:47.659 7398-7398/? D/LOVE_XIXI: And return type [B
01-08 23:20:47.660 7398-7398/? D/LOVE_XIXI: And return value 31242231111133112112312231113221232151532133112131212123111127132
01-08 23:20:47.660 7398-7398/? D/LOVE_XIXI: You call ba.a.b
01-08 23:20:47.660 7398-7398/? D/LOVE_XIXI: And return type [C
01-08 23:20:47.661 7398-7398/? D/LOVE_XIXI: And return value 3335126384033433833351264033951264033353740339543403395434233954338333512640339537383394126383335434040951265933354340333537403395434033351263833954340339437403395126403394364033354363
01-08 23:20:47.661 7398-7398/? D/LOVE_XIXI: result is !#~&(!+&!#~(!_~(!#%(!_+(!_+*!_+&!#~(!_%&!^~&!#+((_~;!#+(!#%(!_+(!#~&!_+(!^%(!_~(!^$(!#+?
01-08 23:20:47.661 7398-7398/? D/LOVE_XIXI: ======== after run =======
01-08 23:20:47.661 7398-7398/? D/LOVE_XIXI: And return type java.lang.String
01-08 23:20:47.661 7398-7398/? D/LOVE_XIXI: And return value !#~&(!+&!#~(!_~(!#%(!_+(!_+*!_+&!#~(!_%&!^~&!#+((_~;!#+(!#%(!_+(!#~&!_+(!^%(!_~(!^$(!#+?

这样看起来就舒服多了,调用流程很清楚,连续过好好几个函数,再浏览一遍主程序,嗯非常清晰完整。

1
2
3
4
5
a.a.c.b(byte[], byte[]) AES_enc
a.a.c.b(byte[]) toHexString
a.a.c.b(String) toBinaryString
a.a.c.a(String) countZeroOne
a.a.b.b(byte[]) DIYB64

1235步都很好懂,主要是第4步不知道在干嘛,而且那段代码jadx反出来居然是错的。。。mdzz

观察一下,输入是binaryString,输出是int[]数组,而且输入必须要"1"开头,观察发现是连续的1的个数、连续的0的个数。逆起来也不复杂,刚好这个int[]加起来长度为384,是一个很吉利的数字。 之后,按照步骤解密一下,看python就可以了

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
target = "!_~&!#+(!_@&!_$*!_+(!_~(!#_(!^~)!_+&!_~((!~(!^~*!^~~!^~~!_~*!^+(!^@(!_+(!^^(!_~*(!~(!_~(!_%*!_~&!_~&!^S(!_~((#~((!~&!^%&!_%&!_+(!^~&!#+((_~(!_~(!_+(!_+;!_+(!^~(!_@&!^~:!^~(!_~(!^~(!_~*!^~;!^+(!_~&!^~(!_~&!^@&!^~(!_~(!^_(!#%(!_~&!^~&!^_&!_+&!__&!_+(!_~(!__~(^~)!_~?"
regular_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
diy_table = "!(&*~;):+IJK%MFD_WHa@GQRSTNO$bcd^fghPLeijklmnABC#EopXVUqrstuvwxy?"

step1 = ""
for c in target:
idx = diy_table.find(c)
step1 += regular_table[idx]

print step1
print len(step1)

import base64

step2 = base64.b64decode(step1)
print step2.encode('hex')
print len(step2)


def g(b):
if b:
return "1"
else:
return "0"


cur = False
step3 = ""
for count in step2:
if cur:
cur = False
else:
cur = True
step3 += g(cur) * ord(count)
print step3
print len(step3)

step4 = int(step3, 2)
step4 = hex(step4)[2:-1]
step4 = step4.decode('hex')
print step4.encode('hex')
print len(step4)

from Crypto.Cipher import AES

key = "imaeskeyimaeskey"
cipher = AES.new(key, AES.MODE_ECB)
step5 = cipher.decrypt(step4)
print step5
print step5.encode('hex')
# w1nner-w1nner-0kill-ch1cken-d1nner

还是挺不错的,有机会研究一下Java层的防御手段。