Story
Tired from all of the craziness in the Inner Sanctum, you decide to venture out to the beach to relax. You doze off in the sand only to be awoken by the loud “reee” of an osprey. A shell falls out of its talons and lands right where your head was a moment ago. No rest for the weary, huh? It looks a little funny, so you pick it up and realize that it’s backwards. I guess you’ll have to reverse it.
The files
An ELF executable is given, and initial decompile shows that it’s packed.
In main
there is an unpacking operation on the function flag_check
.
Each byte is passed to scramble
where it performs some bitwise operations.
void main(int param_1)
{
ulong scrambled_byte;
long is_valid;
int j;
int i;
if (param_1 < 2) {
puts("need a flag!");
}
i = 0;
while (i < 0x7a69) {
j = 0;
while (j < 0x228) {
scrambled_byte = scramble((byte)flag_check[j]);
flag_check[j] = SUB81(scrambled_byte,0);
j = j + 1;
}
i = i + 1;
}
is_valid = flag_check();
if (is_valid == 0) {
puts("Wrong!");
}
else {
puts("Correct!");
}
return;
}
Unpacking
We did not reverse engineer the scramble function, instead inserted a breakpoint at 0x004006db
and dumped the .text
section.
The memory map of the process shows that the section has read, write, and execute permissions.
After repairing the ELF file, we’re able to get a working binary that showed us the checking logic. We noticed some obfuscation techniques being thrown in to mess up the decompiler but Ghidra did surprisingly well.
We noticed that the flag is being passed in via rax
and did some adjustments to make the output a bit more accurate.
Here’s the gist of it.
ulong flag_check(char *flag)
{
char c, key;
int len;
long k;
char *flag_ptr;
int i, j;
bool ret;
k = -1;
flag_ptr = flag;
/* strlen operation */
do {
len = (int)k;
if (k == 0) break;
k = k + -1;
len = (int)k;
c = *flag_ptr;
flag_ptr = flag_ptr + 1;
} while (c != '\0');
/* scrambling the flag */
key = 0x50;
i = 0;
while (i < 0x539) {
j = 0;
while (j < (int)(~len - 1U)) {
flag[j] = flag[j] ^ key;
key = key ^ flag[j];
j = j + 1;
}
i = i + 1;
}
ret = true;
i = 0;
while (i < (int)(~len - 1U)) {
if (ret == false) {
ret = false;
}
else {
/* scrambled flag is here */
ret = flag[i] == (&scrambled_flag)[i];
}
i = i + 1;
}
return (ulong)ret;
}
This functions computes the length of the flag and performs a long series of XORing operations.
Finally it checks byte-by-byte against the scrambled flag, the values of scrambled_flag
when dumped in decimals are.
[72, 95, 54, 53, 53, 37, 20, 44, 29, 1, 3, 45, 12, 111, 53, 97, 126, 52, 10, 68, 36, 44, 74, 70, 25, 89, 91, 14, 120, 116, 41, 19, 44, 0]
Solving
The scrambling is then implemented with Python and the flags were replaced with symbols. Constraints were added to the scrambled flag, and we run the script with different guesses to the flag length.
import claripy
def enc(flag):
key = 0x50
for _ in range(1337):
for i, c in enumerate(flag):
a = c
flag[i] ^= key
key = a
return flag
def check(flag):
chk = [72, 95, 54, 53, 53, 37, 20, 44, 29, 1, 3, 45, 12, 111, 53, 97, 126, 52, 10, 68, 36, 44, 74, 70, 25, 89, 91, 14, 120, 116, 41, 19, 44, 0]
for i, c in enumerate(flag):
solver.add(c == chk[i])
# Password length is 33
password_len = 33
solver = claripy.Solver()
flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(password_len)]
flag = claripy.Concat(*flag_chars)
for c in flag_chars:
solver.add(c >= 0x20)
solver.add(c < 0x7f)
enc(flag_chars)
check(flag_chars)
try:
for f in solver.eval(flag, 1):
username = f.to_bytes(password_len, 'big')
print(username)
except claripy.errors.UnsatError:
print(f"Unsat for len {password_len}")
pctf{ok_nothing_too_fancy_there!}