Skip to content

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

c
__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:

py
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:

asm
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:

asm
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 movs with a constant, starting from the first movabs and ending at the mov BYTE PTR [rbp-0x55],0x1:

python
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:

python
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}