第四篇:分析项目入口–main.asm

admin 2026-01-26 02:05:54 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了main.asm的入口代码,通过汇编与C语言对比讲解了程序初始化、主循环及重连机制。核心在于构建无限死循环以维持连接,利用Sleep函数降低CPU占用。文章对比了阻塞IO与轮询的区别,解释了重连逻辑的必要性,为红队工具开发或恶意代码分析提供了基础框架,帮助理解底层运行逻辑。 综合评分: 95 文章分类: 二进制安全,恶意软件,红队,安全开发,逆向分析


cover_image

第四篇:分析项目入口 – main.asm

原创

RabbitQ RabbitQ

安全研究站

2026年1月25日 18:43 北京

3.1 整体流程图

3.2 主要功能

完整代码我会放到github上:code/main/main.asm(汇编版本)和 code/main/main.c(C语言版本),这里我会通过C语言与汇编语言对照来讲清楚具体都在干嘛

准备工作

汇编代码

main:
    push rbp
    mov rbp, rsp
    sub rsp, 32

    lea rcx, [rel msg_debug_main]
    call printf

C 语言代码

int main() {
    printf("[*] Main start....\r\n");

在汇编中,程序开始时需要建立函数栈帧。这三条指令的作用是:

  1. push rbp:将上一个函数的 rbp 保存到栈中,这样函数返回时可以恢复
  2. mov rbp, rsp:将当前栈指针赋值给基址指针,作为当前函数的基准
  3. sub rsp, 32:分配32字节的栈空间,这是Windows x64调用约定要求的”影子空间”

而在 C 语言中,这些工作完全由编译器自动完成,程序员不需要关心,这就是高级语言的优势之一。

主循环

汇编代码

main_loop:
    ; 打印循环调试信息
    lea rcx, [rel msg_debug_loop]
    call printf

    ; 检查连接
    cmp byte [rel is_connected], 0
    je reconnect

    ; 接收和处理命令(模拟)
    lea rcx, [rel msg_debug_recv]
    call printf

    lea rcx, [rel msg_debug_exec]
    call printf

    lea rcx, [rel msg_debug_send]
    call printf

    ; 休息
    mov rcx, 1000
    call Sleep
    jmp main_loop

C 语言代码

main_loop:
    printf("[*] Main loop iteration\r\n");

    // 检查连接状态
    if (is_connected == 0) {
        goto reconnect;
    }

    // 接收和处理命令(模拟)
    printf("[*] Receiving command...\r\n");
    printf("[*] Executing command...\r\n");
    printf("[*] Sending response...\r\n");

    // 等待1秒(避免占满CPU)
    Sleep(1000);

    // 跳回循环开始
    goto main_loop;

主循环是整个程序的核心,它的执行流程如下:

  1. 打印调试信息:首先输出一条消息,表示程序正在运行。这对调试很有帮助,可以确认程序是否正常工作。
  2. 检查连接状态:使用 cmp 指令比较 is_connected 变量的值。如果值为0(表示未连接),则跳转到 reconnect 标签进行重连。
  3. 接收和处理命令:在正常连接的情况下,程序会依次调用三个函数来模拟命令处理流程:
  • 接收命令
  • 执行命令
  • 发送响应
  1. 睡眠1秒:调用 Sleep(1000) 让程序暂停1秒。这样做有几个好处:
  • 避免100%占用CPU
  • 降低程序被发现的风险
  • 为系统节省资源
  1. 跳回循环开始:使用 jmp main_loop 无条件跳转回循环开始,形成一个无限循环。

这个循环会一直运行,直到程序被强制终止(如按Ctrl+C),接下来讲讲这个死循环是咋实现的,先看汇编代码:

main_loop:           ; ← 这是标签
    ; 做点事...
    jmp main_loop    ; ← 跳回标签

然后是C代码:

main_loop:
    // 做点事...
    goto main_loop;  // ← 跳回标签

这里其实就是放那一个标签,就这玩意就像书签,jmp(或 goto)就是”跳到那个书签”,注意昂,我们在正常的写代码的时候,这玩意一般这么写:

while(1){
    // 做点事...
}

有个问题不知道你们看代码的时候有没有琢磨,为啥要休眠,直接放代码,汇编代码:

mov rcx, 1000    ; 参数:1000毫秒 = 1秒
call Sleep       ; 调用Windows API

C 语言代码:

Sleep(1000);  // 1000毫秒 = 1秒

如果不睡眠,死循环会一直空转,CPU占用会达到100%。这会导致系统变慢,其他程序响应迟缓,睡眠可以让程序让出CPU时间片,降低占用,给其他进程运行的机会。我的粉丝里面搞红队的可能比较多,可能会有人问我,为啥CS在执行sleep 0之后不会有我刚说的问题呢,其实这里是它使用了阻塞式网络通信(如ReadFile、ConnectNamedPipe),这些API在等待数据时会将线程挂起而不占用CPU,即使循环中有Sleep(0),线程大部分时间都处于阻塞等待状态,不会像普通程序那样疯狂轮询。简单来说:普通程序没有真正的阻塞点所以会空转占CPU,而Beacon在等网络数据时线程是挂起的,不占CPU时间。

重连

汇编代码

reconnect:
    ; 打印重连消息
    lea rcx, [rel msg_debug_reconnect]
    call printf

    ; 模拟重连(简化版)
    mov byte [rel is_connected], 1

    mov rcx, 5000
    call Sleep
    jmp main_loop

C 语言代码

reconnect:
    printf("[*] Connection lost, attempting to reconnect...\r\n");

    // 模拟重连(这里简化处理)
    is_connected = 1;

    // 等5秒后重试
    Sleep(5000);

    // 跳回主循环
    goto main_loop;

重连逻辑用于处理连接断开的情况,其执行步骤如下:

  1. 打印重连消息:向用户输出一条消息,告知程序正在尝试重新连接服务器。这在实际运行时可以帮助了解程序的状态。
  2. 模拟重连:在完整版本中,这里会调用网络函数重新建立与服务器的连接。但在这个简化版本中,我们直接将 is_connected 变量设置为1,表示连接已恢复。这样可以演示程序逻辑,而不需要实际的网络通信。
  3. 等待5秒:使用 Sleep(5000) 让程序暂停5秒。设置这个延迟有两个重要原因:
  • 避免程序在短时间内频繁尝试重连,这可能会对服务器造成压力
  • 给网络环境一个恢复的时间,如果服务器只是暂时不可用,稍后再重连可能会成功
  1. 跳回主循环:使用 jmp main_loop 返回到主循环,让程序重新检查连接状态并继续正常工作。

这种自动重连机制就是为了防止网络出现波动、Server临时重启等原因导致连接断开的情况,即使出现问题程序也能自动恢复运行。

退出

汇编代码

exit_implant:
    lea rcx, [rel msg_debug_exit]
    call printf

    ; 清理资源(简化版省略)

    mov rcx, 0
    call ExitProcess
    ret

C 语言代码

exit_implant:
    printf("[*] Shutting down...\r\n");

    // 清理资源(简化版)
    // 无代码

    // 退出程序
    ExitProcess(0);

    return 0;

退出处理部分的执行步骤如下:

  1. 打印退出消息:输出一条关闭消息,通知用户程序即将结束。这有助于确认程序正常终止。
  2. 清理资源:在完整版本中,这里会执行以下清理操作:
  • 关闭网络套接字连接

  • 释放动态分配的内存

  • 清理其他系统资源

但在简化版本中,这些步骤被省略了,因为我们没有实际创建这些资源。

  1. 调用 ExitProcess:使用 Windows API 的 ExitProcess(0) 函数终止程序。参数0表示程序正常退出(非0通常表示错误)。注意:ExitProcess 是 Windows 特有的函数,它会立即终止整个进程,不会返回。
  2. return 语句:在 C 语言代码中的 return 0; 语句实际上不会执行,因为 ExitProcess 已经终止了程序。但按照 C 语言的规范,main 函数应该返回一个整数值,所以这里还是保留了这个语句。

在当前的程序结构中,这个退出代码实际上永远不会被执行到,因为主循环是无限循环。程序只能通过外部方式(如按 Ctrl+C 或任务管理器结束进程)来终止,在完整的实现中,会添加一个特殊的退出命令(我用的是exit,你随便叫什么,叫aaa都行,这里后面再说)来让程序正常退出。

补充

虽然之 前讲过函数调用,但是还是再说一嘴吧,call 做了什么:

  1. 把”下一条指令的地址”压栈(返回地址)
  2. 跳转到 receive_command
  3. receive_command 执行完后 ret
  4. ret 从栈中取出返回地址,跳回来

大概是下面这个情况:

主程序:
    [指令1]
    call func  ← 这里
    [指令2]    ← 返回到这

函数:
func:
    [函数体]
    ret        ← 返回

而我们在写C编译器自动处理这些细节,只需要写函数名加括号

3.3 实际运行示例

使用我之前发的build.bat编译 code/main.asm 后运行,屏幕输出:

使用gcc main.c编译 code/main.c 后运行,屏幕输出:

3.4 本章总结

这一章把 main.asm 的核心结构讲完了,其实就几件事:

先初始化建好栈帧,然后进入主循环,主循环就是死循环,一直检查连接、收命令、执行、发结果,最后睡一秒再继续。连接断了有重连逻辑,等5秒再试。退出代码现在用不到,但以后加个 exit 命令就能用上了。

几个关键点:

  • 无限循环不是bug,是feature,这样才能一直等着接收命令
  • Sleep(1000) 很重要,不然CPU直接干到100%,系统都卡死
  • 重连机制让程序更稳定,网络抖一下自己就能恢复

好了第三章就到这,下一章讲工具函数~


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:安全研究站 RabbitQ RabbitQ《第四篇:分析项目入口 – main.asm》

评论:0   参与:  0