Skip to content

Latest commit

 

History

History
1463 lines (903 loc) · 33.5 KB

File metadata and controls

1463 lines (903 loc) · 33.5 KB

三、Linux 中的汇编语言

在本章中,我们将讨论 Linux 中的汇编语言编程。我们将学习如何构建自己的代码。汇编语言是一种低级编程语言。低级编程语言是依赖于机器的编程,是计算机理解的最简单的形式。在汇编中,您将处理计算机体系结构组件,如寄存器和堆栈,这与大多数高级编程语言(如 Python 或 Java)不同。此外,汇编语言不是一种可移植语言,这意味着每种汇编编程语言都特定于一种硬件或一种计算机体系结构;例如,英特尔有自己特定的汇编语言。我们正在学习汇编,不是为了构建一个复杂的软件,而是为了构建我们自己的定制 Shellcode,所以我们将使它变得非常简单和简单。

我保证,在本章之后,你将以不同的方式看待每个程序和过程,你将能够理解计算机是如何真正执行你的指令的。让我们开始吧!

汇编语言代码结构

在这里,我们将不讨论语言结构,而是讨论代码结构。你还记得内存布局吗?

让我们再看一看:

我们将把我们的可执行代码放在.text部分,把我们的变量放在.data部分:

让我们进一步了解一下堆栈。堆栈为LIFO,表示最后一次输入第一次输出,因此它不是随机访问,而是使用推送和弹出操作。推就是把东西推到堆栈的顶部。让我们看一个例子。假设我们有一个堆栈,它只包含0x1234

现在,让我们使用组件push 0x5678将某些内容推入堆栈。此指令将值0x5678推入堆栈,这将改变堆栈指针指向0x5678

现在,如果我们想从堆栈中获取数据,我们使用一条pop指令,它将提取推入堆栈的最后一个元素。因此,采用相同的堆栈布局,让我们使用pop rax提取最后一个元素,它将提取值0x5678并将其移动到RAX寄存器:

这很简单!!

我们将如何在 Linux x64 上编写程序集?其实很简单,;你记得系统调用吗?这就是我们将如何通过调用系统命令来执行我们想要的。例如,如果我想退出一个程序,那么我必须使用exit系统调用。

首先,这个文件/usr/include/x86_64-linux-gnu/asm/unistd_64.h包含 Linux x64 的所有系统调用。让我们搜索exit系统调用:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep exit
#define __NR_exit 60
#define __NR_exit_group 231

exit系统调用有一个系统调用号60

现在,让我们看看它的论点:

$ man 2 exit 

以下屏幕截图显示了前面命令的输出:

只有一个参数,status,并且它有int数据类型来定义退出状态,如零状态表示无错误:

void _exit(int status);

现在,让我们看看如何使用寄存器调用 Linux x64 系统调用:

我们只需将系统调用号放在RAX中,然后将RDI中的第一个参数,将第二个参数放在RSI中,依此类推,如上图所示。

让我们看看如何调用exit系统调用:

我们只需要在RAX中放入60,这是exit系统调用号,然后在RDI中放入0,这是退出状态;是的,就是这么简单!

让我们更深入地看汇编代码:

mov rax, 60
mov rdi, 0

第一行告诉处理器将值60移动到rax,第二行告诉处理器将值0移动到rdi

如您所见,一条指令的一般结构为{Operation} {Destination}, {Source}

数据类型

数据类型在汇编中很重要。我们可以使用它们来定义一个变量,或者当我们想要在寄存器或内存的一小部分上执行任何操作时。

下表说明了基于长度的部件中的数据类型:

| 名称 | 指令 | 字节 | | | 字节 | db | 1. | 8. | | 单词 | dw | 2. | 16 | | 双字 | dd | 4. | 32 | | 四字 | dq | 8. | 64 |

为了完全理解,我们将在汇编中构建一个 hello world 程序。

你好,世界

好的,让我们开始更深入。我们将构建一个 hello world,它无疑是任何程序员的基本构建块。

首先,我们需要了解我们真正需要的是什么,这是在屏幕上打印hello world的系统调用。为此,让我们搜索write系统调用:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep write 
#define __NR_write 1
#define __NR_pwrite64 18
#define __NR_writev 20
#define __NR_pwritev 296
#define __NR_process_vm_writev 311
#define __NR_pwritev2 328

我们可以看到,write系统调用是编号为1;现在让我们看看它的论点:

$ man 2 write

以下屏幕截图显示了前面命令的输出:

write系统调用有三个参数;第一个是文件描述符:

ssize_t write(int fd, const void *buf, size_t count);

文件描述符有三种模式:

| 整数值 | 名称 | stdio.h的别名 | | 0 | 标准输入 | stdin | | 1 | 标准输出 | stdout | | 2 | 标准误差 | stderr |

当我们要在屏幕上打印hello world时,我们要选择标准输出1,第二个参数,它是指向我们要打印的字符串的指针;第三个参数是字符串的计数,包括空格。

下图说明了寄存器内部的内容:

现在,让我们跳到完整的代码:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

section .data

    hello_world: db 'hello world',0xa
    length: equ $-hello_world

在包含所有变量的.data部分中,代码中的第一个变量是数据类型为 byte(dbhello_world变量,它包含一个hello world字符串和0xa,这意味着一个新行,就像 C 中的\n一样。第二个变量是length,它包含一个长度为 byte 的hello_world字符串equ表示相等,$-表示评估当前行。

.text部分,如前所述,我们将1移动到rax,表示write系统调用号,然后将1移动到rdi作为文件描述符设置为标准输出的指示符,然后将hello_world字符串的地址移动到rsi,并移动hello_world的长度字符串到rdx,最后,我们调用syscall,这意味着执行。

现在,让我们组装并链接目标代码,如下所示:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ ./hello-world 

上述命令的输出如下所示:

它打印了hello world字符串,但由于程序不知道下一步要去哪里,所以退出了Segmentation fault。我们可以通过添加exit系统调用来修复它:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data

    hello_world: db 'hello world',0xa
    length: equ $-hello_world

我们只是通过将60移动到rax来添加exit系统调用,然后将1移动到rdi,表示退出状态,最后调用syscall执行exit系统调用:

让我们组装链接并重试:

现在它正常退出;我们也用echo $?确认退出状态:

退出状态为1,如我们所选!

堆栈

正如我们在上一章中所讨论的,堆栈是为每个正在运行的应用程序分配的空间,用于存储变量和数据。堆栈支持两种操作(推送和弹出);使用push操作将元素推送到堆栈,这将导致堆栈指针移动到较低的内存地址(堆栈从高内存增长到低内存)并指向堆栈顶部,而pop则获取堆栈顶部的第一个元素并提取它。

让我们来看一个简单的例子:

global _start

section .text

_start:

    mov rdx,0x1234
    push rdx
    push 0x5678
    pop rdi
    pop rsi

    mov rax, 60
    mov rdi, 0
    syscall
section .data

这个代码非常简单;让我们编译并链接它:

$ nasm -felf64 stack.nasm -o stack.o
$ ld stack.o -o stack

然后,我将在调试器中运行该应用程序(调试器将在下一章中解释),以向您展示堆栈的实际工作方式。

首先,在我们运行程序之前,除了 RSP 寄存器之外,所有寄存器都是空的,RSP 寄存器现在指向堆栈的顶部00007ffdb3f53950

然后,执行第一条指令,将0x1234移动到rdx

正如我们所看到的,rdx寄存器现在保存0x1234,堆栈中还没有任何更改。第二条指令将rdx的值推入堆栈,如下所示:

看看堆栈部分;移动到较低的地址(从5048,现在包含0x1234。第三条指令是直接将0x5678推送到堆栈:

第四条指令将堆栈中的最后一个元素提取到rdi

正如您所看到的,堆栈现在不再包含0x5678,它移动到了rdi。最后一条指令是将堆栈中的最后一个元素提取到rsi

现在堆栈恢复正常,0x1234移动到rsi

好的,到目前为止,我们已经介绍了两个关于如何构建 hello world 程序的基本示例,以及堆栈中的 push/pop 操作,其中我们看到了一些基本指令,例如movpushpop,还有更多。现在,您可能想知道为什么我没有解释这些说明,而是先带您浏览了这些示例。我的策略将带您进入下一节;在这里,我们将介绍汇编语言所需的所有基本指令。

数据操纵

数据操作是在汇编中移动数据,这是一个非常重要的话题,因为我们的大多数操作都是移动数据来执行指令,所以我们必须真正了解如何使用它们,比如mov指令,以及如何在寄存器之间、寄存器与内存之间移动数据,将地址复制到寄存器,以及如何使用xchg指令交换两个寄存器或寄存器与内存之间的内容,然后如何使用lea指令将源的有效地址加载到目标。

mov 指令

mov指令是 Linux 汇编中使用的最重要的指令,我们在前面的所有示例中都使用了它。

mov指令用于在寄存器之间以及寄存器和内存之间移动数据。

让我们看一些例子。首先,让我们从将数据直接移动到寄存器开始:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x56789

    mov rax, 60
    mov rdi, 0
    syscall

section .data

此代码只会将0x1234复制到rax0x56789复制到rbx

让我们更进一步,在前面的示例中添加一些寄存器之间的移动数据:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x56789

    mov rdi, rax
    mov rsi, rbx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们添加的只是将raxrbx的内容分别移动到rdirsi

让我们尝试在寄存器和内存之间移动数据:

global _start

section .text

_start:

    mov al, [mem1]
    mov bx, [mem2]
    mov ecx, [mem3]
    mov rdx, [mem4]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: db 0x12
    mem2: dw 0x1234
    mem3: dd 0x12345678
    mem4: dq 0x1234567891234567

mov al, [mem1]中,括号表示将mem1的内容移动到al。如果我们使用不带括号的mov al, mem1,它会将mem1的指针移动到al

在第一行中,我们将0x12移动到 RAX 寄存器,因为我们只移动 8 位,所以我们使用了 AL(RAX 寄存器中可以容纳 8 位的较低部分),因为我们不需要使用所有 64 位。还要注意的是,我们将mem1内存部分定义为db,它是字节,或者它可以容纳 8 位。

请看下表:

| 64 位寄存器 | 32 位寄存器 | 16 位寄存器 | 8 位寄存器 | | 雷克斯 | EAX | 斧头 | 啊,艾尔 | | RBX | EBX | BX | 波黑 | | RCX | ECX | CX | CH,CL | | 黑索今 | EDX | DX | DH,DL | | RSI | ESI | 硅 | SIL | | RDI | 电子数据交换 | DI | 迪尔 | | RSP | ESP | 服务提供商 | SPL | | 限制性商业惯例 | EBP | 英国石油公司 | BPL | | R8 | R8D | R8W | R8B | | R9 | R9D | R9W | R9B | | R10 | R10D | R10W | R10B | | R11 | R11D | R11W | R11B | | R12 | R12D | R12W | R12B | | R13 | R13D | R13W | R13B | | R14 | R14D | R14W | R14B | | R15 | R15D | R15W | R15B |

然后,我们将定义为dw的值0x1234移动到rbx寄存器中,然后我们在 BX 中移动了 2 个字节(16 位),可以容纳 16 位。

然后,我们将定义为dd的值0x12345678移动到 RCX 寄存器,将其 4 个字节(32 位)移动到 ECX。

最后,我们将定义为dq0x1234567891234567移动到 RDX 寄存器,它是 8 个字节(64 位),因此我们将其移动到 RDX:

这是执行后在寄存器中的外观。

现在,让我们谈谈将数据从寄存器移动到内存。请看下面的代码:

global _start

section .text

_start:

    mov al, 0x34
    mov bx, 0x5678
    mov byte [mem1], al
    mov word [mem2], bx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

    mem1: db 0x12
    mem2: dw 0x1234
    mem3: dd 0x12345678
    mem4: dq 0x1234567891234567

在第一条和第二条指令中,我们将值直接移动到寄存器,在第三条指令中,我们将寄存器 RAX(AL)的内容移动到mem1,并用字节指定长度。然后,在第四条指令中,我们将寄存器 RBX(RX)的内容移动到mem2并用字指定长度。

这是移动任何值之前的mem1mem2的内容:

下一个屏幕截图是在将值移动到mem1mem2之后,该值已更改:

数据交换

数据交换也很容易;用于使用xchg指令在两个寄存器或寄存器与内存之间交换内容:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x5678
    xchg rax, rbx
    mov rcx, 0x9876
    xchg rcx,[mem1]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: dw 0x1234

在前面的代码中,我们将0x1234移动到rax寄存器,然后将0x5678移动到rbx寄存器:

然后,在第三条指令中,我们将raxrbx的内容与xchg指令交换:

然后,我们将0x9876推到rcx寄存器,mem1保持0x1234

现在,交换rcxmem1

加载有效地址

加载有效地址lea指令将源地址加载到目的地址:

global _start

section .text

_start:

    lea rax, [mem1]
    lea rbx, [rax]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: dw 0x1234

首先,我们将mem1的地址移动到rax,然后将rax内的地址移动到rbx

现在两个都指向mem1,其中包含0x1234

算术运算

现在,我们将讨论算术运算(加法和减法)。开始吧:

global _start

section .text

_start:

    mov rax,0x1
    add rax,0x2

    mov rbx,0x3
    add bl, byte [mem1]

    mov rcx, 0x9
    sub rcx, 0x1

    mov dl,0x5
    sub byte [mem2], dl

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: db 0x2
    mem2: db 0x9

首先将0x1移动到rax寄存器,然后添加0x2,结果将存储在rax寄存器中。

然后,我们将0x3移动到rbx寄存器,并添加mem1的内容,其中包含0x2rbx的内容,结果将存储在rbx中。

然后,我们将0x9移动到rcx寄存器,然后减去0x1,结果将存储在rcx中。

然后,我们将0x5移动到rdx寄存器,从rdx中减去mem2的内容,结果将存储在mem2存储部分:

减法后的mem2内容如下:

现在,让我们谈谈带进位的加法和带借位的减法:

global _start

section .text

_start:

    mov rax, 0x5
    stc
    adc rax, 0x1

    mov rbx, 0x5
    stc
    sbb rbx, 0x1

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们将0x5移动到rax寄存器,然后设置进位标志,它将携带1。在此之后,我们将rax寄存器的内容添加到0x1中,并添加到进位标志,即1。这将给我们带来0x7(5+1+1)

然后,我们将0x5移动到rbx寄存器并设置进位标志,然后从rbx寄存器中减去0x1,并在进位标志中减去另一个1;这将给我们0x3(5-1-1)

现在,这里的最后一部分是递增和递减操作:

global _start

section .text

_start:

    mov rax, 0x5
    inc rax
    inc rax

    mov rbx, 0x6
    dec rbx
    dec rbx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们将0x5移动到rax寄存器,用1增加rax的值,然后我们再次增加,得到0x7

然后,我们将0x6移动到rbx寄存器,用1递减rbx的值,然后我们再次递减,得到0x4

循环

现在,我们将讨论汇编中的循环。与任何其他高级语言(Python、Java 等)一样,我们可以使用循环进行迭代,使用 RCX 寄存器作为计数器,然后使用loop关键字。让我们看看下面的例子:

global _start

section .text

_start:

    mov rcx,0x5
    mov rbx,0x1

increment:

    inc rbx
    loop increment

    mov rax, 60
    mov rdi, 0
    syscall

section .data

在前面的代码中,我们希望将 RAX 的内容增加五倍,因此我们将0x5移动到rcx寄存器,然后将0x1移动到rbx寄存器:

然后,我们添加了increment标记作为我们想要重复的块的开始指示,然后我们将递增指令添加到rbx寄存器的内容中:

然后,我们调用了loop increment,它将减少 RCX 寄存器的内容,然后从increment标记再次开始:

现在它将一直运行,直到 RCX 寄存器达到零,然后流将退出该循环:

现在,如果程序是用 RCX 上的值重写的呢?让我们看一个例子:

global _start

section .text

_start:

    mov rcx, 0x5

print:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello
    mov rdx, length
    syscall

loop print

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    hello: db 'Hello There!',0xa
    length: equ $-hello

执行此代码后,程序将陷入无限循环中,如果我们仔细观察,我们将看到代码在执行 syscall 后覆盖 RCX 寄存器中的值:

因此,我们必须找到一种保存 RCX 寄存器的方法,例如将其保存在堆栈中。首先,在执行 syscall 之前,我们在堆栈中推送当前值,在执行 syscall 之后,我们再次用我们的值覆盖 RCX 中的任何内容,然后减小该值并在堆栈中再次推送以保存它:

global _start

section .text

_start:

    mov rcx, 0x5

increment:

    push rcx
    mov rax, 1
    mov rdi, 1
    mov rsi, hello
    mov rdx, length
    syscall
    pop rcx

loop increment

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    hello: db 'Hello There!',0xa
    length: equ $-hello

这样,我们将值保存在 RCX 寄存器中,然后再次将其放入 RCX 中使用。请看前面代码中的pop rcx指令。RCX 又回到了0x5,果然:

控制流量

在这里,我们将讨论控制执行流。正常的执行流程是执行步骤 1,然后执行步骤 2,依此类推,直到代码正常退出。如果我们决定要在第 2 步中发生一些事情,那么代码跳过第 3 步,直接执行第 4 步,或者我们只想跳过第 3 步而不等待事情发生,该怎么办?有两种类型的跳跃:

  • 无条件改变流程
  • 根据标志的更改更改流

现在,让我们从无条件跳转开始:

global _start

section .text

_start:

jmp exit_ten

    mov rax, 60
    mov rdi, 12
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data

前面的代码包含四个exit系统调用,但退出状态不同(120101),我们从jmp exit_ten开始,即跳转到exit_ten位置,跳转到这段代码:

    mov rax, 60
    mov rdi, 10
    syscall

执行并正常退出,退出状态为10。请注意,下一节永远不会执行:

    mov rax, 60
    mov rdi, 12
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

让我们确认一下:

$ nasm -felf64 jmp-un.nasm -o jmp-un.o
$ ld jmp-un.o -o jmp-un
$ ./jmp-un
$ echo $?

在以下屏幕截图中可以看到上述命令的输出:

我们可以看到,代码以退出状态10退出。

让我们看另一个例子:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_one
    mov rdx, length_one
    syscall

jmp print_three

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

print_three:
    mov rax, 1
    mov rdi, 1
    mov rsi, hello_three
    mov rdx, length_three
    syscall

    mov rax, 60
    mov rdi, 11
    syscall

section .data

    hello_one: db 'hello one',0xa
    length_one: equ $-hello_one

    hello_two: db 'hello two',0xa
    length_two: equ $-hello_two

    hello_three: db 'hello three',0xa
    length_three: equ $-hello_three

在前面的代码中,它以打印hello_one开始。然后点击jmp print_three按钮,执行流程变为print_three位置,开始打印hello_three。永远不会执行以下部分:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

让我们确认一下:

$ nasm -felf64 jmp_hello.nasm -o jmp_hello.o
$ ld jmp_hello.o -o jmp_hello
$ ./jmp_hello

在以下屏幕截图中可以看到上述命令的输出:

现在,让我们继续讨论条件,老实说,我们不能涵盖所有条件,因为列表很长,但我们将看到一些示例,以便您理解这个概念。

如果跳转低于(jb指令,则表示如果设置了进位标志CF)则将执行跳转(CF 等于1

如前所述,我们可以使用stc指令手动设置 CF。

让我们修改前面的示例,但使用jb指令,如下所示:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_one
    mov rdx, length_one
    syscall

    stc

jb print_three

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

print_three:
    mov rax, 1
    mov rdi, 1
    mov rsi, hello_three
    mov rdx, length_three
    syscall

    mov rax, 60
    mov rdi, 11
    syscall

section .data

    hello_one: db 'hello one',0xa
    length_one: equ $-hello_one

    hello_two: db 'hello two',0xa
    length_two: equ $-hello_two

    hello_three: db 'hello three',0xa
    length_three: equ $-hello_three

如您所见,我们执行了stc来设置进位标志(即 CF 等于1,然后我们使用jb指令来测试,如果 CF 等于1,这意味着跳转到print_three

下面是另一个例子:

global _start

section .text

_start:

    mov al, 0xaa
    add al, 0xaa

jb exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在前面的示例中,add 操作将设置进位标志,然后我们使用jb指令进行测试;如果 CF 等于1,则跳至exit_ten

现在,让我们看看另一种方法,即如果低于或等于(jbe指令),则跳转,这意味着 CF 等于1零标志(ZF等于1。前面的示例也可以,但让我们尝试其他方法来设置 ZF 等于1

global _start

section .text

_start:

    mov al, 0x1
    sub al, 0x1

jbe exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在前面的代码中,减法操作将设置 ZF,然后我们将使用jbe指令测试 CF 是否等于1或 ZF 是否等于1;如果为 true,则跳转执行exit_ten

另一种类型为非符号跳转(jns,表示 SF 等于0

global _start

section .text

_start:

mov al, 0x1
sub al, 0x3

jns exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在前面的代码中,减法操作会将符号标志SF)设置为等于1。之后,我们将测试 SF 是否等于0,它将失败,并且不会跳转执行exit_ten,并将以退出状态0继续正常退出:

程序

汇编中的过程可以作为高级语言中的函数,这意味着您可以编写一段代码,然后调用它来执行。

例如,我们可以构建一个过程,该过程可以获取两个数字并将它们相加。此外,我们可以在执行过程中使用call指令多次使用它。

建造程序很简单。首先,在_start之前定义您的过程,然后添加您的指令并以ret指令结束您的过程。

让我们尝试构建一个可以接受两个数字并将其相加的过程:

global _start

section .text

addition:

    add bl,al
    ret

_start:

    mov al, 0x1
    mov bl, 0x3
    call addition

    mov r8,0x4
    mov r9, 0x2
    call addition

    mov rax, 60
    mov rdi, 1
    syscall

section .data

首先,我们在_start部分之前添加了addition部分。然后,在addition部分,我们使用add指令添加R8R9寄存器中的内容,并将结果放入R8寄存器,然后我们用ret结束addition过程。

然后,我们将1移至R8寄存器,将3移至R9寄存器:

然后调用addition过程,将下一个指令地址推送到堆栈中,即mov r8,0x4

注意RSP现在指向下一个操作,我们在addition过程中,然后代码将两个数字相加并将结果存储在R8寄存器中:

然后点击ret指令,将执行流程设置回mov r8,0x4

这将移动4R8寄存器,然后移动2R8寄存器:

然后调用addition过程,将下一条指令推入堆栈,即mov rax, 60

然后,将两个数字相加,并将结果存储在R8寄存器中:

然后,我们再次点击ret指令,它将从堆栈中弹出下一条指令并将其放入RIP寄存器,这相当于pop rip

然后,代码将继续执行exit系统调用。

逻辑运算

现在,我们将讨论逻辑运算,如按位运算和位移位运算。

按位运算

逻辑运算中有四种位运算:AND、OR、XOR 和 NOT。

让我们从 AND 位操作开始:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    and rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

首先,我们将0x10111011移动到rax寄存器,然后我们将0x11010110移动到rbx寄存器:

然后,我们在两侧执行了位运算,并将结果存储在 RAX 中:

让我们看看RAX寄存器中的结果:

现在,让我们转到 OR 逐位操作,并修改前面的代码以执行该操作:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    or rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

我们将两个值移到了raxrbx寄存器:

然后,我们对这些值执行 OR 操作:

现在,让我们在RAX寄存器中确认结果:

现在让我们看看具有相同值的 XOR 逐位操作:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    xor rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

将相同的值移动到raxrbx寄存器:

然后,执行 XOR 操作:

让我们看看RAX寄存器中的内容:

您可以使用寄存器上的 XOR 指令来清除该寄存器的内容。例如,xor raxrax将用零填充 RAX 寄存器。

现在,让我们看最后一个,它不是按位操作,它将把 1 变成 0,把 0 变成 1:

global _start

section .text

_start:

    mov al,0x00
    not al

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在以下屏幕截图中可以看到前面代码的输出:

发生的情况是 NOT 指令将 0 更改为 1(ff,反之亦然。

位移位操作

如果您遵循每个图表所示,那么位移位操作是一个简单的主题。主要有两种类型的位移位运算:算术移位运算和逻辑移位运算。但是,我们还将看到旋转操作。

让我们从算术移位运算开始。

算术移位运算

让我们把这件事尽可能简单。算术移位有两种类型:左移位SAL)和右移位SAR)。

在 SAL 中,我们在最低有效位侧推0,如果最高有效位侧的额外位是1,则可能会影响CF

所以这个移位的结果不会影响CF上,它会是这样的:

让我们举一个例子:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    sal rax, 4
    sal rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们将0x0fffffffffffffff移到rax寄存器,现在看起来是这样的:

现在,我们想一次用 4 位执行 SAL:

因为最高有效位为零,所以不会设置 CF:

现在,让我们尝试另一轮:我们再推一个零,最高有效位是 1:

将设置进位标志:

现在,让我们看看 SAR 指令。在 SAR 中,根据最高有效位推送一个值,如果是0,则推送0,如果是1,则推送1以防止符号改变:

最高有效位用作符号指示,0表示正数,1表示负数。

因此,在合成孔径雷达中,它将随最重要位中的任何内容而移动。

让我们来看一个例子:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    sar rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

因此,输入将如下所示:

因此,SAR 四次将推送0四次,因为最高有效位为零:

此外,设置 CF 是因为最低有效位为 1:

逻辑移位

逻辑移位也包含两种移位类型:逻辑左移**(SHL)和逻辑右移**(SHR)。SHL 和萨尔一模一样。

****让我们看看下面的代码:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    shl rax, 4
    shl rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

此外,它将从最低有效位侧推零四次:

这不会对进位标志产生任何影响:

在第二轮,它将再次推动零位四次:

最高有效位为 1,因此这将设置进位标志:

现在让我们转到 SHR。它只需从最高有效位侧推送一个 0,而无需保持符号不变:

现在,请尝试以下代码:

global _start

section .text
_start:

    mov rax, 0xffffffffffffffff
    shr rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们移动 64 位的 1:

在此之后,我们将执行 SHR 32 次,这将把 32 个零推到最高有效位侧:

此外,由于最低有效位为 1,这将设置进位标志:

旋转操作

旋转操作很简单:我们将寄存器的内容向右或向左旋转。在这里,我们只讨论向右旋转ROR)和向左旋转ROL)。

让我们从 ROR 开始:

在 ROR 中,我们只是从右向左旋转位,而不添加任何位;让我们看看下面的代码:

global _start

section .text

_start:

    mov rax, 0xffffffff00000000
    ror rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们将0xffffffff00000000移至rax寄存器:

然后,我们将开始从右向左移动位 32 次:

没有与 1 的转换,因此不会设置进位标志:

让我们移动 ROL,它与 ROR 相反,ROR 从左向右旋转位,而不添加任何位:

让我们看一下前面的示例,但 ROL:

global _start

section .text

_start:

    mov rax, 0xffffffff00000000
    rol rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们还将0xffffffff00000000移至rax寄存器:

然后,我们将开始从左向右旋转位 32 次:

我们正在旋转,因此这将设置进位标志:

总结

在本章中,我们讨论了 Linux 中的 Intel x64 汇编语言,以及如何处理堆栈、数据操作、算术和逻辑运算,如何控制执行流,以及如何在程序集中调用系统调用。

现在我们已经准备好制作我们自己的定制 Shellcode,但在此之前,您需要学习调试和逆向工程的一些基础知识,这将是我们的下一章。****