Tuesday, October 28, 2008

Defcon 16 CTF - Krypto

Krypto is a dynamically linked and not stripped ELF binary which listens on port 20020. Connection to this port does not return any information.


This is the interesting code left, after having put aside the typical functions (daemon creation, privileges drop, ...):


The program begins with reading the key file and keeps its content in memory. Then, a 32 characters string only composed of "A" is created, and Krypto reads on the socket a maximal 63 characters buffer (or stops if it finds "\n"). The address of this buffer is stored in ebx.


The following "repne scasb" instruction searches for the "\0" character (as eax has been set to 0 via "xor eax, eax") in the string pointed to by edi. At this point, this string in edi is the previously read buffer on the socket via "mov edi, ebx".

After this instruction, ecx is set to the string length, and edi points to the character following "\0".

A call to the RC4_set_key() function is then performed, with the buffer read on the socket and the previously calculated length as parameters. The way RC4 algorithm works is easy to understand: the key allows generation of a pseudorandom stream of bits, called keystream, which is then used to encrypt data with a simple XOR. As a consequence, encryption and decryption functions are the same. Here, the key is the user-provided entry.

This are the definition of the OpenSSL RC4 functions:
void RC4_set_key(RC4_KEY *key, int len, const unsigned char *data);
void RC4(RC4_KEY *key, unsigned long len, const unsigned char *indata, unsigned char *outdata);

The RC4_set_key() function gives us a RC4_KEY variable created from the string key, which is then used as argument to the RC4() function. The latter function encrypts the 32 "A" string generated in the first place. After this call, esi points to the encrypted result.

The alarm() function is called to kill the process after 2 seconds. We now have a "call esi", where esi is a pointer to our encrypted buffer. Obviously, it crashes, but the service forked during connection so nothing can be seen and the service still runs. If the service does not crash (the "call esi" returns), the alarm() function is called again to cancel the first call to alarm(). Finally, a four characters string is sent on the socket and the exit() function terminates the child.

To sum up, we have:
  1. String_to_be_encrypted = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
  2. RC4_Key = our 63 characters maximum buffer
  3. RC4 encryption of the fixed string with the user-provided key
  4. Execution of the encryption result

So, where is the vulnerability which gives us a shell?

Obviously, we cannot bruteforce RC4 to forge a valid shellcode. The problem results from the way the key length is calculated. In fact, krypto expects the key to end with "\0"; we can use a very small key (of a few bytes), have it followed by a "\0", and include whatever we want at the end of the buffer.

Why do that? We must keep in mind that after the "repne scasb" instruction, edi is a pointer to the character following the "\0", that is to say, the buffer left which will not be used as the RC4 key. We now have to make the RC4 encryption generate a result with a jump to edi as first bytes. We can use "push edi ret" instructions (opcodes: "\x57\xC3").

We have to bruteforce a small key for the RC4 encryption of the "A" string to begin with these two opcodes. A 4 bytes key can be used; this key must not contain "\0" or "\n", and must not remain the same to avoid a too easy IDS blacklisting.

This is the C code to bruteforce the RC4 key:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <openssl/rc4.h>

int main(int ac, char **av)
{
  unsigned short a, b, c, d;
  RC4_KEY          key;
  unsigned char  data[4];
  unsigned char  outdata[32];
  srand(time(NULL));

  while (42) {
    a = 1 + (int) (255.0 * (rand() / (RAND_MAX + 1.0)));
    if (a == 0x0a)
         continue;
    for (b = 1; b <= 0xff; b++) {
      for (c = 1; c <= 0xff; c++) {
        for (d = 1; d <= 0xff; d++) {
          data[0] = d;
          data[1] = c;
          data[2] = b;
          data[3] = a;
          data[4] = 0;
          RC4_set_key(&key, 4, data);
          RC4(&key, 32, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", outdata);
          if (outdata[0] == 0x57 && outdata[1] == 0xc3 
                  && b != 0x0a && c != 0x0a && d != 0x0a)
          {
              printf("rc4key = \"\\x%02x\\x%02x\\x%02x\\x%02x\"\n", d, c, b, a);
              return 1;
          }
        }
      }
    }
  }
  return 0;
}

For the shellcode, we must keep in mind the "killer" alarm() function, and make it fork. The size of the buffer is limited (and even further reduced by the fork), we have to use a stager.

The final packet to send is like [RC4 KEY]['\0'][shellcode fork][shellcode stager]['\n'] then [TAG][real shellcode].

No comments:

Post a Comment