Linux的Shellcode "Hello, World!"

29

我有以下可用的NASM代码:

global _start

section .text

_start:
    mov eax, 0x4
    mov ebx, 0x1
    mov ecx, message
    mov edx, 0xF
    int 0x80

    mov eax, 0x1
    mov ebx, 0x0
    int 0x80

section .data
    message: db "Hello, World!", 0dh, 0ah

以下是包含上述 NASM 目标代码的 C 封装器,其输出为 "Hello, World!\n":

char code[] =
"\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"
"\xb9\x00\x00\x00\x00"
"\xba\x0f\x00\x00\x00"
"\xcd\x80\xb8\x01\x00"
"\x00\x00\xbb\x00\x00"
"\x00\x00\xcd\x80";

int main(void)
{
    (*(void(*)())code)();
}

然而当我运行代码时,似乎汇编代码没有执行,但程序正常退出。有什么想法吗?

谢谢

2个回答

81
当您注入此shellcode时,您不知道message中有什么内容:
mov ecx, message

在注入的进程中,它可以是任何东西,但它不会是"Hello world!\r\n",因为它在数据段中,而你只转储了文本段。你可以看到你的shellcode没有"Hello world!\r\n"
"\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"
"\xb9\x00\x00\x00\x00"
"\xba\x0f\x00\x00\x00"
"\xcd\x80\xb8\x01\x00"
"\x00\x00\xbb\x00\x00"
"\x00\x00\xcd\x80";

这是shellcode开发中常见的问题,解决方法如下:
global _start

section .text

_start:
    jmp MESSAGE      ; 1) lets jump to MESSAGE

GOBACK:
    mov eax, 0x4
    mov ebx, 0x1
    pop ecx          ; 3) we are poping into `ecx`, now we have the
                     ; address of "Hello, World!\r\n" 
    mov edx, 0xF
    int 0x80

    mov eax, 0x1
    mov ebx, 0x0
    int 0x80

MESSAGE:
    call GOBACK       ; 2) we are going back, since we used `call`, that means
                      ; the return address, which is in this case the address 
                      ; of "Hello, World!\r\n", is pushed into the stack.
    db "Hello, World!", 0dh, 0ah

section .data

现在转储文本部分:
$ nasm -f elf shellcode.asm
$ ld shellcode.o -o shellcode
$ ./shellcode 
Hello, World!
$ objdump -d shellcode

shellcode:     file format elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:   e9 1e 00 00 00   jmp    8048083 <MESSAGE>

08048065 <GOBACK>:
 8048065:   b8 04 00 00 00   mov    $0x4,%eax
 804806a:   bb 01 00 00 00   mov    $0x1,%ebx
 804806f:   59               pop    %ecx
 8048070:   ba 0f 00 00 00   mov    $0xf,%edx
 8048075:   cd 80            int    $0x80
 8048077:   b8 01 00 00 00   mov    $0x1,%eax
 804807c:   bb 00 00 00 00   mov    $0x0,%ebx
 8048081:   cd 80            int    $0x80

08048083 <MESSAGE>:
 8048083:   e8 dd ff ff ff   call   8048065 <GOBACK>
 8048088:   48               dec    %eax                    <-+
 8048089:   65               gs                               |
 804808a:   6c               insb   (%dx),%es:(%edi)          |
 804808b:   6c               insb   (%dx),%es:(%edi)          |
 804808c:   6f               outsl  %ds:(%esi),(%dx)          |
 804808d:   2c 20            sub    $0x20,%al                 |
 804808f:   57               push   %edi                      |
 8048090:   6f               outsl  %ds:(%esi),(%dx)          |
 8048091:   72 6c            jb     80480ff <MESSAGE+0x7c>    |
 8048093:   64               fs                               |
 8048094:   21               .byte 0x21                       |
 8048095:   0d               .byte 0xd                        |
 8048096:   0a               .byte 0xa                      <-+

$

我标记的那些行是我们的"Hello, World!\r\n"字符串:

$ printf "\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a"
Hello, World!

$ 

因此,我们的C封装将是:

char code[] = 

    "\xe9\x1e\x00\x00\x00"  //          jmp    (relative) <MESSAGE>
    "\xb8\x04\x00\x00\x00"  //          mov    $0x4,%eax
    "\xbb\x01\x00\x00\x00"  //          mov    $0x1,%ebx
    "\x59"                  //          pop    %ecx
    "\xba\x0f\x00\x00\x00"  //          mov    $0xf,%edx
    "\xcd\x80"              //          int    $0x80
    "\xb8\x01\x00\x00\x00"  //          mov    $0x1,%eax
    "\xbb\x00\x00\x00\x00"  //          mov    $0x0,%ebx
    "\xcd\x80"              //          int    $0x80
    "\xe8\xdd\xff\xff\xff"  //          call   (relative) <GOBACK>
    "Hello wolrd!\r\n";     // OR       "\x48\x65\x6c\x6c\x6f\x2c\x20\x57"
                            //          "\x6f\x72\x6c\x64\x21\x0d\x0a"


int main(int argc, char **argv)
{
    (*(void(*)())code)();

    return 0;
}

让我们来测试一下,使用 -z execstack 来启用读取即执行(尽管名称中有“堆栈”,但是是进程级别的),这样我们就可以在 .data.rodata 部分执行代码:

$ gcc -m32 test.c -z execstack -o test
$ ./test 
Hello wolrd!

它能够工作。在64位系统上,也需要使用-m32。因为32位ABI的int $0x80无法处理像PIE可执行文件中的.rodata这样的64位地址。此外,机器码是针对32位汇编的。虽然同样的字节序列在64位模式下会解码成等效的指令,但并不总是如此。

现代GNU ld.rodata放在与.text不同的段中,因此它可以是非可执行的。过去只需使用const char code[]即可将可执行代码放置在只读数据页面中。至少对于不想修改自身的shellcode而言,这已经足够了。


3
不确定为什么你没有得到任何赞,但这是一个很棒的答案。感谢你的帮助。 - Mr. Shickadance
4
如果你使用一个需要以空字符结尾的字符串函数,比如strcpy等字符串函数,那么空字节就会成为问题,它不会读取整个shellcode字符串。否则的话就没问题了。 - user1129665

21

正如BSH所提到的,你的shellcode中不包含消息字节。在定义msg字节之前跳转到MESSAGE标签并调用GOBACK例程是一个好方法,因为msg的地址将作为返回地址位于堆栈顶部,可以弹出到存储msg地址的ecx寄存器中。

但是你和BSH的代码都有一个小问题。它们包含NULL字节(\x00),当函数指针引用时会被视为字符串结束符。

有一种聪明的方法可以解决这个问题。你存储到eax、ebx和edx中的值足够小,可以通过直接访问al、bl和dl将它们一次性写入相应寄存器的低半字节中。上半字节可能包含垃圾值,因此可以执行异或运算。

b8 04 00 00 00 ------ mov $0x4,%eax


变成

b0 04          ------ mov $0x4,%al
31 c0          ------ xor    %eax,%eax


与先前的指令集不同,新的指令集不包含任何NULL字节。
因此,最终程序看起来像这样:
global _start

section .text

_start:
jmp message

proc:
    xor eax, eax
    mov al, 0x04
    xor ebx, ebx
    mov bl, 0x01
    pop ecx
    xor edx, edx
    mov dl, 0x16
    int 0x80

    xor eax, eax
    mov al, 0x01
    xor ebx, ebx
    mov bl, 0x01   ; return 1
    int 0x80

message:
    call proc
    msg db " y0u sp34k 1337 ? "

section .data

汇编和链接:

$ nasm -f elf hello.asm -o hello.o
$ ld -s -m elf_i386 hello.o -o hello
$ ./hello
 y0u sp34k 1337 ? $ 

现在从hello二进制文件中提取shellcode:
$ for i in `objdump -d hello | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\\x$i" ; done

输出:

\xeb\x19\x31\xc0\xb0\x04\x31\xdb\xb3\x01\x59\x31\xd2\xb2\x12\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xb3\x01\xcd\x80\xe8\xe2\xff\xff\xff\x20\x79\x30\x75\x20\x73\x70\x33\x34\x6b\x20\x31\x33\x33\x37\x20\x3f\x20

现在我们可以编写驱动程序来运行shellcode。
#include <stdio.h>

char shellcode[] = "\xeb\x19\x31\xc0\xb0\x04\x31\xdb"
                   "\xb3\x01\x59\x31\xd2\xb2\x12\xcd"
                   "\x80\x31\xc0\xb0\x01\x31\xdb\xb3"
                   "\x01\xcd\x80\xe8\xe2\xff\xff\xff"
                   "\x20\x79\x30\x75\x20\x73\x70\x33"
                   "\x34\x6b\x20\x31\x33\x33\x37\x20"
                   "\x3f\x20";


int main(int argc, char **argv) {
    (*(void(*)())shellcode)();
    return 0;
}

现代编译器中有一些安全功能,比如NX保护,可以防止在数据段或堆栈中执行代码。因此,我们应该明确指定编译器禁用这些功能。
$ gcc -g -Wall -fno-stack-protector -z execstack launcher.c -o launcher

现在可以调用launcher来启动shellcode。
$ ./launcher
 y0u sp34k 1337 ? $ 

对于更复杂的shellcode,还会有另一个障碍。现代Linux内核有ASLR地址空间布局随机化,您可能需要在注入shellcode之前禁用它,特别是当通过缓冲区溢出时。

root@localhost:~# echo 0 > /proc/sys/kernel/randomize_va_space 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接