Following indices bases system is used to avoid ambiguity. Whenever element of a collection is referenced by number, 0-based index implied.
Ie, element 0 of list [1, 2, 4, 8, 16] is 1, Element 3 is 8.
When element is reference in explanation with word (first, third...), 1-based system is implied.
Ie, first character of string Hello World! is H, fifth is o.
Solution code was redacted for readability purposes. Due to time pressure during the competition I was using a lot of one-letter variables and questionable code structure.
Its time to open Ghidra. The main function is a thin wrapper that initializes standard rust runtime and then
calls _ZN11rusty_vault4main17h33c04fad0008f474E this is where the magic happens.
The function has very typical structure for security challenge. It consists of 3 key parts:
Initialization. Usually includes many constants for key, cypher setup;
Key mutation. This section can be recognised by many complicated loops/jmps/branches or cipher;
Verification. This section typically has string/byte array comparison and two branches: success and failure.
# this are contastants to initialise cipher state# from the first look# it is at least 0xe x 4 byte integers which gives us 15x4 = 60 bytes*__s1=0x3256a6fa;__s1[1]=0xcd3071c3;__s1[2]=0xf161629;__s1[3]=0x65e74f39;__s1[4]=0xdb05fa2e;__s1[5]=0x1247eacc;__s1[6]=0xed7ff4c8;__s1[7]=0xadf63090;__s1[8]=0xa750b1ab;__s1[9]=0xd1b5cfa2;__s1[10]=0x9ab32e3b;__s1[0xb]=0x8ea036fe;*(undefined8*)(__s1+0xc)=0x6179cbe7049f1890;__s1[0xe]=0x385bd95c;if(aes::autodetect::aes_intrinsics::STORAGE==-1){#someAESinitializationaes::autodetect::aes_intrinsics::init_get::cpuid(&local_9b8,1);aes::autodetect::aes_intrinsics::init_get::cpuid_count(&local_d78,7,0);if((~(uint)local_9b0&0xc000000)==0){uVar9=core::core_arch::x86::xsave::_xgetbv();uVar9=(uint)local_9b0>>0x19&(uVar9&2)>>1;aes::autodetect::aes_intrinsics::STORAGE=(char)uVar9;if(uVar9!=0)gotoLAB_00108dc3;}else{aes::autodetect::aes_intrinsics::STORAGE='\0';}}elseif(aes::autodetect::aes_intrinsics::STORAGE=='\x01'){LAB_00108dc3:# method annotated by Ghidra _<aes::ni::Aes256Enc as crypto_common::KeyInit>::new_<>::new(&local_d78,&DAT_0014a074);aes::ni::aes256::inv_expanded_keys(local_508,&local_d78);memcpy(local_5f8,&local_d78,0xf0);memcpy(&local_d78,local_5f8,0x1e0);gotoLAB_00108e2e;}aes::soft::fixslice::aes256_key_schedule(&local_d78,&DAT_0014a074);LAB_00108e2e:memcpy(&local_9b8,&local_d78,0x3c0);# method annotated by Ghidra _<aes_gcm::AesGcm<Aes,NonceSize,TagSize> as core::convert::From<Aes>>::from_<>::from(local_418,&local_9b8);local_9b8=0;local_9b0=&DAT_00000001;local_9a8=0;local_d78=&PTR_s_Enter_the_password_to_unlock_the_0015a118;#promptforpasswordlocal_d70=1;local_d68=8;local_d60=ZEXT816(0);std::io::stdio::_print(&local_d78);local_d78=(undefined**)std::io::stdio::stdin();auVar12=std::io::stdio::Stdin::read_line(&local_d78,&local_9b8);#readlineintovariableauVar12
So we can see a large array initialized. After that AES setup. Then program prompts the password and stores
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).
From this section important information we are looking for:
What algorithm is used;
How its initialized.
Annotation aes_gcm::AesGcm<Aes,NonceSize,TagSize> tells us its AES 256 GCM, we can now find documentation and all important
params and calls: https://docs.rs/aes-gcm/latest/aes_gcm/.
documentation sample
1 2 3 4 5 6 7 8 910111213141516171819202122232425
useaes_gcm::{aead::{Aead,AeadCore,KeyInit,OsRng},Aes256Gcm,Nonce,Key// Or `Aes128Gcm`};// The encryption key can be generated randomly:letkey=Aes256Gcm::generate_key(OsRng);// Transformed from a byte array:letkey: &[u8;32]=&[42;32];letkey: &Key<Aes256Gcm>=key.into();// Note that you can get byte array from slice using the `TryInto` trait:letkey: &[u8]=&[42;32];letkey: [u8;32]=key.try_into()?;// Alternatively, the key can be transformed directly from a byte slice// (panicks on length mismatch):letkey=Key::<Aes256Gcm>::from_slice(key);letcipher=Aes256Gcm::new(&key);letnonce=Aes256Gcm::generate_nonce(&mutOsRng);// 96-bits; unique per messageletciphertext=cipher.encrypt(&nonce,b"plaintext message".as_ref())?;letplaintext=cipher.decrypt(&nonce,ciphertext.as_ref())?;assert_eq!(&plaintext,b"plaintext message");
Key points:
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.
cipher.encrypt() takes nonce (according to docs 12 bytes) and plaintext.
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
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
part for now to safe time in case its not really needed. After the crazy loop, AES encrypt() is called.
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
that first one is self (aka this), and rest of params could be because we invoke some overloaded/internal method. I decided to
run program with gdb debugger to set a breakpoint here and see what this params are.
Instruction that I want to set breakpoint at is at address 0x001090be in Ghidra (we can't set breakpoint at address 0x001090be because
binary has PIE enabled and therefore every launch loaded to different address). Function _ZN11rusty_vault4main17h33c04fad0008f474E starts at 0x00108cf0, so
its 0x00108cf0 - 0x001090be = 974 bytes into the function. Therefore gdb command is br *(_ZN11rusty_vault4main17h33c04fad0008f474E+974).
Here I can see params of the call:
4th is password that we entered (probably plain_text) and before that is pointer to nonce which we can read from memory:
123
(gdb)x/12bx0x55555559e068# read 12 bytes in hex (we know length from docs)
0x55555559e068:0xff0x060x720x450xc60xae0x7b0x9f
0x55555559e070:0xc10x360xd40x8e
Now we understand what program is doing it encrypts password that we enter and expects result to be 0xfaa6.... Or more formally:
1234
AES.encrypt(password) = expected_value
# Because we have expected value, we can caluculate password using formular:
AES.decrypt(expected_value) = password