# ACS 2023: rustarm

## TL;DR

> Simple string encryptor written in Rust with known character set and XOR key.

## Challenge Overview

For this challenge, we are given a Rust binary and an encoded secret that may potentially be our encrypted flag.

<figure><img src="/files/LoDwCBv0EGsXld6rLhCA" alt=""><figcaption></figcaption></figure>

After playing around with the output, we observed the following behaviour:

1. If user input does not start the flag format, an error message `Invalid input format` is thrown.&#x20;
2. The Rust program encounters a length error whenever our input length is <5 characters.
3. When there are no characters between the curly brackets, no encrypted text is outputted.&#x20;

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.

<figure><img src="/files/FtLoxXI303sc7kFC9U4z" alt=""><figcaption></figcaption></figure>

## **Solution 1: Decoding Values Manually**

### Validating Input Format

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.

<figure><img src="/files/7AtuvqDV1yoTw8wIhpB2" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/kId2KrH7M2CECnIXsv8r" alt=""><figcaption></figcaption></figure>

### Non-Null Terminated Strings

Next, we can find a bunch of strings that are not null-terminated in Rust.&#x20;

<figure><img src="/files/jqicy4tZVIdwZKc4F0Rs" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/xYQKa9Dtbd0jBTKCFgxB" alt=""><figcaption></figcaption></figure>

### XOR Operation

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.

<figure><img src="/files/VEcZevuOqXm0VSWDBrEL" alt=""><figcaption></figcaption></figure>

#### Tracing Operand 1: uVar3

Right Click -> Highlight -> Forward Operator Slice

uVar3 -> local\_a8

<figure><img src="/files/xqyhTaQ5aX9iyNqPnKni" alt=""><figcaption></figcaption></figure>

Right Click -> Highlight -> Backward Operator Slice

local\_a8 -> local\_e8

<figure><img src="/files/EjUmJhMJ6xILvi8qklAF" alt=""><figcaption></figcaption></figure>

From this, we know that the first XOR operand is the 23-long string `ACSISACEANCYBERSECURITY`.

<figure><img src="/files/EvijaXD5SDcfJpaTAiFc" alt=""><figcaption></figcaption></figure>

#### Tracing Operand 2: uVar4

Right Click -> Highlight -> Forward Operator Slice

uVar4 -> local\_158

<figure><img src="/files/c8LB8RUHRaGla9z0SpQL" alt=""><figcaption></figcaption></figure>

Right Click -> Highlight -> Backward Operator Slice

local\_158 -> local\_58 (Recall that we have previously identified local\_58 as our user-controlled input)

<figure><img src="/files/PTe1kPbSQlL9pk779sd8" alt=""><figcaption></figcaption></figure>

### Hypothesis 1

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.

<figure><img src="/files/b0oKMkTwpRSbWqSQy3ii" alt=""><figcaption></figcaption></figure>

Based on that, we now know that the program is doing a simple character-by-character XOR like the following pseudocode:

```python
for i in range(23):
    output[i] = input[i] ^ key[i]
```

However, when attempting to reverse this logic, we get several corrupted characters - Perhaps there's more twist to it?

<figure><img src="/files/ZiHS3JWonHAFjfTfNC9U" alt=""><figcaption></figcaption></figure>

### Hypothesis 2&#x20;

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.

<figure><img src="/files/xnaULz6lu0NfnuRgZmom" alt=""><figcaption></figcaption></figure>

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:

```python
for i in range(23):
    output[i] = alphabet.index(input[i]) ^ alphabet.index(key[i])
```

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:

<pre class="language-python"><code class="lang-python">for i in range(23):
<strong>    input[i] = alphabet[output[i] ^ alphabet.index(key[i])]
</strong></code></pre>

<figure><img src="/files/JzJAAVQmWbNUrEjiLH4K" alt=""><figcaption></figcaption></figure>

**Flag:** ACS{cR0s$\_C0mpi1e\_wi7h\_Rust}

### Final Script

```python
#!/usr/bin/python3

alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()-_=+"
output = bytes.fromhex("2c171c245d43000e3a24202323472130092757002a2b15")
key = [alphabet.index(c) for c in "ACSISACEANCYBERSECURITY"]

assert(len(output) == len(key))

flag = ""
for e, k in zip(output, key):
    flag += alphabet[e ^ k]
print("ACS{" + flag + "}")
```

## **Solution 2: Brute Forcing**

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.

<figure><img src="/files/gOibzaQUdQeLnzM85pFm" alt=""><figcaption></figcaption></figure>

To export the character set with a breeze, I used `convert_to_python_list.py` from LazyGhidra to convert it into a Python list.

<figure><img src="/files/hOnesyanhQxkKxfT8z4O" alt=""><figcaption></figcaption></figure>

Then, I wrote a simple brute-force script to compare the results.

<figure><img src="/files/fplnzjSAPukUfDSH69ly" alt=""><figcaption></figcaption></figure>

**Flag:** ACS{cR0s$\_C0mpi1e\_wi7h\_Rust}

### Final Script

```python
#!/usr/bin/python3

from pwn import *

exe = "./rustarm"
elf = context.binary = ELF(exe, checksec=False)

context.clear(arch="amd64")
context.log_level = 'CRITICAL'
warnings.filterwarnings(action='ignore', category=BytesWarning)

char_set = [
    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 
    0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 
    0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 
    0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x21, 0x40, 
    0x23, 0x24, 0x25, 0x5E, 0x26, 0x2A, 0x28, 0x29, 0x2D, 0x5F, 0x3D, 0x2B
]

enc = "2c171c245d43000e3a24202323472130092757002a2b15"
global password
password_length = 23

def main():
    password = "ACS{"

    for i in range(password_length):
        for char in char_set:
            io = elf.process()
            io.recvuntil(b'plain text: ')

            candidate_password = password + chr(char) + chr(125) # closing curly bracket
            io.sendline(candidate_password)
            print("Trying: ", candidate_password)

            io.recvuntil(b'encrypt text: ')
            output = io.recvline()[:-1].decode()

            if enc.startswith(output):
                print("Password Found:", candidate_password)
                password = password + chr(char)
                break

            io.close()
            time.sleep(0.15)

if __name__ == "__main__":
        main()
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jesuscries.gitbook.io/home/ctf-writeups/reverse-engineering/acs-2023-rustarm.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
