Just Rust -- ångstromCTF 2020
Just Rust – ångstromCTF 2020
by RageKnify
Points: 170 (Dynamic, 47 solves)
Description: Clam really enjoys writing in C, but he realizes that he’ll have to learn another language eventually. After all, he’s grown pretty rusty after only programming in C for a year. So, he decided to write a program to create some ASCII art from user input. He also provided a sample output!
Given: program, flag_output
Solution
TL;DR
-
Try to reverse
-
Give up on Reversing Rust
-
Identify which output bits each char affects
-
Brute-force the flag char by char until each bit has the target value
Exploit in solve.py
Description
We are given a program that asks for 32 characters and prints out some ASCII art based on the input.
$ ./just_rust
What do you want to encode?
asdf
Input must be 32 characters.
$ ./just_rust
What do you want to encode?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OOOOOOOO
@@@@@@@@
@@@@@@@@
@@@@@@@@
@@@@@@@@
OOOOOOOO
OOOOOOOO
@@@@@@@@
We also have the desired output with the flag redacted:
$ ./just_rust
What do you want to encode?
[REDACTED]
CCHJEHMK
CFKJCEOL
FOJLMOJJ
BDN@H@BA
ODMJHFCJ
MOOKMOOO
OOAOFOGI
@@@@@@@@
I opened Ghidra to try to Reverse the program, but the first 5 lines of main were:
habe1453c04c6104c(local_1e8,&PTR_s_What_do_you_want_to_encode?_Cann_00338de8,1,8,0);
_print(local_1e8);
h980c23501ad67776(&local_1b8);
local_188 = stdin();
read_line(local_1a0,&local_188,&local_1b8,read_line);
hfa3ac7a45b0a87ed(local_1a0,0x12bfff,0x12);
h8846cd2964247bd9(&local_188);
Seeing those weirdly named functions I closed Ghidra and decided to do it Black Box.
Playing around I noticed that changing one char of the input only changed a few chars of the output:
$ ./just_rust
What do you want to encode?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OOOOOOOO
@@@@@@@@
@@@@@@@@
@@@@@@@@
@@@@@@@@
OOOOOOOO
OOOOOOOO
@@@@@@@@
$ ./just_rust
What do you want to encode?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
OOOOOOOG
H@@@@@@@
@@@@@@@@
@@@@@@@@
@@@@@@@@
OOOOOOOO
OOOOOOOO
@@@@@@@@
Solution
I wrote a python script to try to brute-force the flag char-by-char.
Try an input
from subprocess import Popen, PIPE
def attempt(inp): # Returns output of a given input in a string
p = Popen("./just_rust", stdin=PIPE, stdout=PIPE)
res = p.communicate(str.encode(inp))[0]
res = res.split(b'\n')[1:-1]
res = ''.join([x.decode() for x in res])
return res
Find which bits differ between 2 strings
def tobits(s): # Stolen from some StackOverflow post
result = []
for c in s:
bits = bin(ord(c))[2:]
bits = '00000000'[len(bits):] + bits
result.extend([int(b) for b in bits])
return result
def calc_diffs(string1, string2):
res = set()
bits1 = tobits(string1)
bits2 = tobits(string2)
for i in range(len(bits1)):
if bits1[i] != bits2[i]:
res.add(i)
return res
Find which bits are affected by each char we don’t know yet
possible_chars = string.ascii_lowercase + string.ascii_uppercase + '0123456789_'
flag = "actf{" + "?"*26 + "}"
default = attempt(flag)
affs = {}
for i in range(5, 5+26): # chars whose values we don't know
changed = set()
for c in possible_chars:
flag_try = flag[:i] + c + flag[i+1:]
result = attempt(flag_try)
diffs = calc_diffs(default, result)
changed.update(diffs)
affs[i] = changed
At this point we can check whether our theory holds:
for key in affs:
bits = list(affs[key])
bits.sort()
print(bits)
[47, 119, 191, 199, 271, 343, 415]
[55, 127, 135, 207, 279, 351, 423]
[63, 71, 143, 215, 287, 359, 431]
[6, 78, 150, 222, 294, 366, 438]
[14, 86, 158, 230, 302, 374, 446]
[22, 94, 166, 238, 310, 382, 390]
[30, 102, 174, 246, 318, 326, 398]
[38, 110, 182, 254, 262, 334, 406]
[46, 118, 190, 198, 270, 342, 414]
[54, 126, 134, 206, 278, 350, 422]
[62, 70, 142, 214, 286, 358, 430]
[5, 77, 149, 221, 293, 365, 437]
[13, 85, 157, 229, 301, 373, 445]
[21, 93, 165, 237, 309, 381, 389]
[29, 101, 173, 245, 317, 325, 397]
[37, 109, 181, 253, 261, 333, 405]
[45, 117, 189, 197, 269, 341, 413]
[53, 125, 133, 205, 277, 349, 421]
[61, 69, 141, 213, 285, 357, 429]
[4, 76, 148, 220, 292, 364, 436]
[12, 84, 156, 228, 300, 372, 444]
[20, 92, 164, 236, 308, 380, 388]
[28, 100, 172, 244, 316, 324, 396]
[36, 108, 180, 252, 260, 332, 404]
[44, 116, 188, 196, 268, 340, 412]
[52, 124, 132, 204, 276, 348, 420]
Yes, none of the otput bits are affected by 2 different chars of the input.
Brute-force chars one by one:
# Removed `\n`s, they don't matter
flag_res = "CCHJEHMKCFKJCEOLFOJLMOJJBDN@H@BAODMJHFCJMOOKMOOOOOAOFOGI@@@@@@@@"
for i in range(5, 5+26):
for c in possible_chars:
flag_try = flag[:i] + c + flag[i+1:]
result = attempt(flag_try)
wrong = calc_diffs(flag_res, result)
# Check that only bits not controlled by this char are wrong
if wrong.isdisjoint(affs[i]):
flag = flag_try
break
print(flag)
Running that we get the flag: actf{b1gg3r_4nd_b4dd3r_l4ngu4g3}
I went to submit the flag and found out I got first blood, so I decided it would be my first writeup.
The challenge author, Aplet123, posted the source code here.
Resources:
Exploit in solve.py