@@ -17,7 +17,7 @@ Solved: 81
17
17
18
18
Input files:
19
19
20
- ??? info "encoding.txt"
20
+ * [ rusty_vault binary ] ( https://github.com/DownUnderCTF/Challenges_2024_Public/tree/f2797a33d8f5851508f37e854afceedf85eee8a3/rev/rusty-vault/publish )
21
21
22
22
23
23
NB:
37
37
38
38
## My struggle
39
39
40
+ ### Analysis
41
+
42
+ Running the binary doesn't give us much:
43
+ ``` bash
44
+ $ ./rusty_vault
45
+ Enter the password to unlock the vault:
46
+ ```
47
+
48
+ Its time to open [ Ghidra] ( https://ghidra-sre.org/ ) . The ` main ` function is a thin wrapper that initializes standard rust runtime and then
49
+ calls ` _ZN11rusty_vault4main17h33c04fad0008f474E ` this is where the magic happens.
50
+
51
+ The function has very typical structure for security challenge. It consists of 3 key parts:
52
+
53
+ 1 . Initialization. Usually includes many constants for key, cypher setup;
54
+ 2 . Key mutation. This section can be recognised by many complicated loops/jmps/branches or cipher;
55
+ 3 . Verification. This section typically has string/byte array comparison and two branches: success and failure.
56
+
57
+ Lets review each section:
58
+
59
+ #### Initialization
60
+ ``` c title="_ZN11rusty_vault4main17h33c04fad0008f474E()"
61
+ # this are contastants to initialise cipher state
62
+ # from the first look
63
+ # it is at least 0xe x 4 byte integers which gives us 15x4 = 60 bytes
64
+ *__s1 = 0x3256a6fa ;
65
+ __s1[1 ] = 0xcd3071c3 ;
66
+ __s1[2 ] = 0xf161629 ;
67
+ __s1[3 ] = 0x65e74f39 ;
68
+ __s1[4 ] = 0xdb05fa2e ;
69
+ __s1[5 ] = 0x1247eacc ;
70
+ __s1[6 ] = 0xed7ff4c8 ;
71
+ __s1[7 ] = 0xadf63090 ;
72
+ __s1[8 ] = 0xa750b1ab ;
73
+ __s1[9 ] = 0xd1b5cfa2 ;
74
+ __s1[10 ] = 0x9ab32e3b ;
75
+ __s1[0xb ] = 0x8ea036fe ;
76
+ *(undefined8 *)(__s1 + 0xc ) = 0x6179cbe7049f1890 ;
77
+ __s1[0xe ] = 0x385bd95c ;
78
+ if (aes::autodetect::aes_intrinsics::STORAGE == -1 ) { # some AES initialization
79
+ aes::autodetect::aes_intrinsics::init_get::cpuid (&local_9b8,1);
80
+ aes::autodetect::aes_intrinsics::init_get::cpuid_count(&local_d78,7,0);
81
+ if ((~ (uint)local_9b0 & 0xc000000) == 0) {
82
+ uVar9 = core::core_arch::x86::xsave::_ xgetbv();
83
+ uVar9 = (uint)local_9b0 >> 0x19 & (uVar9 & 2) >> 1;
84
+ aes::autodetect::aes_intrinsics::STORAGE = (char)uVar9;
85
+ if (uVar9 != 0) goto LAB_00108dc3;
86
+ }
87
+ else {
88
+ aes::autodetect::aes_intrinsics::STORAGE = '\0';
89
+ }
90
+ }
91
+ else if (aes::autodetect::aes_intrinsics::STORAGE == '\x01') {
92
+ LAB_00108dc3:
93
+ # method annotated by Ghidra _ <aes::ni::Aes256Enc as crypto_common::KeyInit>::new
94
+ _ <>::new(&local_d78,&DAT_0014a074);
95
+ aes::ni::aes256::inv_expanded_keys(local_508,&local_d78);
96
+ memcpy(local_5f8,&local_d78,0xf0);
97
+ memcpy(&local_d78,local_5f8,0x1e0);
98
+ goto LAB_00108e2e;
99
+ }
100
+ aes::soft::fixslice::aes256_key_schedule (&local_d78,&DAT_0014a074);
101
+ LAB_00108e2e:
102
+ memcpy (&local_9b8,&local_d78,0x3c0);
103
+ # method annotated by Ghidra _ <aes_gcm::AesGcm<Aes,NonceSize,TagSize> as core::convert::From<Aes >>::from
104
+ _ <>::from(local_418,&local_9b8);
105
+ local_9b8 = 0;
106
+ local_9b0 = &DAT_00000001;
107
+ local_9a8 = 0;
108
+ local_d78 = &PTR_s_Enter_the_password_to_unlock_the_0015a118; # prompt for password
109
+ local_d70 = 1;
110
+ local_d68 = 8;
111
+ local_d60 = ZEXT816(0);
112
+ std::io::stdio::_print (&local_d78);
113
+ local_d78 = (undefined ** )std::io::stdio::stdin();
114
+ auVar12 = std::io::stdio::Stdin::read_line(&local_d78,&local_9b8); # read line into variable auVar12
115
+ ```
116
+
117
+ So we can see a large array initialized. After that AES setup. Then program prompts the password and stores
118
+ it in `auVar12`. Key initialization also gives away AES key size - 256 bits (based on calls `aes::soft::fixslice::aes256_key_schedule` and `aes::ni::aes256::inv_expanded_keys`).
119
+
120
+ From this section important information we are looking for:
121
+
122
+ 1. What algorithm is used;
123
+ 2. How its initialized.
124
+
125
+ Annotation `aes_gcm::AesGcm<Aes,NonceSize,TagSize>` tells us its AES 256 GCM, we can now find documentation and all important
126
+ params and calls: https://docs.rs/aes-gcm/latest/aes_gcm/.
127
+
128
+ ```rust hl_lines="21-23" title="documentation sample"
129
+ use aes_gcm::{
130
+ aead::{Aead, AeadCore, KeyInit, OsRng},
131
+ Aes256Gcm, Nonce, Key // Or `Aes128Gcm`
132
+ };
133
+
134
+ // The encryption key can be generated randomly:
135
+ let key = Aes256Gcm::generate_key(OsRng);
136
+
137
+ // Transformed from a byte array:
138
+ let key: &[u8; 32] = &[42; 32];
139
+ let key: &Key<Aes256Gcm> = key.into();
140
+
141
+ // Note that you can get byte array from slice using the `TryInto` trait:
142
+ let key: &[u8] = &[42; 32];
143
+ let key: [u8; 32] = key.try_into()?;
144
+
145
+ // Alternatively, the key can be transformed directly from a byte slice
146
+ // (panicks on length mismatch):
147
+ let key = Key::<Aes256Gcm>::from_slice(key);
148
+
149
+ let cipher = Aes256Gcm::new(&key);
150
+ let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // 96-bits; unique per message
151
+ let ciphertext = cipher.encrypt(&nonce, b"plaintext message".as_ref())?;
152
+ let plaintext = cipher.decrypt(&nonce, ciphertext.as_ref())?;
153
+ assert_eq!(&plaintext, b"plaintext message");
154
+ ```
155
+
156
+ Key points:
157
+
158
+ * ` Aes256::new(&key) ` takes address of key. In our program there is call ` _<>::new(&local_d78,&DAT_0014a074); ` So ` DAT_0014a074 ` could be the key.
159
+ * ` cipher.encrypt() ` takes nonce (according to docs 12 bytes) and plaintext.
160
+
161
+ #### Key mutation
162
+
163
+ Now it time to what is happening to key and password.
164
+
165
+ ??? info "_ ZN11rusty_vault4main17h33c04fad0008f474E()"
166
+ ```c hl_lines="80"
167
+ if (auVar12._ 0_8_ == 0) {
168
+ __ dest = &DAT_00000001;
169
+ if (local_9a8 != 0) {
170
+ puVar5 = local_9b0 + local_9a8;
171
+ do {
172
+ bVar7 = puVar5[ -1] ;
173
+ uVar8 = (ulong)bVar7;
174
+ if ((char)bVar7 < '\0') {
175
+ bVar1 = puVar5[ -2] ;
176
+ if ((char)bVar1 < -0x40) {
177
+ bVar2 = puVar5[ -3] ;
178
+ if ((char)bVar2 < -0x40) {
179
+ puVar6 = puVar5 + -4;
180
+ uVar9 = bVar2 & 0x3f | ((byte)puVar5[ -4] & 7) << 6;
181
+ }
182
+ else {
183
+ puVar6 = puVar5 + -3;
184
+ uVar9 = bVar2 & 0xf;
185
+ }
186
+ uVar9 = bVar1 & 0x3f | uVar9 << 6;
187
+ }
188
+ else {
189
+ puVar6 = puVar5 + -2;
190
+ uVar9 = bVar1 & 0x1f;
191
+ }
192
+ uVar9 = bVar7 & 0x3f | uVar9 << 6;
193
+ uVar8 = (ulong)uVar9;
194
+ if (uVar9 == 0x110000) break;
195
+ }
196
+ else {
197
+ puVar6 = puVar5 + -1;
198
+ }
199
+ uVar9 = (uint)uVar8;
200
+ if ((4 < uVar9 - 9) && (uVar9 != 0x20)) {
201
+ if (0x7f < uVar9) {
202
+ uVar3 = (uint)(uVar8 >> 8);
203
+ if (uVar3 < 0x20) {
204
+ if ((uVar8 & 0xffffff00) == 0) {
205
+ bVar7 = core::unicode::unicode_data::white_space::WHITESPACE_MAP[ uVar8 & 0xff] ;
206
+ LAB_00108f32:
207
+ bVar11 = (bool)(bVar7 & 1);
208
+ }
209
+ else {
210
+ if (uVar3 != 0x16) goto LAB_00109025;
211
+ bVar11 = uVar9 == 0x1680;
212
+ }
213
+ LAB_00108f35:
214
+ if (bVar11 != false) goto LAB_00108f40;
215
+ }
216
+ else {
217
+ if (uVar3 == 0x20) {
218
+ bVar7 = (byte)core::unicode::unicode_data::white_space::WHITESPACE_MAP[ uVar8 & 0xff]
219
+ >> 1;
220
+ goto LAB_00108f32;
221
+ }
222
+ if (uVar3 == 0x30) {
223
+ bVar11 = uVar9 == 0x3000;
224
+ goto LAB_00108f35;
225
+ }
226
+ }
227
+ }
228
+ LAB_00109025:
229
+ __ n = (long)puVar5 - (long)local_9b0;
230
+ if (__ n != 0) {
231
+ if ((long)__ n < 0) {
232
+ uVar10 = 0;
233
+ }
234
+ else {
235
+ uVar10 = 1;
236
+ __ dest = (undefined * )__ rust_alloc(__ n,1);
237
+ if (__ dest != (undefined * )0x0) goto LAB_00109060;
238
+ }
239
+ alloc::raw_vec::handle_error(uVar10,__ n);
240
+ goto LAB_0010922a;
241
+ }
242
+ break;
243
+ }
244
+ LAB_00108f40:
245
+ puVar5 = puVar6;
246
+ } while (puVar6 != local_9b0);
247
+ }
248
+ __ n = 0;
249
+ memcpy(__ dest,__ src,__ n);
250
+ local_9b8 = __ n;
251
+ local_9b0 = __ dest;
252
+ local_9a8 = __ n;
253
+ _ <>::encrypt(&local_d90,local_418,&DAT_0014a068,__ dest,__ n); # call AES encrypt
254
+ ```
255
+
256
+ It has a lot of going on. The only thing I can tell from initial look thought it there is ` while ` loop and a lot of branches
257
+ on each iteration. It would take a quite some time to get my head around what is going on here. Probably want to skip this
258
+ part for now to safe time in case its not really needed. After the crazy loop, AES ` encrypt() ` is called.
259
+
260
+ Earlier we saw that encrypt is supposed to take 2 params: nonce and plain text to encrypt. Here we can see 5 params. I can guess
261
+ that first one is ` self ` (aka this), and rest of params could be because we invoke some overloaded/internal method. I decided to
262
+ run program with gdb debugger to set a breakpoint here and see what this params are.
263
+
264
+ Instruction that I want to set breakpoint at is at address 0x001090be in Ghidra (we can't set breakpoint at address 0x001090be because
265
+ binary has PIE enabled and therefore every launch loaded to different address). Function ` _ZN11rusty_vault4main17h33c04fad0008f474E ` starts at 0x00108cf0, so
266
+ its ` 0x00108cf0 - 0x001090be = 974 ` bytes into the function. Therefore gdb command is ` br *(_ZN11rusty_vault4main17h33c04fad0008f474E+974) ` .
267
+
268
+ Here I can see params of the call:
269
+ 4th is password that we entered (probably ` plain_text ` ) and before that is pointer to nonce which we can read from memory:
270
+
271
+ ``` bash
272
+ (gdb) x/12bx 0x55555559e068 # read 12 bytes in hex (we know length from docs)
273
+ 0x55555559e068: 0xff 0x06 0x72 0x45 0xc6 0xae 0x7b 0x9f
274
+ 0x55555559e070: 0xc1 0x36 0xd4 0x8e
275
+ ```
276
+
277
+ #### Verification
278
+
279
+ Last section of the program is to verify state (ie check that password was correct):
280
+
281
+ ``` c title="_ZN11rusty_vault4main17h33c04fad0008f474E()"
282
+ if ((local_d80 == 0x3c ) && (iVar4 = bcmp(__s1,local_d88,0x3c ), iVar4 == 0 )) { # some check
283
+ local_d78 = &PTR_s_Congratulations,_you_have_opened_0015a150; # this is what we want to see
284
+ local_d70 = 1;
285
+ local_d68 = 8;
286
+ local_d60 = ZEXT816(0);
287
+ std::io::stdio::_print (&local_d78);
288
+ }
289
+ else {
290
+ local_d78 = &PTR_s_nope_0015a140; # when password is wrong
291
+ local_d70 = 1;
292
+ local_d68 = 8;
293
+ local_d60 = ZEXT816(0);
294
+ std::io::stdio::_ print(&local_d78);
295
+ }
296
+ uVar10 = 0;
297
+ ```
298
+
299
+ Here we can see comparison of local_d80 to 0x3c which is 60. Looks like expected length as we also see call
300
+ bcmp with 3 params:
301
+
302
+ 1. `__s1` (which we identified is initialized with 60 bytes) - first param to compare;
303
+ 2. `local_d88` second param to compare;
304
+ 3. 0x3c (number of bytes to compare).
305
+
306
+ With debugger we can easily obtain expected value:
307
+ ```bash
308
+ (gdb) x/60bx 0x5555555b2b80
309
+ 0x5555555b2b80: 0xfa 0xa6 0x56 0x32 0xc3 0x71 0x30 0xcd
310
+ 0x5555555b2b88: 0x29 0x16 0x16 0x0f 0x39 0x4f 0xe7 0x65
311
+ 0x5555555b2b90: 0x2e 0xfa 0x05 0xdb 0xcc 0xea 0x47 0x12
312
+ 0x5555555b2b98: 0xc8 0xf4 0x7f 0xed 0x90 0x30 0xf6 0xad
313
+ 0x5555555b2ba0: 0xab 0xb1 0x50 0xa7 0xa2 0xcf 0xb5 0xd1
314
+ 0x5555555b2ba8: 0x3b 0x2e 0xb3 0x9a 0xfe 0x36 0xa0 0x8e
315
+ 0x5555555b2bb0: 0x90 0x18 0x9f 0x04 0xe7 0xcb 0x79 0x61
316
+ 0x5555555b2bb8: 0x5c 0xd9 0x5b 0x38
317
+ ```
318
+
319
+ ### Exploit
320
+
321
+ Now we understand what program is doing it encrypts password that we enter and expects result to be ` 0xfaa6... ` . Or more formally:
322
+ ```
323
+ AES.encrypt(password) = expected_value
324
+
325
+ # Because we have expected value, we can caluculate password using formular:
326
+ AES.decrypt(expected_value) = password
327
+ ```
328
+
329
+ ??? success "solve.py"
330
+ ```py
331
+ from Crypto.Cipher import AES
332
+
333
+ nonce = bytes.fromhex('ff067245c6ae7b9fc136d48e')
334
+ key = bytes.fromhex('9587e8e7dec03c28a28ca1f7352723816c216e10714a620b9e367893389690cf')
335
+ expected_value = bytes.fromhex('faa65632c37130cd2916160f394fe7652efa05dbccea4712c8f47fed9030f6adabb150a7a2cfb5d13b2eb39afe36a08e90189f04e7cb79615cd95b38')
336
+ cipher = AES.new(key, AES.MODE_GCM, nonce)
337
+ res = cipher.decrypt(expected_value)
338
+ print(res)
339
+ ```
40
340
41
341
42
342
## Epilogue
43
343
44
344
* Official website: [ https://downunderctf.com/ ] ( https://downunderctf.com/ )
45
345
* Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public
346
+
347
+ * [ PIE] : Position Independent Executable
0 commit comments