ahe17 (android hacker event 2017) writeup

一开始是flanker在微博上转发的,说是面向android hacker的ctf,那我肯定得参加一下,结果下来ak了,题目都不是很难,这国际版的ctf也是比较水,不知道主办方是什么心态。。。

转载请联系本人,否则作侵权处理。 相关文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/ahe17

比赛前放出三道题,说是做出来的local hacker可以领取T恤,都是送分题,没写writeup,就记住三个flag。

1
2
3
4
5
6
ahe17 cat easy/flag
9083
ahe17 cat intermediate/flag
NATIVEAHE
ahe17 cat trivial/flag
AndroidHackingIsFun

1、AES-Decrypt

It's right in front of you, just decrypt it!

没有任何输入,点击后在JNI里对某个数据进行解密,返回的string为“Key=Af03BC291F82”,用来迷惑的,哪有这种key啊,屏幕上又说“AES-256-CBC”,请我们解密另一段data。 JNI里符号表都在,使用手写的方式调用libssl和libcrypto。 直接找到Java_MainActivity_decryptAES,上面进行了一大堆操作,后面对操作结果进行了memcpy以及使用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
int __fastcall Java_challenge_teamsik_aesdecryption_MainActivity_decryptAES(JNIEnv *env, int a2, int a3)
{
v72 = v71;
if ( v71 <= -1 )
v71 = -1;
v73 = (char *)operator new[](v71);
((void (__fastcall *)(JNIEnv *, int, _DWORD, signed int, char *))(*env)->GetByteArrayRegion)(env, v70, 0, v72, v73);
v74 = (char *)malloc(0x20u);
_aeabi_memcpy(v74, v73, 32);
do
_android_log_print(3, "CHALLENGE", "0x%2x", (unsigned __int8)v74[v30++]);
while ( v30 != 32 );
//出题人这里为我们打印出了32个字符,经过大量计算得到
s = 'wvN+';
v84 = '84Zs';
v85 = 'yv3j';
v86 = 'MaID';
v87 = 'rL6u';
v88 = 'nNnj';
v89 = 'AO/8';
v90 = 'Gxen';
v91 = '3nXU';
v92 = 'aeOP';
v93 = '=8Iv';
v94 = aNvwsz48j3vydia[44];
v75 = strlen((const char *)&s);
v76 = sub_39E4(&s, v75, &v81);
v77 = sub_3A58(v76, v81, v74, (const char *)&unk_FD56, v82);
if ( v77 == -1 )
{
v79 = (*env)->NewStringUTF;
v78 = "Invalid key used!";
}
else
{
v82[v77] = 0;
v78 = v82;
v79 = (*env)->NewStringUTF;
}
return ((int (__fastcall *)(JNIEnv *, char *))v79)(env, v78);
}

在sub_39e4和sub_3a58中,分别进行了b64解码和AES解密,通过API的名称可以猜到。 再猜一下长度,根据API名字是aes_256_cbc,找到unk_FD56位置的IV,从Log里找到key,解密后把string返回到JNI里,所以2个参数找全,开始解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iv = [0x99,0x3F,0x76,6,0xA7,0x88,0x1C,0x67,0x49,0x27,0x66,6,0x8D,0xE9,0xD8,0xAA]
key = [0x0,0xc2,0xb2,0x0,0xcb,0xaf,0xed,0x4e,0x14,0xb3,0x52,0x93,0x56,0x36,0xf6,0x0,0x65,0x7c,0x40,0x6c,0xbb,0x8f,0x29,0xc0,0xa1,0x4f,0xbb,0xec,0xb6,0xad,0x8e,0x54]
enc_data = "+NvwsZ48j3vyDIaMu6LrjnNn8/OAnexGUXn3POeavI8="
enc_flag = "T9WoXhrsQHgY3NLr8SwBbw=="

iv = ''.join(chr(a) for a in iv)
key = ''.join(chr(a) for a in key)

cipher = AES.new(key, AES.MODE_CBC, iv)
plain_data = cipher.decrypt(base64.b64decode(enc_data))
# 'Key=AF03BC291F82\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
cipher = AES.new(key, AES.MODE_CBC, iv)
plain_flag = cipher.decrypt(base64.b64decode(enc_flag))
# 'AHE17{Frida!}\x03\x03\x03'

2、Token-Generator(doNetChallenge) 这题还是比较难搞的,用的是doNet,使用mono技术,Java层是一个外壳,没太大的用处。上两款软件,ILSpy和dnSpy一起搞一下,打开时候巨卡无比。相比JEB这两款软件简直low爆了。。。 一般核心的dll放在assets里,找名字最特殊的那个。 硬着头皮从onCreate开始看,每次onCreate时候创建一个AES-256,但马上又clear掉,删掉所有的临时文件,随机挑选一个X开头的方法去调用,每个X开头的方法在结束后再去随机调用另一个X开头的方法,这样一共递归调用10次。 X开头的方法有很多个,主要功能有几类:

  1. 解密、创建文件,文件名为foo+uuid(唯一而且随机)和bar+uuid(唯一且随机)
  2. check,拿到目录下所有foo文件,并且依次进行SHA1,将结果作为AES的key,拿到目录下所有的bar文件,并且依次进行SHA1,将结果作为AES的IV,对给定的String进行解密
  3. (不太记得了)

总之,就是瞎jb调用、瞎jb解密,总有一次会解密一大堆文件,总有一次可能解密出了flag。思路大概就是遍历所有的可能,碰运气可能将结果解密出来。 我们拖出所有的foo+uuid和bar+uuid文件,对其进行sha1的操作,共有6个sha1。

1
2
3
4
5
6
7
8
9
sha-foo
d81971165e45b516046682ee542b01fc466f64d7
4b14e05adacc8d763f740e742a4db43e435e34b4 AHE17-d0tn€t-c0de
bef7ceb95a7aad298ba23c49f5205e3f592db3de

sha-bar
8f2d21526780e83bb51ea8877d253fd6fe95d8e2
ca73e884a2f7ead5c337156dc3073804b6d5987f
36ca6e181a1bd20bf54ad9427eecb6a5ef35627d

观察API,搜索C#的一系列知识,发现不是AES,而是Rijdael的算法,使用Visual Studio的C#去调用一下API,最多9次就好了,给定20byte的key,给定20byte的IV,给定密文

1
enc = [17, 185, 186,161,188,43,253,224,76,24,133,9,201,173,255,152,113,171,225,163,121,177,211,18,50,50,219,190,168,138,97,197]

这里我就不写C#的代码了,反正是抄来的,不会写的话抄这个题的源码,最后某次解密成功,得到flag为

1
AHE17-d0tn€t-c0de

然而那个符号中国键盘打印不出来,复制粘贴应该可以。 这题真是神坑,不认识C#,带有大量随机性。。。还好没出啥大事


3、Flag_Validator

4个小check,分别在校验4部分;1个大check,校验整个字符串的hash值。

第一个check: 输入的字符串逆序后base64编码为"cHVtcjRX"

1
2
3
from base64 import b64decode
print b64decode("cHVtcjRX")[::-1]
# W4rmup

第二个check: 将给定的int[]进行一系列运算,再将int转化为char再转化为string,与输入做比较,写一段Java模拟一遍即可。

1
ch4ll3ng3

第三个check: 使用反射去调用第二个check时候的那个方法,给定的int[]不一样,结果也不一样,同样用Java模拟一遍。

1
5UcC33D3d

第四个check: 将输入string的md5值与某个值进行比较,再cmd5上花钱买掉即可。

1
continue1

最后的check是连带-,组成W4rmup-ch4ll3ng3-5UcC33D3d-continue1,进行SHA1,与预期做对比。 结果是

1
W4rmup-ch4ll3ng3-5UcC33D3d-continue1

4、You Can Hide - But You Cannot Run

一个比较骚的题,几十个线程同时启动,对某文件进行写入操作,每个线程只写1byte,而且写完就退出。如果我们观察这个文件的话,每一秒这个文件的里存放的char是不一样的。

这时我们猜测,将输出的字符按照时间顺序连起来,应该就是flag。

观察一下这一堆线程,只有int sleepTillTimechar c是不一样的,所有思路就是对smali文件进行正则,拿到time和char的对应关系。 这里使用这段python来提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
patter = re.compile(
r"const-wide/.*? v0, (0x.*?)iput-wide.*?Lhackchallenge/ahe17/teamsik/org/romanempire/threads/.*>sleepTillTime.*const/16 v0, (0x.*?)iput-char v0, p0.*->c:C",
flags=re.DOTALL)
di_path = "/Users/leadroyal/Tools/android_tools/workspace/YouCanHideButYouCannotRun/smali/hackchallenge/ahe17/teamsik/org/romanempire/threads"
for root, dirs, files in os.walk(di_path):
for name in files:
full_path = os.path.join(root, name)
fd = open(full_path, 'rb')
data = fd.read()
fd.close()
match_res = patter.findall(data)
t_time = int(match_res[0][0].strip(' \n'), 16)
t_char = int(match_res[0][1].strip(' \n'), 16)
# print t_time, ",",
# print t_char, ",",

之后进行简单的排序即可,输出是

1
Aol jsvjrdvyr ohz ybzalk Puav h zapmm tvklyu hya zahabl, Whpualk if uhabyl, svhaolk if aol Thzzlz, huk svclk if aol mld. Aol nlhyz zjylht pu h mhpslk ylcpchs: HOL17{IlaalyJyfwaZ4m3vyKpl}

显然是单表替换之类的,扔到quipquip里解一下,得到flag

1
The clockwork has rusted Into a stiff modern art statue, Painted by nature, loathed by the Masses, and loved by the few. The ?ears scream in a failed revival: AHE17{BetterCryptS4f3orDie}

AHE17{BetterCryptS4f3orDie}


5、Why Should I Pay? 简单的注册机,有一个在线购买,但是URL坏掉了,购买成功后会在 SharedPreferences里写一些数据。 在Java里采集了MAC地址。 在MainActivity里有一处JNI,传入了MAC地址和Key,于是打开so文件。 mdzz,在so里直接看到flag。。。

AHE17{pr3mium4ctiv4ted}


剩下两道没做的,一个是web题,服务器贼卡,写脚本跑完了也不知道要干嘛;还有个brainfuck,没有target,不知道在干嘛。

总体来说,ahe17的出题真是水。。。我还以为会有我做不出来的,失望了