Shell-Spawning Shellcode

shell-spawning_shellcode.html

Now that you've learned how to make system calls and avoid null bytes, all sorts of shellcodes can be constructed. To spawn a shell, we just need to make a system call to execute the /bin/sh shell program. System call number 11, execve(), is similar to the C execute() function that we used in the previous chapters.

Code View:

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 [arg]". 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 [arg] filename.
argv is an array of argument strings passed to the new program. envp
is an array of strings, conventionally of the form key=value, which are
passed 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[]).

The first argument of the filename should be a pointer to the string "/bin/sh", since this is what we want to execute. The environment array— the third argument—can be empty, but it still need to be terminated with a 32-bit null pointer. The argument array—the second argument—must be nullterminated, too; it must also contain the string pointer (since the zeroth argument is the name of the running program). Done in C, a program making this call would look like this:

To do this in assembly, the argument and environment arrays need to be built in memory. In addition, the "/bin/sh" string needs to be terminated with a null byte. This must be built in memory as well. Dealing with memory in assembly is similar to using pointers in C. The lea instruction, whose name stands for load effective address, works like the address-of operator in C.

Instruction

Description

lea ,

Load the effective address of the source operand into the destination operand.

With Intel assembly syntax, operands can be dereferenced as pointers if they are surrounded by square brackets. For example, the following instruction in assembly will treat EBX+12 as a pointer and write eax to where it's pointing.

89 43 0C mov [ebx+12],eax

The following shellcode uses these new instructions to build the execve() arguments in memory. The environment array is collapsed into the end of the argument array, so they share the same 32-bit null terminator.

After terminating the string and building the arrays, the shellcode uses the lea instruction (shown in bold above) to put a pointer to the argument array into the ECX register. Loading the effective address of a bracketed register added to a value is an efficient way to add the value to the register and store the result in another register. In the example above, the brackets dereference EBX+8 as the argument to lea, which loads that address into EDX. Loading the address of a dereferenced pointer produces the original pointer, so this instruction puts EBX+8 into EDX. Normally, this would require both a mov and an add instruction. When assembled, this shellcode is devoid of null bytes. It will spawn a shell when used in an exploit.

This shellcode, however, can be shortened to less than the current 45 bytes. Since shellcode needs to be injected into program memory somewhere, smaller shellcode can be used in tighter exploit situations with smaller usable buffers. The smaller the shellcode, the more situations it can be used in. Obviously, the XAAAABBBB visual aid can be trimmed from the end of the string, which brings the shellcode down to 36 bytes.

This shellcode can be shrunk down further by redesigning it and using registers more efficiently. The ESP register is the stack pointer, pointing to the top of the stack. When a value is pushed to the stack, ESP is moved up in memory (by subtracting 4) and the value is placed at the top of the stack. When a value is popped from the stack, the pointer in ESP is moved down in memory (by adding 4).

The following shellcode uses push instructions to build the necessary structures in memory for the execve() system call.

This shellcode builds the null-terminated string "/bin//sh" on the stack, and then copies ESP for the pointer. The extra backslash doesn't matter and is effectively ignored. The same method is used to build the arrays for the remaining arguments. The resulting shellcode still spawns a shell but is only 25 bytes, compared to 36 bytes using the jmp call method.

2. A Matter of Privilege

To help mitigate rampant privilege escalation, some privileged processes will lower their effective privileges while doing things that don't require that kind of access. This can be done with the seteuid() function, which will set the effective user ID. By changing the effective user ID, the privileges of the process can be changed. The manual page for the seteuid() function is shown below.

Code View:

SETEGID(2) Linux Programmer's Manual SETEGID(2)

NAME seteuid, setegid - set effective user or group ID

SYNOPSIS #include
#include
int seteuid(uid_t euid);
int setegid(gid_t egid);
DESCRIPTION
seteuid() sets the effective user ID of the current process.
Unprivileged user processes may only set the effective user ID to
ID to the real user ID, the effective user ID or the saved set-user-ID.
Precisely the same holds for setegid() with "group" instead of "user".
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is
set appropriately.

This function is used by the following code to drop privileges down to those of the "games" user before the vulnerable strcpy() call.

Fortunately, the privileges can easily be restored at the beginning of our shellcode with a system call to set the privileges back to root. The most complete way to do this is with a setresuid() system call, which sets the real, effective, and saved user IDs. The system call number and manual page are shown below.

This way, even if a program is running under lowered privileges when it's exploited, the shellcode can restore the privileges. This effect is demonstrated below by exploiting the same program with dropped privileges.

3. And Smaller Still

A few more bytes can still be shaved off this shellcode. There is a single-byte x86 instruction called cdq, which stands for convert doubleword to quadword. Instead of using operands, this instruction always gets its source from the EAX register and stores the results between the EDX and EAX registers. Since the registers are 32-bit doublewords, it takes two registers to store a 64-bit quadword. The conversion is simply a matter of extending the sign bit from a 32-bit integer to 64-bit integer. Operationally, this means if the sign bit of EAX is 0, the cdq instruction will zero the EDX register. Using xor to zero the EDX register requires two bytes; so, if EAX is already zeroed, using the cdq instruction to zero EDX will save one byte

31 D2 xor edx,edx

compared to

99 cdq

Another byte can be saved with clever use of the stack. Since the stack is 32-bit aligned, a single byte value pushed to the stack will be aligned as a doubleword. When this value is popped off, it will be sign-extended, filling the entire register. The instructions that push a single byte and pop it back into a register take three bytes, while using xor to zero the register and moving a single byte takes four bytes

31 C0 xor eax,eaxB0 0B mov al,0xb

compared to

6A 0B push byte +0xb58 pop eax

These tricks (shown in bold) are used in the following shellcode listing. This assembles into the same shellcode as that used in the previous chapters.

The syntax for pushing a single byte requires the size to be declared. Valid sizes are BYTE for one byte, WORD for two bytes, and DWORD for four bytes. These sizes can be implied from register widths, so moving into the AL register implies the BYTE size. While it's not necessary to use a size in all situations, it doesn't hurt and can help readability.