balsn-ctf-2023

Played with Friendly Maltese Citizens and came first in this ctf. I just spent some time solve merger-2077 since last weekend were work days for me. Learning game hacks from scratch in such a short time is challenging. Glad that we managed to solve it.

Scoreboard

Untitled

Challenge Info

Untitled

First Look

Decompress the apk file and take a first look at the structure.

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
merger-2077
├── AndroidManifest.xml
├── assets
│   ├── bin
│   │   └── Data
│   │   ├── boot.config
│   │   ├── data.unity3d
│   │   ├── Managed
│   │   │   ├── Metadata
│   │   │   │   └── ばかみたい
│   │   │   └── Resources
│   │   │   └── mscorlib.dll-resources.dat
│   │   ├── RuntimeInitializeOnLoads.json
│   │   ├── ScriptingAssemblies.json
│   │   ├── unity_app_guid
│   │   └── unity default resources
│   └── UnityServicesProjectConfiguration.json
├── classes.dex
├── lib
│   ├── arm64-v8a
│   │   ├── lib_burst_generated.so
│   │   ├── libil2cpp.so
│   │   ├── libmain.so
│   │   └── libunity.so
│   └── armeabi-v7a
│   ├── lib_burst_generated.so
│   ├── libil2cpp.so
│   ├── libmain.so
│   └── libunity.so
├── META-INF
│   ├── CERT.RSA
│   ├── CERT.SF
│   └── MANIFEST.MF
├── res
│   ├── mipmap-hdpi-v4
│   │   ├── app_icon.png
│   │   └── app_icon_round.png
│   ├── mipmap-ldpi-v4
│   │   ├── app_icon.png
│   │   └── app_icon_round.png
│   ├── mipmap-mdpi-v4
│   │   ├── app_icon.png
│   │   └── app_icon_round.png
│   ├── mipmap-xhdpi-v4
│   │   ├── app_icon.png
│   │   └── app_icon_round.png
│   ├── mipmap-xxhdpi-v4
│   │   ├── app_icon.png
│   │   └── app_icon_round.png
│   └── mipmap-xxxhdpi-v4
│   ├── app_icon.png
│   └── app_icon_round.png
└── resources.arsc

18 directories, 35 files

It’s a unity game and il2cpp reverse, with a japanese metadata file name.

A Failed Try

A common way to solve an il2cpp reverse challenge is find where it load (and decrypt) the metadata.

Since the name is in japanese and hard to identify in IDA, an easy way is to locate ERROR: Could not open %s . The function is long so i will put the code there instead of a pic.

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
v46 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
*(_OWORD *)s = xmmword_10E00A7;
result = sub_4A9420(s, (int)a2, a3, a4, a5, a6, a7, a8, (int)v33, v34, v35, v37, ptr, v39, v41, v42);
qword_1646A88 = result;
if ( result )
{
v11 = blowfish_init((int)&unk_163FC04, &unk_1645A40);
sub_431938(v11);
v33 = "Metadata";
v34 = 8LL;
v12 = (unsigned __int64)v35 >> 1;
if ( (v35 & 1) != 0 )
v13 = ptr;
else
v13 = v36;
if ( (v35 & 1) != 0 )
v12 = v37;
v43 = v13;
v44 = v12;
sub_4332F8(&v43, &v33);
if ( (v35 & 1) != 0 )
operator delete(ptr);
v14 = strlen(s);
if ( (v39 & 1) != 0 )
v15 = v42;
else
v15 = v40;
if ( (v39 & 1) != 0 )
v16 = v41;
else
v16 = (unsigned __int64)v39 >> 1;
v33 = s;
v34 = v14;
v43 = v15;
v44 = v16;
sub_4332F8(&v43, &v33);
if ( (v35 & 1) != 0 )
v17 = (const char *)ptr;
else
v17 = v36;
v18 = fopen(v17, "r");
fseek(v18, 0LL, 2);
v19 = ftell(v18);
fclose(v18);
v20 = malloc(v19);
v21 = qword_1646A88;
v22 = v20;
memcpy(v20, (const void *)qword_1646A88, v19);
blowfish_decrypt(v22, v19, v21);

It is easy to identify the decrypt function as blowfish since the constants are not modified. After a carefully analysis, we confirm that it is the blowfish. So we can easily decrypt the metadata and use il2cppdumper to fetch the function and struct information. But things didn’t go as expected. The decrypted metadata has obfuscated header.

Untitled

The Sanity and version do not appear at the right positions and we fail to recover the header since we got conflicts. So the static way failed.

Final Solution

Use the magisk version of il2cppdumper to dump dynamically. Search dump.cs for Assembly-CSharp.dll and find something interesting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SuperSecretValueStorage : MonoBehaviour
{
// Fields
private const Int32 TotalSz; // 0x0
private Byte[] superSecretBytes; // 0x18
private ICryptoTransform _enc; // 0x20
private ICryptoTransform _dec; // 0x28

// Properties

// Methods
// RVA: 0x550cc4 VA: 0x7ba5062cc4
private Dictionary`2 DeserializeMapping() { }
// RVA: 0x550f1c VA: 0x7ba5062f1c
private Byte[] SerializeMapping(Dictionary`2 mapping) { }
// RVA: 0x54efcc VA: 0x7ba5060fcc
public Object ReadSecretValue(String val) { }
// RVA: 0x54f1b4 VA: 0x7ba50611b4
public Void WriteSecretValue(String value, Object o) { }
// RVA: 0x5510a0 VA: 0x7ba50630a0
private Void Start() { }
// RVA: 0x551298 VA: 0x7ba5063298
public Void .ctor() { }
}

The class seems to be used to store something with encrypt, decrypt, read and write methods. After hooking and fetching the args of ReadSecretValue and WriteSecretValue with frida, I find that, everytime the score updates, the ReadSecretValue and WriteSecretValue are called with string score as argument, to look up an encrypted dictionary.

After some reverse, I found where the dictionary is decrypted and the length of it.

Untitled

So, I use frida to hook after the decryption and dump the memory of the dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function writeFile(content: ArrayBuffer) {
var file = new File(
"/data/user/0/com.DefaultCompany.balsnctf2023/files/decrypt.txt",
"wb"
);
file.write(content);
file.flush();
file.close();
}

function hook_start() {
var addr = Module.getBaseAddress("libil2cpp.so");
var key = addr.add("0x54F10C");
Interceptor.attach(key, {
onEnter: function (args) {
console.log("start hook");
var x0 = this.context.x0;
console.log("x0 is ", x0.readByteArray(0x100));
writeFile(x0.readByteArray(0x10010));
},
});
}

hook_start();

Just search for BALSN to find the flag.

hack.lu-wp seccon-quals-2023

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×