Making ELF packer for fun and chocapicz (part 2)

Written by aaSSfxxx -

As promised, here is the second article about my ELF packer.
Here, I'll talk about dynamically-linked ELF (i.e. which has dependencies to ".so" modules), which is more tricky than the "basic" packer I showed before. The code is still NASM, and still under 32bit (feel free to rewrite the code to support 64-bit architecture ;))

Loading ld-linux

The first step needed here is loading the dynamic linker itself (because our asm program is standalone, so we don't have dynamic linker loaded at this time). To know which dynamic linker we have to use, let's read the filename contained in the "PT_INTERP" section.
Once the linker filename got, we have to map it into memory (and here, we can do it directly by opening the file and using the file descriptor returned, no need to "emulate" mapping). But, we have to take care of the "bss" section, which has to be zeroed after mapping of data section (the bss section is located at the end of file, according to Linux kernel code). I made a function which maps ld-linux.so into memory (quick and dirty code), and here is its code:

; This function (shortened to see the most interesting part) loads the interpreter into memory space
load_interp:
    push ebp
    mov ebp, esp
    sub esp, 1ch
    ; Zeroes some variables
    xor edx, edx
    mov [ebp-10h], edx
    mov [ebp-18h], edx
    mov [ebp-1ch], edx
    ; Get a FD to the dynamic loader
    xor ecx, ecx
    mov ebx, [ebp+8]
    mov eax, SYS_OPEN
    int 0x80
    test eax, eax
    jz exit
    mov [ebp-4], eax

    ; snipped code from previous article

    ; Start our job
    .loop:
        cmp dword[ebx+elf32_phdr.p_type], PT_LOAD
        jnz .next
            push ebx

            ;; push file offset

            mov eax, dword [ebx+elf32_phdr.p_offset] ; file offset
            ; remove page glitch to offset
            mov edx, dword [ebx+elf32_phdr.p_vaddr]
            and edx, 0xfff
            sub eax, edx     
            push eax ; push file offset

            ;; push file descriptor

            push dword [ebp-4] ; push fd

            ;; add "MAP_FIXED" flag if we already mapped something

            mov eax, MAP_PRIVATE | MAP_DENYWRITE ; flags
            cmp dword[ebp-10h], 0
            jz .no_fixed
            or eax, MAP_FIXED
            .no_fixed:
            push eax ; push flag

            ;; get read/write flags

            push ebx
            mov ebx, [ebx+elf32_phdr.p_flags]
            call get_map_prot
            pop ebx
            push eax ; push flags

            ;; push size of mapping

            mov eax, dword [ebx+elf32_phdr.p_memsz]
            ; add remainder
            mov edx, dword [ebx+elf32_phdr.p_vaddr]
            and edx, 0fffh
            add eax, edx
            ; round it up to 1 page
            add eax, 1000h
            and eax, 0fffff000h
            push eax

            ;; get RVA where to map it and add imagebase to it

            mov eax, dword [ebx+elf32_phdr.p_vaddr]
            add eax, [ebp-10h]
            and eax, 0fffff000h
            push eax ; voffset

            mov ebx, esp
            mov eax, SYS_MMAP
            int 0x80
            add esp, 24

            pop ebx

            ;save image base if not stored
            cmp dword[ebp-10h], 0
            jnz .nosaved
            mov [ebp-10h], eax
            mov ebx, eax
            add ebx, dword [ebx+elf32_hdr.e_phoff]
            .nosaved: 

            ;; Save elf_bss and map_bss as Linux kernel does

            ;save elf_bss if greater than stocked
            mov edx, [ebp-10h]
            add edx, dword[ebx+elf32_phdr.p_filesz]
            add edx, dword[ebx+elf32_phdr.p_vaddr]
            cmp dword[ebp-18h], edx
            ja .no_elf_bss
                mov [ebp-18h], edx
            .no_elf_bss:

            ;save map_bss if greater than stocked
            mov edx, [ebp-10h]
            add edx, dword[ebx+elf32_phdr.p_memsz]
            add edx, dword[ebx+elf32_phdr.p_vaddr]
            cmp dword[ebp-1ch], edx
            ja .no_map_bss
                mov [ebp-1ch], edx
            .no_map_bss:

        .next:
        add ebx, 32
        dec ecx
    jnz .loop
    ; Now, fill of zeros padding between last_bss and end of file
    mov ecx, [ebp-18h] ; grab elf_bss
    mov edi, ecx
    add ecx, 1000h
    and ecx, 0fffff000h ; align it to one page
    sub ecx, edi
    xor eax, eax
    rep stosb
    mov eax, [ebp-14h]
    add eax, [ebp-10h]
    leave
    ret 4

get_map_prot:
    xor eax, eax
    test ebx, PF_R
    jz .test_w
    or eax, PROT_READ
    .test_w:
    test ebx, PF_W
    jz .test_x
    or eax, PROT_WRITE
    .test_x:
    test ebx, PF_X
    jz .next
    or eax, PROT_EXEC
    .next:
    ret

This function returns the entry point of the dynamic linker, in order to be called after, and the function "get_map_prot" returns permissions according to section flags in ELF.

But, even if we map ld-linux, our job is not done here, there is extra initialization to do before.

Auxiliary vectors

When Linux kernel loads a executable file, it puts some stuff in the stack after envp (and of course, argc and argv). This zone is called "auxiliary vectors", because of its name "auxp" in ld-linux code. So, to have something working properly, we'll have to modify some of this auxiliary vectors, but we'll have to find them before. To do this, here is the code:

; Finds the auxp array (after endp)
find_auxp_array:
  lea eax, [ebp+8] ; ptr to argv
  mov ecx, [ebp+4]
  lea eax, [eax + 4*ecx] ;ptr to envp
  .loop:
    add eax, 4
    cmp dword[eax], 0
    jnz .loop
    add eax, 4
  ret

This code has to be called in the "main" function which has to have a stack frame, because it uses stack frame of main function to grab argument pointer. Then it lookups envp pointer array in odrer to locate auxp array.

An auxp element is a structure which is:

typedef struct
{
  int a_type;                   /* Entry type */
  union
    {
      long int a_val;           /* Integer value */
      void *a_ptr;              /* Pointer value */
      void (*a_fcn) (void);     /* Function pointer value */
    } a_un;
} Elf32_auxv_t;

Each auxiliary vector has an ID which specifies the type of data contained in the vector. For us, we'll need to update stuff related to ELF header, and more especially related to program header table (and also the entry point), because ld-linux uses these vectors. So, we'll need to modify AT_PHNUM and AT_ENTRY vectors (we don't change AT_PHDR, we assume program header is still at the same place), which is done here:

    call find_auxp_array
    ; change data in auxp vectors
    .auxp_loop:
        cmp dword[eax], 0
            jz .auxp_end ; jump if no auxp vectors
        cmp dword[eax], AT_PHNUM
        jnz .no_phnum
            movzx ebx, word[edx+elf32_hdr.e_phnum]
            mov dword[eax+4], ebx
        .no_phnum:
        cmp dword[eax], AT_ENTRY
        jnz .no_entry
            mov ebx, [edx+elf32_hdr.e_entry]
            mov dword[eax+4], ebx
        .no_entry:
        add eax, 8
        jmp .auxp_loop
    .auxp_end:
    ; program has PT_INTERP, map it into mem
    push dword[ebp-dynamic]
    call load_interp

Then, once all these initializations done, enjoy ur ELF packer :].

Source code of this can be found at this address