Skip to content

Commit 4cd72e6

Browse files
committed
rusty vault writeup
1 parent 5bb42cf commit 4cd72e6

File tree

2 files changed

+304
-2
lines changed

2 files changed

+304
-2
lines changed

mkdocs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ nav:
1818
- DNAdecay: ./dna/index.md
1919
- Sign in: ./sign_in/index.md
2020
- sssshhhh: ./sssshhhh/index.md
21-
# - Rusty vault: ./rusty/index.md
21+
- Rusty vault: ./rusty/index.md
2222
# - Jmp flag: ./jmp_flag/index.md
2323
# - Pac shell: ./pac_shell/index.md

solve/rusty/index.md

+303-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Solved: 81
1717

1818
Input files:
1919

20-
??? info "encoding.txt"
20+
* [rusty_vault binary](https://github.com/DownUnderCTF/Challenges_2024_Public/tree/f2797a33d8f5851508f37e854afceedf85eee8a3/rev/rusty-vault/publish)
2121

2222

2323
NB:
@@ -37,9 +37,311 @@ NB:
3737

3838
## My struggle
3939

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+
```
40340

41341

42342
## Epilogue
43343

44344
* Official website: [https://downunderctf.com/](https://downunderctf.com/)
45345
* Official writeups: https://github.com/DownUnderCTF/Challenges_2024_Public
346+
347+
*[PIE]: Position Independent Executable

0 commit comments

Comments
 (0)