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 ;))
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.
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