ACS 2023: rustarm
find plain text
Last updated
find plain text
Last updated
Simple string encryptor written in Rust with known character set and XOR key.
For this challenge, we are given a Rust binary and an encoded secret that may potentially be our encrypted flag.
After playing around with the output, we observed the following behaviour:
If user input does not start the flag format, an error message Invalid input format
is thrown.
The Rust program encounters a length error whenever our input length is <5 characters.
When there are no characters between the curly brackets, no encrypted text is outputted.
Based on the observations above, it's safe to deduce that our flag length is 23 characters (excluding flag format) since there are 23 hex values in the provided enc
file.
At the start of the main function, there is a nested if statement that checks local_58 against the flag format ACS{}
. Based on this, we know that local_58 is our user-controlled input.
Furthermore, this portion of code in the main function shifts our user input forward by 4 bytes, which explains why the starting flag format ACS{
is not included in the encryption. At this point, we can also safely assume that the ending bracket }
will be excluded from the encryption.
Next, we can find a bunch of strings that are not null-terminated in Rust.
To clear these up for better readability. We can undefine all these data sections with Right Click -> Clear Code Bytes
, then redefine them in a character array of their respective size like char[23].
Further down, we find a suspicious XOR operation that involves 2 operands. To understand where the operands originated from, we will trace the variables using Ghidra's Slice Highlighting feature.
Right Click -> Highlight -> Forward Operator Slice
uVar3 -> local_a8
Right Click -> Highlight -> Backward Operator Slice
local_a8 -> local_e8
From this, we know that the first XOR operand is the 23-long string ACSISACEANCYBERSECURITY
.
Right Click -> Highlight -> Forward Operator Slice
uVar4 -> local_158
Right Click -> Highlight -> Backward Operator Slice
local_158 -> local_58 (Recall that we have previously identified local_58 as our user-controlled input)
The fact that the string ACSISACEANCYBERSECURITY
is the same length as our flag makes it a good candidate for the XOR encryption key. A classic way to verify this is by having the key XOR itself; and if the result is all zeroes, we know that the key is valid.
Based on that, we now know that the program is doing a simple character-by-character XOR like the following pseudocode:
However, when attempting to reverse this logic, we get several corrupted characters - Perhaps there's more twist to it?
Looking back, I realized there's a character set [0-9a-zA-Z\D\W]
initialized as local_d0, which has yet to be referenced anywhere.
At this point, I was wondering what kind of additional manipulation could've been applied to both our user input and the ACSISACEANCYBERSECURITY
key, which then leads me to the following logic:
The reason why I knew the alphabet.index()
function is needed for both the input and key operand, is because it would be impossible to get all zeroes when XORing the key by itself in the case where alphabet.index()
is applied only on either one of the operands.
Reversing the logic would then give us the following pseudocode:
Flag: ACS{cR0s$_C0mpi1e_wi7h_Rust}
Another behaviour that I noticed was the encryption is performed independently of each position. This means that, unlike in a typical Hash Diffusion scenario, any wrong guesses attempted will not alter other parts of the ciphertext. This behaviour alone reduces the number of guesses (premutations) required to brute force the flag.
To export the character set with a breeze, I used convert_to_python_list.py
from LazyGhidra to convert it into a Python list.
Then, I wrote a simple brute-force script to compare the results.
Flag: ACS{cR0s$_C0mpi1e_wi7h_Rust}