PlaidCTF - Pound 290

by
marcof + jinblack
on May 23, 2016
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:

defread_tweet():print"Read the top 20 tweets by Trump!"print"Enter a number (1 - 20)"tweet_number=raw_input()time.sleep(5)try:withopen("tweets/{0}".format(tweet_number),'r')asf:printf.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:

defrun_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:")iflen(input1)>3orlen(input2)>3:print"Number has to be less than 1000"returnstr_to_hash="[]{0}[]{1}##END".format(input1,input2)sim_id=hashlib.sha256(str_to_hash).hexdigest()sim_name="sims/sim-{0}".format(sim_id)ifos.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])ifret!=0:print"Compiler error!"returnos.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

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:

voidpropagate_forward(intk){// Somewhere total_length will be used :), with some buffer or heap
intlength_diff=L2-L1;inti,j;for(i=0;i<L1-1;i++){// At random, swap money to keep circulation of money
if(rand()%2){inttmp=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):

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:

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

We need the libc used by the remote binary to calculate system_in_libc_at_runtime

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:

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:

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

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.