Sunday, January 13, 2013

About Shellcodes in C

This is a follow up of our previous introductory post about shellcodes. Here we aim for coding more complex shellcodes directly in C. We'll mostly use default tools like gcc and as, at the end also a small python script to reorder and pack things. We'll play with linux but the concepts and scripts posted here can also be used to generate Windows shellcode (both 32 and 64 bits).

Suppose we want to recreate the "Hello World!" shellcode but in C using functions and declaring global strings and stuff like that. Normal relaxed plain C like this..

  1. int write(int fd, void* buffer, unsigned int size){
  2.     int ret;
  3.     asm volatile (
  4.         "movl $4, %%eax\n\t"
  5.         "movl %1, %%ebx\n\t"
  6.         "movl %2, %%ecx\n\t"
  7.         "movl %3, %%edx\n\t"
  8.         "int $0x80"
  9.         : "=a"(ret)
  10.         : "g"(fd), "g"(buffer), "g"(size)
  11.         : "%ebx"
  12.     );
  13. return ret;
  14. }
  15. char message[] = "HOLA MUNDO!(shellcode)\n";
  16. int shellcode(){
  17.     write(1,message,sizeof(message));
  18. }

Well not that relaxed. In a shellcode environment we don't have libc nor any other function ready to use. We have 2 ways to mitigate this:

  1. Perform system calls directly  int 80/systenter  (works only for POSIX)
  2. Solve and use already loaded api libraries (preferred in win32)

In our linux example we use software interruption 0x80 to make system calls directly. System call number 4 is the write system call in any linux. In order to do the same in Windows we need to do some other magic to learn the address of some system DLLs and then search for the desired function.

Compiling the C shellcode

Let's use gcc to compile our C code into object code. 

$ gcc -m32 -march=i386 -fno-stack-protector -fno-common -Os -fomit-frame-pointer -fno-PIC -c -static shellcode.c

We have added some command line arguments to simplify a bit the generated object code. We can inspect shellcode.o with objdump, it has two interesting sections: .text and .data.

$ objdump -s shellcode.o 

shellcode.o:     file format elf32-i386

Contents of section .text:

 0000 53b80400 00008b5c 24088b4c 240c8b54  S......\$..L$..T
 0010 2410cd80 5bc353b8 04000000 bb010000  $...[.S.........
 0020 00b90000 0000ba18 000000cd 805bc3    .............[. 
Contents of section .data:
 0000 484f4c41 204d554e 444f2128 7368656c  HOLA MUNDO!(shel
 0010 6c636f64 65290a00                    lcode)..        

and one relocation; which is the message section .data referenced from the code in section .text.

$ objdump -r shellcode.o 

shellcode.o:     file format elf32-i386


OFFSET   TYPE              VALUE 
00000022 R_386_32          message

So we can't simply extract, concatenate in memory and jump to it because the code won't run as you may expect. To be able to fix our single relocation example we need to choose a memory layout, put every section in memory and then modify the address at .text+0x22 so it points to the message at .data+0x00. Obviously we need to sort this things out at runtime; we do it with a small loader in assembler.

The loader

To fix the relocations we need to know the starting address of every section in memory. We can't force or estimate in advance where the shellcode is going to be placed so the addresses of each section must be solved at runtime. That's easy to do in x86.

  1. .section .text
  2.     #esp shall point to writeable memory
  3.     jmp labelA
  4. dummy:
  5.     jmp begin
  6. labelA:
  7.     call dummy
  8. begin:
  9.     #read eip into esi
  10.     popl %esi

We take the address of our code from the stack issuing a dummy function call. We still have to do some offsets calculations to reach the start of each section. Lets layout the sections in this way:


The metadata contains the offsets of every relocation in the following sections. As loader knows where is itself in memory, it can find and process the metadata and fix the relocations. Of course the metadata must be previously generated using the information in the object file. We'll see how to build it with a small python script in a while, meanwhile let's agree in its format:


Where N is the number of relocations, RELOCi is the offset of the relocation number i (from the beginning of .text) and START is the offset of the entry point. The remaining of the loader will fix the relocations and jump to the entry point.

  1.     subl $(begin-relocs), %esi  #esi points to [METADATA]
  3.     movl (%esi), %ecx           #Number of relocations N
  4.     leal 8(%esi,%ecx,4), %edi   #Calculate the start of .text
  5.     andl %ecx,%ecx
  6.     jz done
  7. fix_reloc:
  8.     movl (%esi,%ecx,4), %eax    #Get a relocation offset
  9.     addl %edi, (%edi,%eax,1)    #Add the start of .text
  10.     dec %ecx
  11.     jne fix_reloc
  12. done:
  13.     addl -4(%edi), %edi         #START plus the start of .text
  14.     jmp *%edi                   #gets the entry point
  15. relocs:

Now we need to prepare an ad-hoc metadata section from the object file and concatenate everything in an opaque chunk of data.

The python script:

To handle the object file (which is in fact an ELF file) we use pyelftools as in the previous shellcode post. This script must do 3 simple tasks:
  1. Extract and concatenate every important section. 
  2. Parse the relocations and symbols from the ELF and build the metadata chunk
  3. Prepend the loader binary
Thanks to the pyelftools, accessing the ELF format is relatively simple. The only tricky part is to understand how relocations work. I wont go too deep in this subject because anyway you'll forget about it in a minute (as I did (that's the real why)). Basically there are place holders in the different sections that point to potentially other sections using symbols. In the resulting shellcode we put one section after the other and recalculate all the relocations in terms of offsets to the first byte of .text section in the shellcode. Then we put all this offsets in the metadata format so the loader task is minimal. Our script only accepts x86(32 bit) ELFs. To handle 64 bit or another architecture we need to also code a different loader.
You may check out the complete script from here.

Try it...

Step 1 - Code your C shellcode

Mostly your thing, just remember not to use anything outside your project and to define the shellcode() funtion. Wild idea, in Linux you may consider using uClibc. An example file here.

Step 2 - Compile your code

You should use a command line like the following:

$ gcc -m32 -march=i386 -fno-stack-protector -fno-common -Os -fomit-frame-pointer -fno-PIC -c -static shellcode.c

Check out gcc manpage for details. Mandatory: -c -fno-common -fno-PIC -static

Step 3 - Run it through our mkShellcode script

$ python shellcode.o shellcode.bin

Object file:

Step 4 - Test it

Use the test.c. It will load the file passed as argument in an executable memory and pass the control to it.
$ gcc -m32 test.c -o test32
$ ./test32 shellcode.bin 
Passing control to the shellcode...
HOLA MUNDO!(shellcode)
The shellcode has returned to main!

No comments:

Post a Comment