Angry Robot
In Grey Cat The Flag 2022, 474 points
You have entered a top secret robot production facilities and your clumsy friend tripped the alarm. You and your friends are about to be "decontaminated".
Luckily, you have unpacked the authentication firmware during previous reconaissance. Can you use them to override the decontamination process?
Challenge files: authentication.tar.gz
Introduction
The provided tar file contains 100 unique binaries. Fortunately, each of the binaries are very similar.
Here's be95e....
:
__int64 __fastcall sub_401176(char a1, int a2)
{
if ( a1 <= 31 || a1 == 127 )
exit(1);
return (unsigned int)((a2 * a1 % 2191 + a1 + a2) % 73 + 48);
}
__int64 __fastcall sub_401209(__int64 a1)
{
unsigned __int8 v2; // [rsp+1Bh] [rbp-55h]
int i; // [rsp+1Ch] [rbp-54h]
__int64 v4[4]; // [rsp+20h] [rbp-50h] BYREF
__int64 v5[6]; // [rsp+40h] [rbp-30h] BYREF
v5[5] = __readfsqword(0x28u);
qmemcpy(v4, "P6<rNK8Z=V4cgeuUWMvMr3ORx;", 26);
qmemcpy(v5, "Y7C43:WWmUYY4?jSJeqEiZa:qK", 26);
v2 = 1;
for ( i = 0; i <= 25; ++i )
{
if ( (unsigned __int8)sub_401176(*(_BYTE *)(i + a1), i) != *((_BYTE *)v4 + i)
|| (unsigned __int8)sub_401176(*(_BYTE *)(i + a1), *((char *)v4 + i)) != *((_BYTE *)v5 + i) )
{
v2 = 0;
}
}
return v2;
}
_BOOL8 __fastcall main(int a1, char **a2, char **a3)
{
char s[8]; // [rsp+0h] [rbp-30h] BYREF
__int64 v5; // [rsp+8h] [rbp-28h]
__int64 v6; // [rsp+10h] [rbp-20h]
__int16 v7; // [rsp+18h] [rbp-18h]
char v8; // [rsp+1Ah] [rbp-16h]
unsigned __int64 v9; // [rsp+28h] [rbp-8h]
v9 = __readfsqword(0x28u);
*(_QWORD *)s = 0LL;
v5 = 0LL;
v6 = 0LL;
v7 = 0;
v8 = 0;
fgets(s, 27, stdin);
return (unsigned __int8)sub_401209((__int64)s) == 0;
}
Not too hard to solve on its own. We can just compute the mapping for each (a1, a2) pair to sub_401176
output, then reverse the mapping to lookup a1
for a certain output and a2
:
a = "P6<rNK8Z=V4cgeuUWMvMr3ORx;"
b = "Y7C43:WWmUYY4?jSJeqEiZa:qK"
x = 2191
mapping = {}
def func(a1,a2):
return (a2 * a1 % x + a1 + a2)% 73 + 48
# Bruteforce (a1, a2) combinations
for i in range(32,127):
for j in range(0,len(a)):
y = func(i,j)
if y in mapping:
mapping[y].append((i,j))
else:
mapping[y] = [(i,j)]
out = ""
for i,j in enumerate(a):
cand = mapping[ord(j)]
for c in cand:
if c[1] == i and func(c[0], ord(j)) == ord(b[i]):
out += chr(c[0])
print(out)
# ip4kzes2j6c4n7exqbvynz0nnm
And we get the right passcode for this binary. But repeating this process 100 times is a bit too tedious as each binary has different a
and b
. The 2191 in a2 * a1 % 2191
is also replaced by a different number for each binary (we'll call this x
).
Not angr
From the challenge name, it's pretty clear that the author (probably an angr pro) intended participants to use angr to solve the password for each binary automatically. Unfortunately I am a noob at angr and couldn't really get it to work well despite my best efforts.
Since I already had a script to solve for the password given a
,b
, and x
, I figured I could just write something to extract these values for each binary. This can be done by simply grepping from the disassembly of the binary.
Let's look for x
first. There it is:
sub ecx, edx
mov edx, ecx
imul edx, 88Fh # 2191 in hex
sub eax, edx
mov edx, eax
So we just need to grep for imul edx
:
objdump -Mintel -d ./be95e0077c106dd8c0648f0c5f4efa349d29a2aa9ec2d998a34d4e7bab48f158 | grep "imul edx"
4011c0: 69 d2 8f 08 00 00 imul edx,edx,0x88f
This worked for all binaries except 6e0f1...
where x
was 2049, so the compiler did some smart optimization to get rid of the imul
.
Let's look at the disassembly for loading a
and b
:
movabs rax,0x5a384b4e723c3650
movabs rdx,0x557565676334563d
mov QWORD PTR [rbp-0x50],rax
mov QWORD PTR [rbp-0x48],rdx
movabs rax,0x524f33724d764d57
mov QWORD PTR [rbp-0x40],rax
mov WORD PTR [rbp-0x38],0x3b78
movabs rax,0x57573a3334433759
movabs rdx,0x536a3f345959556d
mov QWORD PTR [rbp-0x30],rax
mov QWORD PTR [rbp-0x28],rdx
movabs rax,0x3a615a694571654a
mov QWORD PTR [rbp-0x20],rax
mov WORD PTR [rbp-0x18],0x4b71
mov BYTE PTR [rbp-0x55],0x1
The final mov
just sets the counter to 1, so I used python to extract all mov
s with a constant, starting from the first movabs
and ending at the mov BYTE PTR [rbp-0x55],0x1
:
for file in sorted(os.listdir(".")):
if len(file) != 64:
continue
out = os.popen(f"objdump -Mintel -d ./{file}").read().strip()
a = b""
start = False
for line in out.split("\n"):
if "movabs" in line:
start = True
if start and "mov" in line and ",0x" in line:
h = int(line.split(",0x")[1],16)
a += h.to_bytes(32, 'little').replace(b"\0",b"")
if "BYTE PTR" in line:
start = False
d[file[:5]].append(a[:len(a)//2])
d[file[:5]].append(a[len(a)//2:])
Since the two strings have the same length, I just split the extracted string in half. Now that we have all the pieces, we can finally solve the challenge:
answers = {}
for key,val in d.items():
answers[key] = solve(val[0], val[2].decode(), val[3].decode())
p = remote("nc challs.nusgreyhats.org 10523")
p.sendline("y")
for i in range(5):
p.recvuntil(" challenge code: ")
x = p.recvline(keepends=False)
p.sendline(answers[x[:5].decode()])
p.interactive()
# grey{A11_H4il_SkyN3t}