Casual McDonald's Employee Scriptorium
BlogMemesGitHubAbout
  • root@JesusCries
  • ⛩️Red Teaming
    • Methodology
    • Red Team Infrastructure
    • Initial Access
    • Reconnaissance
    • Lateral Movement
    • Post-Exploitation
      • Credentials Dumping
    • Persistence
    • Evasion
      • Memory Scanner
      • Antimalware Scan Interface (AMSI)
      • Event Tracing for Windows (ETW)
      • Attack Surface Reduction (ASR)
      • Microsoft Windows Defender Application Control (WDAC)
      • EDR Evasion
    • Offensive Development
      • Process Injection & Shellcode Loader
      • Portable Executable (PE) Loader
      • User Defined Reflective Loader
      • Beacon Object Files
    • Command & Control (C2)
      • Cobalt Strike
      • Havoc
      • Mythic
      • Sliver
    • Miscellaneous
      • Interesting Read
      • Certification Reviews
        • Certified Red Team Lead (CRTL)
  • 🧊Active Directory & Pentest
    • Check List
  • 🚩CTF Writeups
    • Reverse Engineering
      • Wargames.MY 2024: World 3
      • Wargames.MY 2023: Defeat the boss!
      • ACS 2023: Licrackense Pt I
      • ACS 2023: babyrev
      • ACS 2023: expr
      • ACS 2023: rustarm
      • ACS 2023: Maze
      • SiberSiaga 2023: Obstacles
      • SiberSiaga 2023: Packed
      • SiberSiaga 2023: Malbot
      • SiberSiaga 2023: Vacine
      • ABOH 2023: MetalPipe
      • ABOH 2023: Grape
      • iCTF 2023: RemoteC4
    • Binary Exploitation
      • HTB Cyber Apocalypse 2024: SoundOfSilence
      • LACTF 2024: pizza
      • ACS 2023: Licrackense Pt II
      • ACS 2023: Shellcoding Test
      • ACS 2023: Coding Test
      • ACS 2023: register
      • Wargames.MY 2023: Pak Mat Burger
      • SiberSiaga 2023: Password Generator
      • NahamCON CTF 2023: nahmnahmnahm
      • NahamCON CTF 2023: Weird Cookie
      • TJCTF 2023: shelly
      • TJCTF 2023: formatter
      • ångstromCTF 2023: gaga2
      • ångstromCTF 2023: leek
      • Space Heroes 2023: Rope Dancer
      • corCTF 2022: babypwn
      • corCTF 2021: Cshell
      • HTB Cyber Apocalypse 2023: Void
      • HTB Cyber Santa CTF 2021: minimelfistic
      • HTB Challenge: pwnshop
  • 🤡Clown Chronicles
    • About Me
    • Blogs
      • How to Win A CTF by Overcomplicating Things
      • Exploring Dynamic Invocation for Process Injection in C# and Rust
    • Projects
    • Memes
    • Others
Powered by GitBook
On this page
  • TL;DR
  • Challenge Overview
  • Solution 1: Decoding Values Manually
  • Validating Input Format
  • Non-Null Terminated Strings
  • XOR Operation
  • Hypothesis 1
  • Hypothesis 2
  • Final Script
  • Solution 2: Brute Forcing
  • Final Script
  1. CTF Writeups
  2. Reverse Engineering

ACS 2023: rustarm

find plain text

PreviousACS 2023: exprNextACS 2023: Maze

Last updated 1 year ago

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.

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.

  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.

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.

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.

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.

Non-Null Terminated Strings

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

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.

Tracing Operand 1: uVar3

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.

Tracing Operand 2: uVar4

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)

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.

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

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?

Hypothesis 2

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:

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:

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

Flag: ACS{cR0s$_C0mpi1e_wi7h_Rust}

Final Script

#!/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.

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}

Final Script

#!/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()
🚩