Swiss Hacking Challenge 2023 - knive

Posted on Apr 26, 2023

Information

Challenge category: re

Challenge Description

As a chef, Robert knew the importance of having the right tools in his kitchen. He had a special knife that he used only for his secret ingredient. No one knew what it was, not even his sous chefs who had been with him for years. It was a small vial of liquid that he would dab onto his knife before using it to slice his meat. His dishes were famous, and people came from all over to try his food, but the secret ingredient remained a mystery.

Files

We are given a knive.zip file. It contains the following executable:

knive: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7571c30ab2d39b922e02f49155c5dc4d6d9e3fd9, for GNU/Linux 3.2.0, stripped
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

Exploitation

Analyzing the file

When decompiling with ghidra (inside of pwndbg) we can see the following:

ulong main(ulong argc, char **argv)

{
    int32_t iVar1;
    ulong uVar2;
    int64_t iVar3;
    ulong *puVar4;
    int64_t in_FS_OFFSET;
    uint8_t uVar5;
    ulong var_540h;
    ulong var_534h;
    int32_t wstatus;
    ulong fd;
    int32_t var_520h;
    int32_t pid;
    uint64_t var_518h;
    ulong var_510h;
    ulong var_508h;
    ulong *var_4f8h;
    ulong nbytes;
    ulong var_498h;
    uchar s [128];
    ulong ptr;
    int64_t canary;
    
    uVar5 = 0;
    canary = *(in_FS_OFFSET + 0x28);
    fd._0_4_ = sym.imp.memfd_create("knive", 0);
    if (fd == -1) {
        uVar2 = 1;
    }
    else {
        puVar4 = &var_510h;
        for (iVar3 = 0xe; iVar3 != 0; iVar3 = iVar3 + -1) {
            *puVar4 = 0;
            puVar4 = puVar4 + uVar5 * -2 + 1;
        }
        fd._4_4_ = sym.imp.inflateInit_(&var_510h, "1.2.11", 0x70);
        if (fd._4_4_ == 0) {
            var_508h._0_4_ = *0x47cc;
            var_510h = 0x4020;
            do {
                nbytes._0_4_ = 0x400;
                var_4f8h = &ptr;
                fd._4_4_ = sym.imp.inflate(&var_510h, 0);
                if (((fd._4_4_ == 2) || (fd._4_4_ == -3)) || (fd._4_4_ == -4)) {
                    uVar2 = 1;
                    goto code_r0x000015e1;
                }
                var_520h = 0x400 - nbytes;
                iVar3 = sym.imp.write(fd, &ptr, var_520h);
                if (iVar3 != var_520h) {
                    uVar2 = 1;
                    goto code_r0x000015e1;
                }
            } while ((nbytes == 0) || (fd._4_4_ != 1));
            iVar1 = sym.imp.pipe(&var_498h);
            if (iVar1 == -1) {
                uVar2 = 1;
            }
            else {
                pid = sym.imp.fork();
                if (pid == -1) {
                    uVar2 = 1;
                }
                else {
                    if (pid == 0) {
                        sym.imp.close(var_498h._4_4_);
                        **argv = var_498h + '0';
                        (*argv)[1] = '\0';
                        iVar1 = sym.imp.fexecve(fd, argv, _reloc.__environ);
                        if (iVar1 == -1) {
                            uVar2 = 1;
                            goto code_r0x000015e1;
                        }
                    }
                    else {
                        sym.imp.close(var_498h);
                        sym.imp.write(var_498h._4_4_, 0x4021, 0x80);
                        sym.imp.write(var_498h._4_4_, 0x47e0, 0x80);
                        sym.imp.puts("Please enter the password:");
                        for (var_518h = 0; var_518h < 0x80; var_518h = var_518h + 1) {
                            s[var_518h] = 0;
                        }
                        sym.imp.fgets(s, 0x80, _reloc.stdin);
                        sym.imp.write(var_498h._4_4_, s, 0x80);
                        sym.imp.waitpid(pid, &wstatus, 0);
                        if ((wstatus >> 8 & 0xffU) == 0x2a) {
                            sym.imp.puts("Congratulations, this input is correct!");
                        }
                        else {
                            sym.imp.puts("Sorry, this is not correct :(");
                        }
                    }
                    uVar2 = 0;
                }
            }
        }
        else {
            uVar2 = 1;
        }
    }

We can see the program first creates a file descriptor and then zlib-deflates some code into that descriptor. Later on this code gets executed in a separate process using fexecve

We can run the binary in gdb and we see that indeed there is forking happening:

pwndbg> run
Starting program: /home/giank/Downloads/knive 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Attaching after Thread 0x7ffff7f7e740 (LWP 48066) fork to child process 48069]
[New inferior 2 (process 48069)]

Now we can dump the executable using the proc filesystem out of the memory into an actual file: cp /proc/48069/exe /tmp/recovered_binary

In IDA the decompilation looks like the following:

__int64 __fastcall main(char a1, char **a2, char **a3)
{
  int v4; // [rsp+10h] [rbp-1A0h]
  int fd; // [rsp+14h] [rbp-19Ch]
  unsigned __int64 i; // [rsp+18h] [rbp-198h]
  char buf[128]; // [rsp+20h] [rbp-190h] BYREF
  char v8[128]; // [rsp+A0h] [rbp-110h] BYREF
  char v9[136]; // [rsp+120h] [rbp-90h] BYREF
  unsigned __int64 v10; // [rsp+1A8h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  if ( a1 != 1 )
    return 0LL;
  fd = **a2 - 48;
  if ( read(fd, buf, 0x80uLL) != 128 )
    return 0LL;
  if ( read(fd, v8, 0x80uLL) != 128 )
    return 0LL;
  if ( read(fd, v9, 0x80uLL) != 128 )
    return 0LL;
  v4 = 0;
  for ( i = 0LL; i <= 0x7F; ++i )
  {
    if ( buf[i] == ((unsigned __int8)(v9[i] ^ v8[i]) ^ 0x2A) )
      ++v4;
  }
  if ( v4 == 128 )
    return 42LL;
  else
    return 0LL;
}

How do we get the values of v8 and v9? I used straceand took the values from the write() calls right before asking the user for input:

strace -xx -v -s 128 ./knive

write(5, "\x9c\xed\x5b\x6d\x6c\x14\x45\x18\x9e\xbd\x5e\xaf\xc7\x47\xb9\x53\x41\xb1\xa0\x5c\x08\x20\x1f\xe9\xd2\x2b\xa5\xa9\x9a\x0a\xb6\x14\x16\x03\x58\xb0\x4d\x34\x01\xaf\xdb\xde\xb6\x77\xf1\x7a\xd7\xdc\xed\x41\x31\x0a\x47\x6a\xd1\xa6\x9c\x41\x8d\x62\x62\x4c\xd4\x98\xa8\x89\x89\xfd\x21\xa4\x51\x6b\x4e\x31\xa0\x98\x10\xf8\x61\x82\xfe\x42\x23\x49\x89\x5f\x27\x28\xa9\x1f\x74\x9d\xd9\x7d\xdf\xeb\xee\x70\x67\x8d\x51\xff\x38\x4f\x72\xfb\xec\xfb\xce\xfb\xcc\xcc\xce\xcc\xce\xf5\xba\xef\xee\x6f\xd9\xb2\xd1\x25\x49\x04\x51\x46", 128) = 128 write(5, "\xc5\xaf\x12\x75\x76\x0c\x5c\x49\xe0\xff\x47\xda\xdd\x1d\xe3\x49\x18\xaa\xfe\x45\x7d\x3a\x53\x9c\x93\x6f\xbe\xf5\x83\x7f\xf5\x0b\x63\x4f\x42\xe8\x0c\x41\x4f\xbd\x90\x97\xae\x6d\xe9\x36\xce\x94\xf6\x5b\x7a\x5d\x67\x40\xfb\x8c\xb6\x6b\xa7\x48\x48\x66\xfe\xb2\x82\xa3\xa3\xd7\x0b\x8e\x7b\x41\x64\x1b\x8a\xb2\x3a\xd2\x4b\xa8\xd4\x68\x09\x63\xa3\x75\x0d\x02\x83\x35\x5e\xb7\xf3\x57\xf5\xc1\xc4\x5a\x4d\xa7\x7b\xd5\x12\x65\x58\xd1\xc6\xd1\xe4\xd1\xe6\xe6\xe4\xe6\xe4\xdf\x90\xc5\xc4\x45\xf3\x98\xfb\x0f\x63\x2e\x7b\x6c", 128) = 128

Decoding the stuff

We build a script in python that does the string1 ^ string2 ^ 0x2A operation (which can be seen in above decompilation) and prints the flag:

#!/usr/bin/python3

import binascii

string1 = bytearray(b"<first bytestring>")
string2 = bytearray(b"<second bytestring>")


# the xor key
key = 0x2A


# xor string1 ^ string2 ^ key
result = bytearray([x ^ y ^ key for (x, y) in zip(string1, string2)])

# print the result
print(result)

Flag

We get the flag:

shc2023{Th3_0pp0s1t3_0f_kn1v3_i5_f0rk_d8ac202f3b10a}

Conclusion

I needed some time to understand the whole fexecve magic but in the end I was able to solve it without touching assembly :D

References

  1. https://github.com/pwndbg/pwndbg/blob/dev/FEATURES.md#ghidra
  2. https://sandflysecurity.com/blog/detecting-linux-memfd-create-fileless-malware-with-command-line-forensics/