Shellcode/Dynamic

Published on 3月 17, 2015

意译:Shellcode/Dynamic

动态shellcode是自链接的shellcode,用来规避多种主机层面的防护措施,比如主机入侵检测系统(HIDS)或者主机入侵防护系统(HIPS)。这些措施能阻止传统的null-free shellcode。通过动态shellcode技术,实现比如不包含中断、系统调用或者明文函数字符串等。

[TOC]

评价

大多安全设施组件都基于RAM的数据和标记为可执行的内容进行运行时分析。而且,许多系统甚至从内核检查内核中断和系统调用(linux审计工具audit就做这个)。其它也许监视ld-linux中提供给普通应用使用共享库调用的=ldruntimeresolve=的运行,=dlfixup()=的蹦床(trampoline,确定不是函数式编程????)等。当应用尝试执行不在它们=.text=段的系统调用或中断、或者尝试使用=ldruntimeresolve=,=dlfixup=,=dlopen=,=dlclose=或者=dlsym=来导入一个不在它导入表(import talbe)的函数时,会触发许多安全系统的警告。另外,使用比如像=dlopen()=和=dlsym()=这样的函数需要使用明文字符串。任何一般的分析都能很容易迅速逆向有效载荷,这是传统null-free shellcode的另一个问题。

一个动态shellcode引擎能够解决这些问题。通过避免C调用惯例使用的寄存器,它可以构建允许开发者写出动态自链接代码的链接器(linker)。于是完全不再需要中断或者系统调用,因为链接器能不倚靠操作系统导入函数。另外,函数哈希被用作阻止函数名通过字符串呈现,解决了上面标准null-free shellcode有的问题。

C调用惯例的影响

通常的系统调用格式或者libc函数调用:

    function_call(%rax) = function(%rdi,  %rsi,  %rdx,  %r10,  %r8,  %r9)

返回值通常置于=rax=中,然而当结构指针被作为参数传递时,在那个参数寄存器中一个指向更改过的结构的指针被返回。

以上陈述显示:写一个链接器时,以下寄存器在没有系统调用的调用之前,不必为函数调用保存。

    %rax, %rbx, %rcx, %rbp, %r11, %r12, %r13, %r14, %r15

大多数寄存器能更改或者被各种libc函数更改,然而=rbx=在libc中被保留为开发者使用。当写一个动态链接器时,函数参数必须被保留,这样开发者能轻易写出动态集成的代码。最后,链接器取=rbx=作为库的基址指针,=rbp=用来哈希函数。这确保了开发者能保持对=rax=,=rdi=,=rsi=,=rdx=,=r10=,=r8=和=r9=的控制。=rcx=寄存器被用来作为指向调用函数标签的指针,也许应在函数调用间被保留。

函数哈希

这个功能希望=rdx=是0,=rsi=中是指向字符串的指针。接着它完成字符串的单向32位哈希并保存在=rsi=中。

首先,把被哈希程序(hasher)使用的不是=rsi=的寄存器保留:

    calc_hash:

    preserve_regs:
        push rax
        push rdx

=rdx=作为调用哈希程序的代码的零寄存器(zreg/zero register)。可以指通过简单的=push/pop=把=rax=置零来:

    initialize_regs:
        push rdx
        pop rax

接着,DF位(directional flag)被清空。这很重要,因为接下来的哈希过程使用了=lodsd=,而有漏洞应用的DF位不确定。

    cld

接着,=al=中的字节和=edx=相加,结果存入=edx=。左移12位(0xc),当=lodsd=载入的字节是null时,哈希值就计算完毕了。

    calc_hash_loop:
        lodsb
        rol edx, 0xc
        add edx, eax
        test al, al
        jnz calc_hash_loop

接着使用push和pop把哈希置入=rsi=:

    calc_done:
        push rdx
        pop rsi

最后,恢复保存到寄存器

    restore_regs:
        pop rdx
        pop rax

遍历到GOT的动态节(dynamic section)

当前执行进程的动态节程序头总是在VMA(Virtual Memory Adress,虚拟内存地址)=0x00400130=。以下是个没有=\x00=(null-free)的版本:

    _start:
        push 0x400130ff
        pop rbx
        shr ebx, 0x8

指向动态节的指针被抽取,长度被添加到动态节的长度上。GOT(Global Offset Table,全局偏移表)刚好就在动态节后面。通过以这种方式计算偏移量,可以不必从文件头中读取GOT的位置来遍历GOT。这有无数的好处。(译者:不知道有啥好处。。。)

    fast_got:
        mov rcx, [rbx]
        add rcx, [rbx+0x10]

抽取一个库指针

这个代码从GOT抽取个指向libc中任意函数的指针。比如在=rcx+0x18=地方,有指向=dlruntimeresolve=的指针。

    extract_pointer:
        mov rbx, [rcx+0x20]

现在寻找想要导入的二进制文件的基指针,首先寻找=\x7fELF=。因为RAM倒着保存信息,使用逆向比较来决定何时逆向循环。

    find_base:
        dec rbx
        cmp [rbx], 0x464c457f
        jne find_base

用户定义代码

现在基指针被计算出来,该载入开发者或用户的代码了。为了让调用函数(invoke_function)从寄存器中可重用,通过getPC来把调用函数的地址存入=rcx=。

    jmp startup

    __initialize_world:
        pop rcx
        jmp _world

    startup:
        call __initialize_word

    invoke_function:
        ...
    _world:
        ; user-defined code goes here

接口

这里开发的运行时链接器能让用户自定的代码从=world=开始。这个接口让开发者能提供函数哈希到=rbp=并且执行=call [rcx]=代替系统调用。这个例子描述了从内核调用exit(0)到使用链接器的API来调用exit(0)的过程。

以未链接的exit形式开始:

    exit:
        push 0x3c
        pop rax
        xor rdi, rdi
        syscall

哈希=exit=(上面的相加右移)得到=0x696c4780=

     ✘  ~/Work/project/blackhat/shellcode  cat hash-generator.s 
    BITS 64

    global _start
    _start:
        jmp startup

    calc_hash:
    ; accept rsi hold function name.
    ; rdx=0 first
    ; return hash in rsi
    ; use rax, rdx, rsi
        ; preserve rax&rdx
        push rax    ; use as accum
        push rdx    ; zero register

        initialize_regs:
            push rdx
            pop rax ;rax = 0
            cld; clear zf for lodsb

            calc_hash_loop:
                lodsb   ; load one byte from rsi to al
                rol edx, 0xc    ;right shift 12bits
                add edx, eax    ;add eax to edx
                test al, al     ; if al='\0'
                jnz calc_hash_loop

        calc_done:
            push rdx
            pop rsi ; move hash in rdx to rsi

        pop rdx
        pop rax ; restore rdx&rax
    ret

    startup:
        pop rax ; pointer to calc_hash
        pop rax ; argc
        pop rsi ; pointer to argv[]

        xor rdx, rdx    ;rdx=0
        call calc_hash

        push rsi    ; save hash on stack
        mov rsi, rsp    ; rsi hold pointer to hash now

        push rdx    ; null
        mov rcx, rsp    ; rcx hold pointer to null now

        mov rdi, 0x4
        loop:
            ; 倒着复制的
            dec rdi
            mov al, [rsi+rdi*1]
            mov [rcx+rdx*1], al
            inc rdx
            cmp rdi, 0  ; gas 竟然不能cmp %rdi, $0....但可以倒过来
            jnz loop

        mov rsi, rcx    ;rsi hold pointer to reverse hash
        inc rdi ; rdi = 1
        mov rax, rdi    ; rax = 1
        syscall         ; write(1, reverse hash)

        mov rax, 0x3c   ; rax=60
        dec rdi         ; rdi=0
        syscall         ; exit(0)
     ~/Work/project/blackhat/shellcode  nasm -felf64 hash-generator.s -o hash-generator.o
     ~/Work/project/blackhat/shellcode  ld hash-generator.o -o hash-generator
     ~/Work/project/blackhat/shellcode  ./hash-generator exit|hexdump -C
    00000000  69 6c 47 80                                       |ilG.|
    00000004

所以,=world=这么写

    _world:
        push 0x696c4780
        pop rbp ; 正好倒过来,看看hash-generator.s的代码
        xor rdi, rdi
        call [rcx]

开发者应该记着当调用那些可能改变寄存器的调用函数时保存=rcx=。或者通过更改=_initializeworld=中pop到的寄存器来移除限制。

调用的函数

这个注释是为了防止开发者忘记接口功能:

    ;
    ;  Takes a function hash in %rbp and base pointer in %rbx
    ;  >Parses the dynamic program headers of the ELF64 image
    ;  >Uses ROP to invoke the function on the way back to the
    ;  -normal return location
    ;
    ;  Returns results of function to invoke.
    ;

所有和libc交互的寄存器和任何可能被链接器使用的寄存器必须被保留,这样它们才能在函数调用时被恢复,=rbp=寄存器被保留两次。这时因为第一次保留在返回前被指向目的函数的指针覆盖。这让shellcode从目的函数返回到开发者定义的函数。

    invoke_function:
        push rbp
        push rbp
        push rdx
        push rdi
        push rax
        push rbx
        push rsi

将=rdx=赋为0,吧函数哈希放入=rdi=来进行将来的比较

    set_regs:
        xor rdx, rdx
        push rbp
        pop rdi

然后目的库导入的基址指针就放入=rbp=

    copy_base:
        push rbx
        pop rbp

需要读取=[rbx+0x130]=四字节,但是添加到八字节的寄存器。

    read_dynamic_section:
        push 0x4c
        pop rax
        add rbx, [rbx + rax * 4]

找到函数导出表,一般叫做=.dynsym=,或者动态符号表。通过遍历头检查动态节的类型。

    check_dynamic_type:
        add rbx, 0x10
        cmpb [rbx], 0x5
        jne check_dynamic_type

一旦ebx指向程序头中正确的位置;放置字符串表的绝对地址到=rax=和动态符号表的绝对地址到=rbx=。

    string_table_found:
        mov rax, [rbx+0x8]  ; rax是动态字符串表的地址
        mov rbx, [rbx+0x18] ; rbx是指向符号表的地址

接着,增加到下一个导出,指向字符串的指针被放入=rsi=来哈希

    check_next_hash:
        add rbx, 0x18   ;下一个条目
        push rdx
        pop rsi
        xor si, [rbx]
        add rsi, rax

=calchash=标签被如上描述方式调用来哈希函数名。

    calc_hash:
        ...

比较当前导出表的函数哈希和想要导出的函数哈希,如果不匹配则跳到下一次导入:

    check_current_hash:
        cmp edi, esi
        jne check_next_hash

一旦哈希被找到,它的函数偏移位于=[rbx+0x8]=四字节。=rdx=被用来作为零寄存器来得到没有=\x00=的四字节。FIXME(not so) 添加到=rbp=基址指针:

    found_hash:
        add rbp, [rbx+4*rdx+0x8]

这里,第一个例子中被保留的=rbp=被目的函数的地址覆盖。

    mov [esp+0x30], rbp

最后恢复所有寄存器。

        pop rsi
        pop rbx
        pop rax
        pop rdi
        pop rdx
        pop rbp
    ret

跳到目的函数代码。

动态shell

一旦添加链接器,一个115字节的socket重用载荷就变成了268字节的动态载入版本。这里有几种优化的方式,作为读者的练习。。。我得回头看看。。。

算了,我先看看Load-time relocation of shared libraries

令上午承蒙翔哥内推,刚填了简历,下午竟然就给我打电话电面了。。。然后就是强行谈及二进制安全被血虐最后被鄙视的过程,哈哈哈。

慢慢看,不把安全作为工作也许是种幸福呢。

毕业前:

  • 游戏

兴趣:

  • 统计学习
  • 二进制安全

工作: - ?