• Total des pages vues: 1406
  • Pages vues aujourd'hui: 271
  • Visiteurs connectés: 4
  • Nombre de visiteurs: 808

Making ELF packer for fun and chocapicz

Written by aaSSfxxx - 07 may 2013

I recently decided to make an ELF packer, in order to learn some cool stuff about Linux kernel and ELF format, so I'll write 2 or 3 articles in this blog to explain some stuff I discovered.
To write this article, I use NASM and a x86 linux kernel (yeah guys, I'm still on a x86 archlinux). But before, let's listen to some music


In this article (and the next(s) which will follow), I decided to make a "real" packer as we can see in win32, i.e. a packer which replaces its memory image by the packed binary's image.
First of all, the packer has to unmap its sections from memory (to make some space to map the "real" binary), but if we do this, we'll execute code which will no longer exist, and it's not a good idea. So, we have to map memory elsewhere, copy our code into that space and continue execution from it to avoid problems. The code into the new memory space will just unmap the old binary, maybe uncompress/decrypt the real binary, and map it into the memory, and then jump to its entry point.
In fact, it's more difficult than this if the binary is dynamically-linked to other libraries, because we'll have to load the dynamic linker manually, and do a lot of extra stuff to get it working (contrary to win32 which loads ntdll.dll by default and does all init stuff in LdrInitializeThunk). So I'll talk about "standalone" or statically-linked binaries packing.

Some thoughts about ELF header

As you may probably know, linux binaries have an ELF header, which describes where is the entry point, how binary is mapped in memory, what are the sections and other stuff like this. Two components are interesting here for us: the ELF header itself, which will give to us the real entry point, and also the "Program Header" which describes how the binary has to be mapped into the memory. So, here are these structures:

Code C :
  2. /* ELF header */
  4. typedef struct {
  5.    unsigned char   e_ident[ELF_NIDENT];   /* magic number et al. */
  6.    u_int16_t       e_type;                /* type of file this is */
  7.    u_int16_t       e_machine;             /* processor type file is for */
  8.    u_int32_t       e_version;             /* ELF version */
  9.    u_int32_t       e_entry;           /* address of program entry point */
  10.    u_int32_t       e_phoff;           /* location in file of phdrs */
  11.    u_int32_t       e_shoff;           /* ignore */
  12.    u_int32_t       e_flags;           /* ignore */
  13.    u_int16_t       e_ehsize;          /* actual size of file header */
  14.    u_int16_t       e_phentsize;       /* actual size of phdr */
  15.    u_int16_t       e_phnum;           /* number of phdrs */
  16.    u_int16_t       e_shentsize;       /* ignore */
  17.    u_int16_t       e_shnum;           /* ignore */
  18.    u_int16_t       e_shstrndx;        /* ignore */
  19. } Elf32_Ehdr;
  21. /* Program header */
  23. typedef struct {
  24.    u_int32_t       p_type;      /* Type of segment */
  25.    u_int32_t       p_offset;    /* Location of data within file */
  26.    u_int32_t       p_vaddr;     /* Virtual address */
  27.    u_int32_t       p_paddr;     /* Ignore */
  28.    u_int32_t       p_filesz;    /* Size of data within file */
  29.    u_int32_t       p_memsz;     /* Size of data to be loaded into memory*/
  30.    u_int32_t       p_flags;     /* Flags */
  31.    u_int32_t       p_align;     /* Required alignment - can ignore */
  32. } Elf32_Phdr;
Here we just need the e_entry, e_phoff and e_phnum fields of the Elf32_Ehdr structure to have offset and number of Elf32_Phdr entries. For the Elf32_Phdr structure, we need to check if p_type is equal to PT_LOAD (we don't give a fuck about other segment types, they do not contain any useful information for us). If it's equal, we'll need p_offset, p_vaddr, p_filesz and p_memsz fields to have information about memory mapping.
And, unlike Win32 and PE header, nothing is aligned in ELF header, so we'll need to get our hands dirty and align everything by yourselves (the program excepts to have the byte of the file at the p_offset at the p_vaddr of the memory).

Btw, if you are interested, I wrote a NASM header to work with elf easier that you can download here.

Let's code baby !

The first "problem" to avoid, as I said in the first part is to copy the code into an empty section. So, we'll use some nasm magic, and do a code like this:

Code ASM :
  2. _start:
  4.    ;; mapping stuff (eax contains mapping addr)
  6.    mov ecx, (packer_end - packer_start)
  7.    mov esi, packer_start
  8.    mov edi, eax
  9.    rep movsb
  11.    jmp eax
  13. packer_start:
  14.    ;; some code and data
  15. packer_end:
We'll also need to know where all the data needed is stored in the mapping, which can be done by storing mapping base (contained in eax) into a local variable and adding data offset relative to packer_start to it.

Now, let's go deeper in the packer code. First of all, it saves the offset. Then it grabs its program header table offset and number of elements, multiplies the number of elements by the program header's size to have the number of bytes to allocate (because the original program header table will be unmapped by the program and we'll get a segfault if we try to access to unmapped memory, so we need to copy the program header table elsewhere).

Code ASM :
  1. do_work:
  2.     push ebp
  3.     mov ebp, esp
  4.     sub esp, 14h
  5.     mov [ebp-offset], eax ; save calculated offset
  6.     xor eax, eax
  7.     mov [ebp-dynamic], eax
  9.     ;; Gets offset and header
  11.     mov edx, 0x08048000
  12.     mov ebx, [edx+elf32_hdr.e_phoff]
  13.     add ebx, edx
  14.     mov esi, ebx
  15.     ; Calculates the right number of sections
  16.     movzx eax, word [edx+elf32_hdr.e_phnum]
  17.     movzx ecx, word [edx+elf32_hdr.e_phentsize]
  18.     push eax ; save number of sections
  19.     mov edx, ecx
  21.     mul cx
  22.     push eax ; save number of bytes to copy
  23.     add eax, 1000h
  24.     and eax, 0fffff000h
  25.     mov [ebp-tempsize], eax
Then it maps new memory space and copies the program header table in the new allocated memory.
Code ASM :
  1.     ;; Maps a section to contain self's program header
  3.     push 0  ; offset
  4.     push -1 ; fd
  5.     push MAP_PRIVATE | MAP_ANONYMOUS ; flags
  6.     push PROT_READ | PROT_WRITE ; protections
  7.     push eax ; calculated size
  8.     push 0   ; no adress
  9.     mov ebx, esp
  10.     mov eax, SYS_MMAP
  11.     int 80h ; syscall
  12.     add esp, 24
  13.     ; copy program headers
  14.     mov [ebp-tempmap], eax
  15.     mov edi, eax
  16.     pop ecx
  17.     rep movsb
  19.     ; Point to first program header
  20.     pop ecx
  21.     mov ebx, eax
It unmaps all the sections of the binary and unmaps the mapping just created once the job done.
Code ASM :
  1.     ;; unmap old ELF sections
  3.     .loop:
  4.         cmp dword[ebx+elf32_phdr.p_type], PT_LOAD
  5.         jnz .next
  6.         push ebx ; push program header offset
  7.         call unmap_stuff
  8.         .next:
  9.         add ebx, edx
  10.         dec ecx
  11.     jnz .loop
  13.     ;; Cleanup old mapping
  14.     mov ebx, [ebp-tempmap]
  15.     mov ecx, [ebp-tempsize]
  16.     mov eax, SYS_MUNMAP
  17.     int 0x80
Then, the packer grabs the original ELF binary (I didn't compress it for the PoC), reads its program header table, and maps the section if PT_LOAD is found. I also check if PT_INTERP exists, and if it's the case, we'll make the packer abort (because we don't map the dynamic linker in this article and without it, we can't do anything).
Code ASM :
  1.     mov edx, (packedbin-do_work)
  2.     add edx, [ebp-offset] ; get elf in memory
  3.     mov ebx, [edx+elf32_hdr.e_phoff]
  4.     add ebx, edx
  5.     movzx ecx, word[edx+elf32_hdr.e_phnum]
  7.     .loop2:
  8.         ; check if it's a loading information segment
  9.         cmp dword[ebx+elf32_phdr.p_type], PT_LOAD
  10.         jnz .no_load
  11.             push dword [ebx+elf32_phdr.p_flags] ; protections
  12.             push dword [ebx+elf32_phdr.p_memsz] ; virtual size
  13.             push dword [ebx+elf32_phdr.p_vaddr] ; virtual address
  14.             push dword [ebx+elf32_phdr.p_filesz] ; file size
  15.             mov eax, dword [ebx+elf32_phdr.p_offset] ; offset
  16.             add eax, edx
  17.             push eax
  18.             call fake_map
  19.         .no_load:
  20.         ; check if it's a dynamic section
  21.         cmp dword[ebx+elf32_phdr.p_type], PT_INTERP
  22.         jnz .no_dynamic
  23.             mov eax, [ebx+elf32_phdr.p_vaddr]
  24.             mov [ebp-dynamic], eax
  25.         .no_dynamic:
  26.         ; switch to next program header entry
  27.         add ebx, 32
  28.         dec ecx
  29.     jnz .loop2
  31.     mov eax, [ebp-dynamic]
  32.     test eax, eax
  33.     jnz .load_interp
  34.         ; program doesn't have PT_INTERP, jmp to its entry point
  35.         mov eax, [edx+elf32_hdr.e_entry]
  36.         jmp .gtfo
  37.     .load_interp:
  38.         ; we don't load interpreter for the moment, simply GTFO and abort.
  39.         jmp exit
  40.     .gtfo:
  41.     leave
  42.     jmp eax
Now, let's have a look about the "fake_map" function.

In the traditional way, Linux kernel opens a file descriptor to the ELF binary, and uses it to map the binary file at different offsets with different sizes (and different permissions of course) as described in the program header table. But, here, we don't have any file descriptor, and we'll have to emulate mapping by hand, what does the "fake_map" function. To simplify code and avoid mprotect syscall I set all the permissions on "rwx" (which is really UGLY, I know). Here is the code, I hope with enough comments:

Code ASM :
  1. %define offset 8
  2. %define size 0ch
  3. %define base 10h
  4. %define map_size 14h
  5. %define elf_flags 18h
  6. fake_map:
  7.     push ebp
  8.     mov ebp, esp
  9.     push ebx
  10.     push esi
  11.     push edi
  12.     push ecx
  14.     ; do the mmap  
  15.     push 0  ; offset
  16.     push -1 ; fd
  17.     push MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED ; flags
  18.     push PROT_READ | PROT_WRITE | PROT_EXEC ; permissions
  20.     ;; Align mapping size
  22.     mov eax, dword[ebp+map_size] ; mapping size
  23.     ; add padding to ELF
  24.     mov ebx, dword[ebp+base]
  25.     and ebx, 0xfff
  26.     add eax, ebx
  27.     ; align size to a page
  28.     add eax, 1000h
  29.     and eax, 0fffff000h
  30.     ;push new size
  31.     push eax
  33.     ;; Align base
  35.     mov eax, dword[ebp+base]
  36.     and eax, 0fffff000h
  37.     push eax  ; push base
  39.     mov ebx, esp
  40.     mov eax, SYS_MMAP
  41.     int 80h ; syscall
  42.     add esp, 24
  44.     ;; Copy the in-memory file into the section
  46.     mov edi, eax
  47.     ; align the offset
  48.     mov esi, [ebp+offset]
  49.     mov eax, [ebp+base]
  50.     and eax, 0fffh
  51.     sub esi, eax
  52.     mov ecx, [ebp+size]
  53.     add ecx, eax
  54.     rep movsb
  56.     pop ecx
  57.     pop edi
  58.     pop esi
  59.     pop ebx
  60.     leave
  61.     ret 14h

If you want to test by yourself, you can download the code here

Classified in : Hacking & Programming - Tags : none

Comments are closed.