Shellcode/Loaders

Published on Mar 16, 2015

意译:Shellcode/Loaders

shellcode Loader被用在缓冲区溢出或者其它形式的二进制挖掘活动中测试shellcode。最好的方法嘛,构建从命令行参数的用户友好的loader,并且传递给刚分配的可执行内存空间。本文在x86指令集写x64汇编来在linux上构建这么个载入器。

32位的在文末给出

[TOC]

可执行loader

命令行参数

命令行参数入栈的顺序是:第二个参数,第一个参数,参数数目。因此,为了从参数获得shellcode,=pop rbx=三次。一旦完成,=rbx=将包含指向shellcode的指针:

BITS 64
global _start
_start:
    pop rbx ;argc
    pop rbx ; 参数列表指针
    pop rbx ; 指向第一个参数的指针

通过mmap()分配可执行内存区域

参考x64 syscall table,自己谷歌就好。

现代操作系统的栈默认并不可执行,但我们成功执行代码需要一个可执行栈。这可以通过mmap系统调用实现。

mmap()=的原型是(=man mmap):

void *mmap(void *addr, size_t length, int prot, int flags,
                 int fd, off_t offset);

在64位处理器上,函数调用如下:

function_call(%rax) = function(%rdi,  %rsi,  %rdx,  %r10,  %r8,  %r9)
              ^system          ^arg1  ^arg2  ^arg3  ^arg4  ^arg5 ^arg6
               call #

首先,=mmap()=的系统调用数(syscall number)放入=rax=:

push 0x9
pop rax

=mmap()=的第一个参数需要是=null=,所以=xor rdi rdi=。

xor rdi, rdi

指定缓冲区大小(4096字节或者0x1000字节) ,这个参数传给=rsi=

push rdi
pop rsi ; rsi = 0
inc rsi ; rsi = 1
shl rsi, 0xc  ; rsi=0x1000

第三个参数=prot=保存在=rdx=中,是内核权限标志(读、写、执行或无),对多个标志,它们用按位或方式合在一起,=PROTREAD|PROTWRITE|PROTEXEC=是数字=7=,所以直接在=rdx=中放入7就行。

push 0x7
pop rdx

接下来的参数=flag=跟=prot=类似,保存了内存映射标志。本例中设置为=MAPPRIVATE|MAPANONYMOUS=,其值为数字=0x22=。存储在=r10=中。

push 0x22
pop r10

最后两个参数应该为=null=,放到=r8=和=r9=中

push rdi
push rdi
pop r8
pop r9

万事具备,进行系统调用。

syscall

接下来=rax=中就包含指向缓冲区的指针,这个指针可以用来把shellcode拷贝进去。

拷贝代码到新内存区域

把=rsi=作为计数器,初始化为0:

inject:
    xor rsi, rsi

把=rdi=设为=null=,等下要把当前字节和=dil=(rdi低8位)中的值比较来确定shellcode的结束位置。

push rsi
pop rdi

如果拷贝到达shellcode末尾,则跳到=injectfinished=:

inject_loop:
    cmp [rbx + rsi * 1], dil
    je inject_finished

每个字节从=[rbx + rsi]=移动到=[rax + rsi]=,通过=r10b=(=r10=低八位)。

mov r10b, [rbx+rsi*1]
mov [rax+rsi*1], r10b

=rsi=作为偏移量和计数器:

inc rsi

继续循环

jmp inject_loop:

在=injectfinished=程序出附上=ret=操作符(opcode)=0xc3=

inject_finished:
    mov byte [rax+rsi*1], 0xc3

一般,操作符(opcode)指指令而字节码(bytecode)不仅包含操作符还包含参数,成为操作数(operand)。

返回代码

代码返回而不是跳转或被调用的原因在于,这更充分模拟了类似有漏洞应用在缓冲区溢出时的环境。有效载荷会返回,因此,当shellcode被加载后,它应该返回。

首先调用=rettoshellcode=。这会把=exit=的地址推入栈顶,于是shellcode结束后返回=exit=的地址。

call ret_to_shellcode

原始的返回地址被覆盖为为shellcode的地址,并且进入(returned into)

ret_to_shellcode:
    push rax
    ret

当shellcode结束时,将返回到=exit=函数优雅的退出

exit:
    push 60
    pop rax
    xor rdi, rdi
    syscall

执行loader

编译链接吧

 ~/Work/project/blackhat/shellcode  nasm -felf64 loader64.s -o loader64.o
 ~/Work/project/blackhat/shellcode  ld loader64.o -o loader64
 ~/Work/project/blackhat/shellcode  ./loader64 $(echo -en "\x48\x31\xff\x6a\x69\x58\x0f\x05\x57\x57\x5e\x5a\x48\xbf\x6a\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b\x58\x0f\x05")
[reverland@gentoo shellcode]$

这个shellcode来自之前启动一个shell的shellcode,注意提示符。

基于返回的载入器

基于返回的代码也能用载入器测试,而且更小,不需要分配内存。

_start:
    pop rbx
    pop rbx
    pop rsp ; rsp现在指向第一个参数
    ret

只是我觉得,似乎参数位置是不可执行的。关于ROP,以后说吧

最后还有些动态载入和动态socket载入器。当shellcode依赖有漏洞二进制程序上下文时包含一个链接的动态部分,这是啥我现在还不知道。。。。。。

下一篇Dynamic shellcode