0%

2019中国杭州网络安全技能大赛 预选赛 哈夫曼之谜 & EasyCpp Writeup

Reverse EasyCpp

[2019/10/25更新]
首先读入16个数字,然后生成一个斐波那契数组,长度也为16
然后对输入数组进行操作,从第二位开始,都加上第一位的值(transform
动态调试可知accumulate的匿名函数对运算完的数组做反转操作
然后与斐波那契数组比较,相同则输出flag
也就是说输入的第一个数字是不会变的,所以就是987
后面的数字直接脚本跑一下就可以得到结果了

Crypto 哈夫曼之谜 100pt

二叉树是真的还没学,还好找到了大佬的代码
南邮数据结构实验二—二叉树的基本操作及哈夫曼编码译码系统的实现

解题思路

得到一个文本,内容如下

1
2
3
4
5
6
7
8
9
10
11
11000111000001010010010101100110110101111101110101011110111111100001000110010110101111001101110001000110

a:4
d:9
g:1
f:5
l:1
0:7
5:9
{:1
}:1

由题意知, 使用了哈夫曼编码。所以上面的是二进制编码,下面的是词频
使用大佬的程序生成哈夫曼树,再解码得到flag。
因为5和d词频一样,如果flag不对,两者替换一下就行了
(原来这题只要提交flag{}里面的字符,难怪我提交2次都错了 = =)

Misc crackme 300pt

分析

这是一道安卓逆向题,反编译后修改apk主入口为CokeyActivity,进入输入用户名和密码的界面。
(默认进入的界面是GameActivity,该游戏基于js编写,代码加密,对这块不熟,所以直接分析另一个Activity)
分析代码可知用户名需为Conan,密码则需要将明文处理一番后与950519fec04js3mjtyioqbwxoshxvbprsjir1miy536kHp6GbGiTfL6GckiKfcUG0PzjFgijHmikAr-DUm39096000030240相等,生成密文的函数代码为(JEB2提取):

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
public static String a(String arg18, int arg19) {
String v0 = arg18.replace(" ", "");
int v2 = 10;
if(v0.length() >= v2) {
if(v0.length() > 99) {
}
else {
String v1 = "IVMDy5x1A0ali-O|fbnKEHjTUQz86oqdNewCvPGX4utJ3SZ9cLR2gmpkFrh7WsYB|da6202b5FA5fF510702092e729A3afN8";
Random v3 = new Random();
StringBuilder v4 = new StringBuilder();
String v5 = "950519";
int v6 = v0.length();
StringBuilder v8 = new StringBuilder();
v8.append(System.currentTimeMillis());
v8.append("");
int v10 = 2;
String v8_1 = v8.toString().substring(v10, 4);
StringBuilder v9 = new StringBuilder();
v9.append(System.currentTimeMillis());
v9.append("");
String v9_1 = v9.toString().substring(6, 8);
String v13_1 = (((int)(Math.random() * 5))) + "";
int v14 = 0;
String v15 = "";
int v7;
for(v7 = 0; v7 < v6; ++v7) {
int v12 = v0.charAt(v7);
if(47 < v12 && v12 < 58) { //数字
v12 = (v12 - 48 + Integer.parseInt(v8_1) + v7 * v7 % arg19) % v2 + 48;
}

if(64 < v12 && v12 < 91) { //大写字母
v12 = (v12 - 65 + Integer.parseInt(v9_1) + v7 * v7 % arg19) % 26 + 65;
}

if(96 < v12 && v12 < 123) { //小写字母
v12 = (v12 - 97 + Integer.parseInt(v13_1) + v7 * v7 % arg19) % 26 + 97;
}

v15 = v15 + (((char)v12));
}

int v0_1 = v15.length();
v2 = v3.nextInt(v0_1 - 1);
String v3_1 = v15.substring(0, v2);
v0 = v15.substring(v2, v0_1);
v0 = v5 + v0 + v8_1;
for(v3_1 = new BigInteger(1, v3_1.getBytes("UTF-8")).toString(v10); v3_1.length() % 8 != 0; v3_1 = "0" + v3_1) {
}

v5_1 = new StringBuilder();
v5_1.append(v6 + arg19);
v5_1.append(String.format("%06d", Integer.valueOf(v2)));
StringBuilder v2_1 = new StringBuilder();
v2_1.append(v6);
v2_1.append("");
v5_1.append(v2_1.toString().length());
String v2_2 = v5_1.toString();
int v5_2;
for(v5_2 = 0; v3_1.length() % 24 != 0; ++v5_2) {
v3_1 = v3_1 + "0";
}

while(v14 <= v3_1.length() - 6) {
v6 = v14 + 6;
int v8_2 = Integer.parseInt(v3_1.substring(v14, v6), v10);
if(v8_2 != 0 || v14 < v3_1.length() - v5_2) {
v4.append(v1.charAt(v8_2));
}
else {
v4.append("*");
}

v14 = v6;
}

v1 = v4.toString();
return v0 + v1 + v9_1 + v13_1 + v2_2 + v1.length();
}
}

return "";
}

分析代码逻辑

可以看到其中有取时间戳,随机数生成,所以我们需要把这些固定下来。

分析Java代码可知密文结构如下:

1
2
950519 fec04js3mjtyioqbwxoshxvbprsjir1miy 53 6kHp6GbGiTfL6GckiKfcUG0PzjFgijHmikAr-DUm 39 0 96 000030 2 40
固定前缀 sec1 时间戳取2-4位 sec2 时间戳取6-8位 5以内随机数 明文长度+32 明文位数-1内取随机数再补0至6位 固定数值(明文长度的长度) sec2长度

sec1: 30-64位替换结果
sec2: 0-30位替换后再处理结果
分析完密文结构可知,明文长度为64位(96-32)
sec2生成具体流程为:
将密文进行逐字替换(所处位置不同,替换结果也不同)。
然后取前30位,逐位转化为二进制,除第一项外,其余项目前面补0至八位,再末尾补0至24的倍数
每六位作为二进制数,转化为十进制,读取指定字符串的该位置字符,再拼接成字符串
所以我们需要逆向求解得到替换的结果(中间密文),再爆破解得原始明文

抽取替换代码为函数

为爆破做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static char getRet (int x, int index ){

if(47 < x && x < 58) {//数字
x = (x - 48 + 53 + index * index % 32) % 10 + 48;
}

if(64 < x && x < 91) {//大写字母
x = (x - 65 + 39 + index * index % 32) % 26 + 65;
}

if(96 < x && x < 123) {//小写字母
x = (x - 97 + 0 + index * index % 32) % 26 + 97;
}

return (((char)x));
}

解题方法

首先我们需要求到前30位,写个python脚本就可

1
2
3
4
5
6
7
8
9
f = "6kHp6GbGiTfL6GckiKfcUG0PzjFgijHmikAr-DUm";
v1 = "IVMDy5x1A0ali-O|fbnKEHjTUQz86oqdNewCvPGX4utJ3SZ9cLR2gmpkFrh7WsYB|da6202b5FA5fF510702092e729A3afN8";
s=""
for i in range(len(f)):
tmp = f[i]
loc = v1.index(tmp)
s += str(bin(loc)).replace("0b","").rjust(6,'0')
print(s)

得到011100110111010101110110011100100110010001100110001100010111010000110001011100100110110000110111001100010011010000110000011000100110001001100101011010010110111000110100001100010110010101110101001100110111001000111001001101000011011000110101
然后把它转成十进制 (去掉了开头的0)

1
2
3
4
5
6
7
ret = ""
s = "11100110111010101110110011100100110010001100110001100010111010000110001011100100110110000110111001100010011010000110000011000100110001001100101011010010110111000110100001100010110010101110101001100110111001000111001001101000011011000110101"
for i in range(30):
ttt = s[8*i:8*(i+1)-1]
#print(ttt)
ret += chr(int(ttt,2))
print(ret)

得到suvrdf1t1rl7140bbein41eu3r9465
这就是中间密文得前30位,而后34位则是fec04js3mjtyioqbwxoshxvbprsjir1miy,拼接起来爆破即可(Java)

1
2
3
4
5
6
7
8
9
10
String interStr = "suvrdf1t1rl7140bbein41eu3r9465fec04js3mjtyioqbwxoshxvbprsjir1miy";
StringBuilder sb = new StringBuilder();
for(int i = 0;i<interStr.length();i++) {
char ta = interStr.charAt(i);
for(int j = 48;j<123;j++) {
if(getRet(j,i) == ta)
sb.append((char)(j));
}
}
Log.d("Xhy",sb.toString());

得到string4c8ah9223abdee53ad0a2673bdc67ac5isthepasswordofclasses2dex
所以这只是第一步,assets\vendor\multidex下有个拓展名为dex的zip文件,使用4c8ah9223abdee53ad0a2673bdc67ac5解压。
里面有个Cokey.smali文件,调用了native方法qwert

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
.field private qwert_param1:Ljava/lang/String;

.field private qwert_param2:Ljava/lang/String;

.method static constructor <clinit>()V
.locals 1

.line 14
const-string v0, "cokey"

invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

.line 15
return-void
.end method

.method public constructor <init>()V
.locals 1

.line 8
invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V

.line 10
const-string v0, "from CokeyActivity"

iput-object v0, p0, Lxyz/sysorem/cokey/Cokey;->qwert_param1:Ljava/lang/String;

.line 11
const-string v0, "from GameActivity"

iput-object v0, p0, Lxyz/sysorem/cokey/Cokey;->qwert_param2:Ljava/lang/String;

return-void
.end method

.method public onCreate(Landroid/os/Bundle;Landroid/os/PersistableBundle;)V
.locals 0
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.annotation build Landroid/support/annotation/Nullable;
.end annotation
.end param
.param p2, "persistentState" # Landroid/os/PersistableBundle;
.annotation build Landroid/support/annotation/Nullable;
.end annotation
.end param

.line 20
invoke-super {p0, p1, p2}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;Landroid/os/PersistableBundle;)V

.line 21
return-void
.end method

.method public native qwert(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.end method

所以新建一个工程,包名跟原程序一样,修改MainActivity为CokeyActivity,代码如下:

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
package xyz.sysorem.cokey;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

public class CokeyActivity extends AppCompatActivity {

static String parm1="from CokeyActivity";
static String parm2="from GameActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String s = qwert(parm1,parm2);
Toast.makeText(this,s,Toast.LENGTH_SHORT).show();
}

static {
System.loadLibrary("cokey");
}
public native String qwert(String s1,String s2);

}

运行试试,返回真実はいつもひとつ !(真相只有一个),果然没那么简单。
ida打开libcokey.so,在JNI_OnLoad注册了qwert方法,指向ready,打开该方法,有获取签名和从证书中读取公钥的操作。并存在对时间戳和随机数的调用,所以需要通过修改跳转来修改程序执行流程。
然后根据一系列操作生成aes_key进行AES解密操作(eee函数),再调用aaa, bbb函数进行转换,目测是转化成16进制字符串
所以关键就在于,如何得到正确的密文和aeskey来进行后续的解密操作
注意到ready函数中有对mystery进行赋值的操作,所以推测mystery即为密文
时间所限,剩下的以后再看吧(逃