【技术分享】NASM x86 Assembly Language
2024-09-30 / 0 评论 / 5,982 阅读 / 958 点赞

【技术分享】NASM x86 Assembly Language

发光的神
2024-09-30 / 0 评论 / 5,982 阅读 / 正在检测是否收录...

留言

复习x86汇编基础,首先为什么要学习汇编语言呢?
在二进制程序逆向中是必不可少的,在二进制程序在没有源代码的情况下,只能进行反汇编逆向分析程序的执行流程。
所谓 "学而不思则罔,思而不学则殆",只有真正的理解了是如何运作的,才能更好的进行后面的分析。

mk0y7z98.png

本教程使用 intel 语法来进行编写,编译器使用的是开源的 NASM,就你叫x86汇编呀,直接干就完了!

简介

本文章会简单的讲解下Windows下的x86汇编,内容比较简短都是示例,x86汇编语言编程也很混乱。
因为有很多不同的汇编器:MASM、NASM、gas、as86、TASM、a86、Terse 等,导致语法都不相同。
出于对 汇编语言发展 的兴趣,本教程不是为了教你汇编,让你大概学习下x86汇编基础语法方便为后期逆向做准备。
当然感兴趣的可以自己研究,这里推荐几个好的教程网站: Linux 汇编语言教程

mk0y9gwb.png

Putting the "You" in CPU

汇编器与链接器

lo3qa90m.png

不管使用哪种汇编器、目标文件格式、链接器或操作系统,编程过程始终是相同的。

Nasm 编译器安装

ln6117qw.png

Nasm win64 下载 - | - MinGW 下载

首先需要下载编译环境,Nasm 与 MinGW 下载好后解压出来,配置好环境变量方便终端进行调用。
这里我推荐几个链接器:ld、alink、golink、gcc 各有秋色,我会演示他们的用法,
前三者打包都是程序最小的状态,而 gcc 会使程序大小变大,因为它加了一些东西。

NASM 汇编语法

输出 Hello, World! - Windows

extern _printf          ; 声明外部的 printf 函数

section .data
    msg db 'Hello, World!', 0 ; 定义字符串常量

section .text
    global _main        ; 设置入口函数为 _main

_main:
    push msg            ; 将字符串地址推入栈中,作为参数传递给 printf
    call _printf        ; 调用 printf 函数
    add esp, 4          ; 清理栈上的参数
    ret                 ; 函数返回

随便创建一个文件,名称.asm 以.asm 后缀结尾,当然后缀什么的都不重要,汇编代码是对的一切都是 ok 的,通过以下代码在 Windows 进行生成再到链接最终生成可执行程序。

/*
 ------ 参数说明
 -f win32 指定 32位 生成目标文件 test.o
 -lmsvcrt 指定链接器需要 C运行库 要用到 printf

 ------ 编译命令
 nasm -f win32 demo.asm -o test.o
 ld test.o -o test.exe -lmsvcrt
*/

ln620vjw.png

ln61pmek.png

生成完毕后,可以发现程序大小只有 4kb 左右,非常小那是因为用汇编进行编写的,没有其他一些没用的函数打包进去。

ln623f6o.png

通过 IDA 打开生成的程序,发现只有 _main 和 _printf 函数,而且代码也是我们写的那一部分。

ln62eywm.png
通过 x64dbg 发现,运行直接到 OEP 时就能看到我们写的代码, 程序入口点是 401000 从这里开始到 40100D 结束(自己写的代码范围)。

ln62zy5o.png

反汇编代码中 push 0x402000 是把内存地址存储的字符串压入到堆栈中,然后通过 call Printf 调用进行输出 Hello, World!。

输出 Hello, World! - Linux

section .data
    ; 定义一个名为 hello 的字符串,用 db 伪指令来定义
    ; db 是 define bytes,用来指定一组字节
    hello db 'Hello, World!', 0 ; 字符串用单引号或双引号括起来,末尾要添加一个零终止符 \0

section .text
    global _start ; 设置入口函数为 _start
    
_start:
    ; 往标准输出 (stdout) 写入字符串
    ; 用系统调用 write
    mov eax, 4    ; 系统调用号 4 (write) 存储到寄存器 eax 
    mov ebx, 1    ; 描述符 1 (stdout) 存储到寄存器 ebx 
    mov ecx, hello   ; 字符串的地址存储到寄存器 ecx 
    mov edx, 13   ; 把字符串的长度(以字节为单位)存储到寄存器 edx 
    int 0x80      ; 系统调用 sys_write

    ; 退出程序
    ; 用系统调用 exit
    mov eax, 1    ; 系统调用号 1 (exit) 存储到寄存器 eax 
    xor ebx, ebx  ; 寄存器 ebx 设置为 0,表示返回值为 0
    int 0x80      ; 系统调用 sys_exit

ln62o74l.png

/*
  ------ 参数说明
  -f elf32 指定32位生成目标文件main.o
  -m elf_i386 指定生成32位可执行elf程序

  ------ 编译命令
  nasm -f elf32 main.asm -o main.o
  ld -m elf_i386 main.o -o main
*/

ln636jx5.png

通过IDA打开,可以看到只有一个 _start 主入口函数,也是没有其他任何函数,通过 sys_write 输出再到 sys_exit 退出结束。
前面的操作让我们实现了编译环境搭建,和第一个汇编程序的编写到运行,现在让我们正式开始吧。(本教程在 Windows 操作系统进行讲解)

视频教程

PE文件节

section .text  // 代码段:用来存储程序的可执行指令(代码)。
section .data  // 数据段:用来存储已经初始化的静态变量和全局变量。
section .rdata // 只读数据段:用来存储只读数据,字符串常量等。
section .bss   // 未初始化数据段:用来存储未初始化的静态变量和全局变量。

section .data  // 可读写数据段:用来存储可以被程序读取或写入的数据。
section .data? // 未定义的数据段

/* 可执行数据段: */
section .text  // 代码段:用来存储程序的可执行指令(代码)。
section .idata // 导入数据段:用来存储从其他动态链接库(DLL)中导入的符号等信息。
section .edata // 导出数据段:用来存储导出给其他模块的符号等信息。
section .rsrc  // 资源数据段:用来存储可执行文件中的资源数据,如图标、位图、菜单和对话框等。

代码注释

; 单行注释(分号)

字符串输出 - utf-8

extern _printf, _system

section .data
    cmd db 'chcp 65001', 0  ; 设置终端输出编码为 utf-8
    msg db '你好,世界!', 10, 0 
    msg2 db 'Hello, World!', 0
   
section .text
    global _main        ; 设置入口函数为 _main

_main:
    push cmd
    call _system
    push msg            ; 将字符串地址推入栈中,作为参数传递给 printf
    call _printf        ; 调用 printf 函数
    push msg2
    call _printf
    add esp, 12         ; 清理栈上的参数
    ret                 ; 函数返回

局部变量 - local var

section .text
global _main

extern _printf

_main:
   push ebp                   ; 保存旧的基址寄存器值
   mov ebp, esp               ; 设置新的基址寄存器值

   sub esp, 4                 ; 分配 4 字节大小的空间作为局部变量
   mov dword [ebp-4], 5       ; 将值 5 存储到局部变量中

   mov eax, dword [ebp-4]     ; 将局部变量值加载到寄存器 eax 中
   push eax                   ; 将局部变量值推入栈中,作为参数传递给 printf
   push msg                   ; 将格式化字符串的地址也推入栈中
   call _printf               ; 调用 printf 函数
   add esp, 12                ; 清理栈上的参数

   pop ebp                    ; 恢复旧的基址寄存器值
   ret                        ; 函数返回

section .data
   msg db 'local variable is %d', 10, 0 ; 格式化字符串.

数据类型 - Data type

整型(Integer) 浮点型(Float)
布尔型(Boolean) 字符型(Character)

section .data
    format db "int: %d", 0     ; 格式化输出模板,%d 整数占位符
    num dd 100  ; 存储整数值100

    msg db "string: Hello", 0  ; 要输出的字符串
    newline db 10, 0           ; 换行符和结尾符

section .text
    global _main

extern _printf

_main:
    push ebp      ; 保存当前的ebp值
    mov ebp, esp  ; 把当前的堆栈指针esp的值赋给ebp,建立函数的堆栈帧

    push dword [num] ; 压入num变量的值,准备进行格式化输出
    push format   ; 压入format变量的地址,作为格式化输出的模板
    call _printf  ; 调用外部函数_printf进行格式化输出
    add esp, 8    ; 清除堆栈上的两个参数,恢复堆栈指针

    push dword newline ; 压入newline变量的值,输出换行符
    call _printf  ; 调用_printf函数输出换行符
    add esp, 4    ; 清除堆栈上的一个参数

    push msg      ; 压入msg变量的地址,准备输出字符串
    call _printf  ; 调用_printf函数输出字符串 Hello
    add esp, 4    ; 清除堆栈上的一个参数

    mov esp, ebp  ; 恢复函数调用前的堆栈状态,将堆栈指针esp的值设为ebp的值
    pop ebp       ; 恢复之前保存在堆栈中的ebp寄存器的值
    ret           ; 函数返回

lnvgudp6.png

条件语句 - if ... else

section .data
    format db "%d", 0
    num dd 1

    format2 db "%d", 0
    num2 dd 0

section .text
    global _main

extern _printf

_main:
    push ebp
    mov ebp, esp
    
    mov eax, 5
    mov ebx, 5
    cmp eax, ebx ; 比较是否相等
    je print_num
    
    push dword [num2]
    push format
    call _printf
    add esp, 8
    
    jmp end
    
print_num: ; 成立则跳转到这
    mov eax, dword [num]
    push eax
    push format2
    call _printf
    add esp, 8

end:
    mov esp, ebp
    pop ebp
    ret

; eax, ebx 寄存器值 都等于 5 所以成立输出 1

条件循环 - for

section .data
    count equ 10
    msg db "Loop iteration.", 10, 0

section .text
    global _main
    extern _printf

_main:
    mov ebx, count  ; 设置计数器为循环次数
    
loop_start:
    cmp ebx, 0      ; 检查计数器是否为零
    je loop_end     ; 如果为零,跳转到循环结束处
    push msg
    call _printf    ; 输出消息
    add esp, 4      ; 清除栈上的参数
    dec ebx         ; 计数器减一
    jmp loop_start  ; 跳转回循环起始点
loop_end:
    mov eax, 0
    ret

条件循环 - while

section .data
    msg db "Loop iteration.", 10, 0

section .text
    global _main
    extern _printf

_main:

loop_start:
    push msg
    call _printf    ; 输出消息
    add esp, 4      ; 清除栈上的参数
    jmp loop_start  ; 跳转回循环起始点

整型数组 - Array

section .data
    array dd 1, 2, 3, 4, 5, 10   ; 整数数组
    len equ ($ - array) / 4      ; 数组长度

section .text
    global _main
    extern _printf

_main:
    xor esi, esi                ; 将数组索引初始化为 0
      
loop:
    cmp esi, len                ; 检查是否到达数组末尾
    jge end_loop
    mov eax, [array + esi * 4]  ; 获取数组元素的值
    push eax                    ; 将元素值推入堆栈
    push format                 ; 将格式化字符串推入堆栈
    call _printf                ; 调用 printf 函数来打印元素值
    add esp, 8                  ; 清理堆栈
    inc esi                     ; 增加数组索引
    jmp loop
    
end_loop:
    ret                         ; 返回函数

section .data
    format db "%d ", 0          ; 格式化字符串,用于打印整数

switch 语句

extern _printf    ; 引入 printf 函数

section .data
   value db 2     ; 定义 value 变量,初始值为 2
   
   msg1 db "1", 0 ; 定义三个输出信息
   msg2 db "2", 0
   msg3 db "3", 0

section .text
    global _main

_main:
    cmp byte [value], 1
    je case_1           ; 比较 value 和 1 的值,如果相等,跳转到 case_1 标签处
    
    cmp byte [value], 2
    je case_2           ; 比较 value 和 2 的值,如果相等,跳转到 case_2 标签处
    
    cmp byte [value], 3 ; 比较 value 和 3 的值,如果相等,跳转到 case_3 标签处
    je case_3     ; 如果都不相等,跳转到 case_default 标签处
    jmp case_default

case_1:
   push msg1      ; 将 msg1 的地址压入栈中,并调用 printf 函数
   call _printf 
   
   add esp, 4     ; 回收栈空间
   jmp end_switch ; 跳转到 end_switch 标签处

case_2:
   push msg2      ; 将 msg2 的地址压入栈中,并调用 printf 函数
   call _printf
   
   add esp, 4     ; 回收栈空间
   jmp end_switch ; 跳转到 end_switch 标签处

case_3:
   push msg3      ; 将 msg3 的地址压入栈中,并调用 printf 函数
   call _printf
   add esp, 4     ; 回收栈空间
   jmp end_switch ; 跳转到 end_switch 标签处

case_default:
   jmp end_switch ; 跳转到 end_switch 标签处

end_switch:
   mov eax, 0     ; 将返回值设为 0,保证程序正常退出
   ret            ; 返回上一级调用

函数调用约定

lo3m01ie.png

方法名采用这样的命名方式,就像 _GetStdHandle@4,是因为在 Win32 中,函数名称会经过编码(mangled 或 decorated)以符合特定的约定。这种约定是为了确保函数在编译后可以正确地对外提供和使用。

Win32 使用的调用约定称为 __stdcall,它规定了函数在被调用时参数的传递方式、栈的清理方式等。根据 __stdcall 的规定,函数名前面会加上一个下划线(_),然后是函数名本身,后面紧跟着一个 at 符号(@),最后是参数列表中的字节数(以十进制表示)。

Cdecl(C Declaration)

  • 参数从右向左入栈。
  • 调用者负责清理堆栈。
  • 支持可变参数。
  • 在多数 C 编译器和 Microsoft Windows、Linux 系统上广泛使用。

Stdcall(Standard Call)

  • 参数从右向左入栈。
  • 被调用函数负责清理堆栈。
  • 常用于 Win32 API。

Fastcall

  • 一部分参数通过寄存器传递,其余的参数从右向左入栈。
  • 被调用函数负责清理堆栈。
  • 具体实现可能因编译器和平台而异。
HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);

函数参数是从右向左压入到堆栈的,那么第一个参数在汇编中就是 dwProcessId(进程id),以此类推压入完成进行调用。

extern _OpenProcess@12 ; 设置调用API加@12,说明函数有3个参数都是 dword(4字节)。

section .data
   PROCESS_ALL_ACCESS equ 0x1F0FF

section .text
   global _main

_main:
   push dword 0                   ; dwProcessId,进程 ID
   push dword 0                   ; bInheritHandle,不继承句柄
   push dword PROCESS_ALL_ACCESS  ; dwDesiredAccess,使用标准读取权限
   call _OpenProcess@12 ; @12 这么写会自动清空堆栈参数,无需写清除参数代码。

lnv7m86g.png

以上是加了 @12 参数自动从堆栈清除的过程,可以发现当执行完 call 时堆栈参数自动被清除。

标准用户输入 - scanf

section .data
    prompt db "Please enter a string: ", 0
    format db "%s", 0
    input db 255 dup(0) ; 申请长度为 255 字节内存字符串数组,初始值都为 0;

section .text
    global _main

extern _printf, _scanf, _puts

_main:
    push ebp
    mov ebp, esp
    
    push dword prompt
    call _printf   ; 输出提示信息
    add esp, 4
    
    ; 接收用户输入
    push dword input
    push dword format
    call _scanf
    add esp, 8
    
    push dword input
    call _puts    ; 显示输入的字符串
    add esp, 4

end:
    mov esp, ebp
    pop ebp
    ret

ln76cd9y.png

调用系统命令 - system

section .data
    cmd db 'cmd /c dir', 0 ; 存储 cmd 执行命令

section .text
    global _main

extern _system

_main:
    push ebp
    mov ebp, esp
    
    push dword cmd ; 压入参数到堆栈
    call _system   ; 调用 system 
    add esp, 4     ; 清除参数

end:
    mov esp, ebp
    pop ebp
    ret

ln76v97h.png

调用弹窗 - MessageBoxA

section .data
   caption db "Hello", 0
   message db "Welcome to the MessageBox!", 0

section .text
   global _main

extern _MessageBoxA@16

_main:
   push 0                 ; 确定:0  消息:48 警告:64
   push caption
   push message
   push 0                 ; NULL (HWND)
   call _MessageBoxA@16   ; 调用MessageBox函数显示消息框
nasm -f win32 demo.asm -o test.obj
ld -m i386pe -o test.exe test.obj -luser32 -lkernel32
// -luser32 -lkernel32 导入动态链接库

lnv7sw96.png

终止进程 - TerminateProcess

section .data
   PROCESS_ALL_ACCESS equ 0x1F0FF

section .text
   global _main

extern _OpenProcess@12, _TerminateProcess@8, _printf

_main:
   push dword 21108              ; dwProcessId,进程 ID
   push dword 0                  ; bInheritHandle,不继承句柄
   push dword PROCESS_ALL_ACCESS ; dwDesiredAccess,使用标准读取权限
   call _OpenProcess@12
   mov ebx, eax                  ; 将句柄保存到 EBX 中

   push ebx                      ; 将句柄值压入堆栈
   push format_str               ; 格式化字符串
   call _printf                  ; 调用 printf 函数
   add esp, 8
   
   push 0
   push ebx
   call _TerminateProcess@8      ; 调用 TerminateProcess 函数
   xor ebx, ebx                  ; 清空 EBX 寄存器
   ret

section .data
   format_str db "Handle value: %d", 0  ; 使用 %d 格式输出整数

lnvfpqys.png

伪代码指令 - import

lo3mmk9b.png

再次提到,对于编译后一个 Win32 函数的名称如何被修饰或装饰这个问题,在这个级别上了解似乎要求过高了。毕竟所有的 C 程序员都可以愉快地写出 "ExitProcess",而不是 "_ExitProcess@4"。

很赞同这个说法,有的时候 下划线(_)加艾特(@)的确不是很好,所以可以借助下伪指令,这样就可以直接导入 Windows api 函数了,不需要再修改名称。

Alink链接器

lo3192em.png

这个 Alink 链接器上一次修改还是在 1999 年,真的是上古链接器了,估计作者也不更新了不过好在还可以用。
Alink 下载

lo313q0a.png

伪指令这些语法,必须生成 obj 然后用 alink 链接生成才行,对于 gcc 和 ld 都是会报错的导致编译不成功,就像以下这样:

lo31d5hz.png

简单调用 printf

extern printf
import printf msvcrt.dll

section .data
    msg db "hey Actdream!", 0

section .text use32

..start:
    push msg
    call [printf]
    add esp, 4
    ret

输出 Hello, World!

extern ExitProcess              ; 声明 ExitProcess 外部函数。
import ExitProcess kernel32.dll ; 引入 ExitProcess 函数在 kernel32.dll 动态链接库里。

extern  GetStdHandle 
import GetStdHandle kernel32.dll

extern  WriteFile
import WriteFile kernel32.dll

segment .data use32      ; 定义 32 位数据段,用来存储程序中使用的数据。
Text db "Hello, World!",0 ; 在数据段里定义了一个以空字符结尾的字符串 "Hello, World!"。

segment .code use32      ; 定义一个 32 位代码段,在这里定义了程序的主要逻辑。
..start:        ; 定义一个程序入口点标签
    push    -11 ; 把参数 -11 压入栈中
    call    [GetStdHandle] ; 调用 GetStdHandle 函数,并将返回值存储在 EAX 寄存器里
    mov     ebx, eax       ; 把句柄存储在 EBX 寄存器里

    push    dword 0      ; 压入参数 0
    lea     eax, [ebp-4] ; 指向文本字符串的指针存储在 EAX 寄存器里
    push    eax          ; 把 EAX 寄存器中的指针地址压入栈中
    push    (message_end - message) ; 把字符串的长度压入栈里(不包括结尾的空字符)
    push    message     ; 把字符串的起始地址压入栈里
    push    ebx         ; 获取到的标准输出句柄压入栈中
    call    [WriteFile] ; 最后调用 WriteFile 函数,把字符串写入到标准输出。

    push    dword 0       ; 压入参数 0
    call    [ExitProcess] ; 调用 ExitProcess 函数,来终止程序的运行

    hlt      ; 暂停 CPU 的运行

message:     ; 定义消息字符串的起始标签
    db      'Hello, World!', 10 ; 在数据段里中定义了一个字符串 "Hello, World!"。
message_end: ; 定义消息字符串的结束标签
/*
  ------ 参数说明
  -fobj 指定输出格式为 obj 目标文件格式
  -subsys console 指定生成的是控制台可执行文件
  -oPE 指定输出文件格式 PE 可执行文件

  ------ 编译命令
  nasm -fobj main.asm
  alink -subsys console -oPE main.obj
*/

lo311fi4.png

调用弹窗 - MessageboxA

[BITS 32]                        ; 声明代码为32位模式

extern ExitProcess
import ExitProcess kernel32.dll  ; 导入ExitProcess函数

extern MessageBoxA
import MessageBoxA user32.dll    ; 导入MessageBoxA函数

segment .data use32
Caption db 'From Assembly',0  ; 定义标题字符串
Text db "Hello, World",0      ; 定义消息字符串

segment .code use32
..start:
push dword 0        ; 参数0(消息框类型)压入栈中
push dword Caption  ; 标题字符串的地址压入栈中
push dword Text     ; 消息字符串的地址压入栈中
push dword 0        ; 参数0(父窗口句柄)压入栈中
call [MessageBoxA]  ; 调用MessageBoxA函数显示消息框

push dword 0        ; 参数0(退出代码)压入栈中
call [ExitProcess]  ; 调用ExitProcess函数终止程序的运行

lo3ny25q.png

读进程内存数据

BOOL WINAPI ReadProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPCVOID lpBaseAddress,
  _Out_ LPVOID  lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesRead
);

读内存要用到 ReadProcessMemory 系统提供的API函数。

lotnpe9f.png
编译后只有一个 start 入口直接就是我们写的代码,很干净的一个程序只有 2.5 kb 大小。

extern OpenProcess
import OpenProcess kernel32.dll
extern ReadProcessMemory
import ReadProcessMemory kernel32.dll
extern printf
import printf msvcrt.dll
extern CloseHandle
import CloseHandle kernel32.dll

%define PROCESS_ALL_ACCESS 0x1F0FF
section .data
    processId dd 27676
    desiredAccess dd PROCESS_ALL_ACCESS
    inheritHandle dd 0
    lpBaseAddress dd 0x4a4610 

    nSize dd 128
    lpNumberOfBytesRead dd 0
    readSuccess db 'Read bytes: %d', 0

section .bss
    buffer resb 128 
    hProcess resd 1

section .text use32
..start:
    push dword [processId]
    push dword [inheritHandle]
    push dword [desiredAccess]
    call [OpenProcess]      ; 打开进程句柄

    mov [hProcess], eax
    lea eax, [lpNumberOfBytesRead]  
    push eax     
    push dword [nSize] 
    lea eax, [buffer] 
    push eax 
    push dword [lpBaseAddress]
    push dword [hProcess]
    call [ReadProcessMemory] ; 读内存数据

    push dword [buffer]
    push readSuccess
    call [printf]           ; 输出数据
    add esp, 8 

    push dword [hProcess]
    call [CloseHandle]      ; 关闭句柄
    ret

lotnde5a.png

写进程内存数据

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);
extern OpenProcess
import OpenProcess kernel32.dll
extern WriteProcessMemory
import WriteProcessMemory kernel32.dll
extern CloseHandle
import CloseHandle kernel32.dll
extern ExitProcess
import ExitProcess kernel32.dll

%define PROCESS_ALL_ACCESS 0x1F0FFF

section .data
    processId dd 27676 
    desiredAccess dd PROCESS_ALL_ACCESS
    inheritHandle dd 0
    lpBaseAddress dd 0x4a4610
    nSize dd 4
    writeValue dd 10

section .bss
    hProcess resd 1
    bytesWritten resd 1

section .text use32
..start:
    push dword [processId]
    push dword [inheritHandle]
    push dword [desiredAccess]
    call [OpenProcess]
    mov [hProcess], eax

    push 0
    push dword [nSize]
    lea eax, [writeValue]  
    push eax 
    push dword [lpBaseAddress]
    push dword [hProcess] 
    call [WriteProcessMemory]

    push dword [hProcess]
    call [CloseHandle]
    ret

lott7ues.png

Golink链接器

lp0itxzp.png

Goasm 与 GoLink,都是由 Jeremy Gordon 开发,是用来与 GoAsm 配合使用的链接器。
主要特点:
轻量级高效:专为快速和高效的链接过程设计。
兼容性:它可以链接其他汇编器(比如 NASM 或 FASM)还有某些 C/C++ 编译器生成的对象文件。
格式的支持:GoLink 对 Windows PE 格式有良好的支持。

用 NASM 原生语法,导入动态链接库函数不需要以下划线 _ 开头,比如以下:
这里插一嘴:我推荐使用这个链接器,方便且轻巧。

官网地址 | Golink 下载

extern printf

section .data
    msg db 'Hello, World!', 0 
 
section .text
    global main
 
main:
    push msg
    call printf
    add esp, 4
    ret

lp0ilrjy.png

/*
  ------ 参数说明
  /entry 指定入口函数
  /console 指定为控制台程序

  ------ 编译命令
  nasm -f win32 main.asm -o main.obj
  golink /entry main main.obj msvcrt.dll /console
*/

简单的窗口程序

lp3m0e69.png

/*
  ------ 编译命令
  nasm -f win32 window.asm -o window.obj
  golink window.obj kernel32.dll user32.dll
*/

隐藏DOS窗口

/* 
  ------ 参数说明
  -subsystem windows 即可隐藏DOS窗口

  ------ 编译命令
  nasm -f win32 main.asm -o main.o
  ld main.o -o main.exe -lkernel32 -lmsvcrt -luser32 -subsystem windows
*/ 

lnzhq8i3.png

生成图标程序

/* 
  ------ 文件生成
  创建文件 resource.rc
  内容:IDI_ICON ICON "mu.ico" 内容写到里面
  使用 windres 生成资源目标文件
  打包资源命令:windres resource.rc resource.o 

  ------ 编译命令
  nasm -f win32 main.asm -o main.obj
  ld main.obj resource.o -o main.exe -lmsvcrt
*/

lnvjg1wr.png

添加UAC清单

创建UAC清单文件:创建一个XML文件名为 nac.manifest 内容填写以下代码。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

创建资源文件:编写一个资源脚本文件名为: src.rc 用来引用清单文件,内容填写以下代码。

#include "winuser.h"

1 RT_MANIFEST "nac.manifest"
IDI_ICON ICON "00.ico"

这里我添加了自定义图标在 src.rc 写了 IDI_ICON 当然不需要也可以去掉。

/*
  ------ 文件生成
  nasm -f win32 main.asm -o main.obj
  windres src.rc -O coff -o src.res

  ------ 编译命令
  ld -o main.exe main.obj src.res -lkernel32 -luser32 -lmsvcrt
*/

lou1tff1.png

vmp加壳问题

lp0jl7ks.png

如果你尝试用 alink 进行链接生成程序,就会发现区段的命名方式,不以 . 开头,导致加壳的时候识别错误。

lp0jkuq9.png

而使用 ld、gcc、golink 进行连接就不会出现这种情况了,就能正常加壳了。

调用第三方DLL

lp96ouxh.png

这里我写了一个自定义的dll,里面就导出了一个 initGo 函数,功能是弹出一个弹窗。

section .data
    dllName db "Bin32.dll", 0
    functionName db "initGo", 0

section .text
    extern LoadLibraryA, GetProcAddress
    global main
    
main:
    push dllName
    call LoadLibraryA
    mov ebx, eax  ; DLL 句柄

    push functionName
    push ebx
    call GetProcAddress
    mov esi, eax  ; 函数指针

    call esi      ; 调用 initGo 函数
    mov ebx, 0
    ret

lp96u0zl.png

/*
  ------ 编译命令
  nasm -f win32 main.asm -o main.obj
  golink /entry main main.obj Bin32.dll Kernel32.dll /console
*/
958

评论 (0)

取消
0:00