「私のBAND」页面,收集我与乐队的演出视频。
- 2024.05.18 徐汇光启城·二向迭音 K-OVER!
- 2024.05.05 复旦·五月雨二次元音乐会 K-OVER!
- 2023.12.03 复旦·2023冬沸点祭 K-OVER!
「私のBAND」页面,收集我与乐队的演出视频。
参考以及题目附件见:Black Hat 2023 0解Pwn题Houseofminho详细WP - Csome
本篇 Writeup 基于参考文章,但对攻击脚本作了一些优化(去除了一些意义不明的代码),并着重于把攻击思路理清楚(原文的思路太跳跃了,并且有一些地方和我的见解不太一样)。
我总是觉得基于hexo的博客更新起来并不是很方便,所以仿照某位同学又搭了一个新的站点,用来存放我的笔记。入口就在顶部的导航栏里,叫做Notes
。
以后博客站点就用来放一些文章、杂记、随笔之类的东西了。
第一次从qemu里面逃出来,但没有完全逃出来,远程没通比赛就结束了S.H.I.T
题目链接:xtxtn/vnctf2024-escape_langlang_mountain2wp (github.com)
关于qemu pwn入门,网上中文资料非常多:
理想的环境是 qemu 内的系统有 ssh,这样就可以直接连上去,甚至使用 scp 传 payload,但是这题没有。
我采用的调试方法是在 Dockerfile 中加一个 gdb,这样就可以在 docker 中调试,但是最佳的调试方法应该是往 docker 里面塞一个 gdbserver,然后用主机的 gdb attach 上去,这样就可以使用主机里的插件。
题目实现设备提供了 vn_mmio_read
和 vn_mmio_write
两个函数。
1 | __int64 __fastcall vn_mmio_read(const char ****a1, __int64 a2) |
object+0xb80
用来保存一个偏移,该函数可以根据缓冲区的相对偏移读数据。
1 | void __fastcall vn_mmio_write(const char ****a1, unsigned __int64 a2, unsigned __int64 a3) |
write 中提供了三个功能:
在检查偏移变量的大小时,由于检查类型是 signed,因此可以把偏移修改为一个负数。于是我们就可以有无限次的任意相对地址读,以及一次任意相对地址写入。
整体思路:
由于我第一次打 qemu pwn,对于其中各种结构体都比较陌生,所以我直接用本办法,在动态调试的时候查看 Buffer 前面的数据,从里面找到可以泄露的指针。(从而给后面本地打得通远程打不通埋下了伏笔)
在不清除结构体信息的情况下,找泄露的时候需要注意一些查找要点:
根据这种方法可以找到两个指针,然后泄露即可。
当然,如果你是一位对设备的 Object 结构体比较熟悉的 qemu pwn 大师,那么你就可以直接泄露结构体的某些字段来泄露程序和堆的地址。具体来说,可以通过 MemoryRegion 结构体:
1 | struct MemoryRegion { |
其中,ops
指向 data 段的 vn_mmio_ops
,opaque
更是指向 vn 的设备结构体,因此泄露这两个指针就可以准确泄露地址,不用担心什么偏移不一样的问题。
在网上可以找到的大部分 pwn 题中,设备本身就有一些函数指针,劫持它们就可以劫持控制流(甚至参数),但本题的设备就是单纯的读和写,并没有什么 encode
、rand
之类的函数。因此,本题需要一个通用的控制流劫持方法。
在 Qemu 中,可以通过注册一个 QEMUTimer 来让 qemu 在一段时间间隔之后调用一个函数,参数为一个 opauqe 指针。相关结构体定义如下:
1 | struct QEMUTimer { |
从内存视角看两个结构体长这样:
1 | struct QEMUTimer { |
在 bss 段有一个数组 main_loop_tlg[4]
,保存了一些 QEMUTimerList
结构体指针,每个 active_timers
都指向一个由 QEMUTimer
结构体组成的链表。qemu 会遍历这些 QEMUTimerList
来检查所有 QEMUTimer
有没有超时并调用它们的 callback 函数(也就是调用 timer->cb(timer->opaque)
,相关源码见qemu-timer.c - util/qemu-timer.c - Qemu source code (v4.2.1) - Bootlin)。
因此,我们可以在通过 main_loop_tlg
泄露某个 timerlist 的地址后,劫持它的 active_timers
指针并伪造一个 QEMUTimer
结构体,从而控制程序调用函数以及参数。
伪造 QEMUTimer
时,可以这样写:
1 | timer->expire_time = 0x114514; |
这样程序就会在 0x114514 纳秒之后调用 system("cat flag")
。
该方法主要参考了:
没有在在线环境下试过这个脚本,不过猜测在线问题不大==。
1 | #define _GUN_SOURCE |
这里记录从 12.05 开始,我如何尝试实践各种方法来完成各种期末任务。
希望自己能做到高效应试!
主要依靠的资源包括:
我在本学期中选课情况、期末时掌握情况和等地如下:
课程名称 | 学分 | 考试方式 | 考试时间 | 掌握情况 | 等地 |
---|---|---|---|---|---|
概率论与数理统计 | 4 | 闭卷 | 2024-01-02 | 前三章略懂,课全翘了 | B |
计算机病毒及其防治 | 2 | 闭卷 | 2023-12-18 | 课全翘了 | B |
信息内容安全 | 2 | 闭卷 | 2023-12-20 | 课全翘了 | B |
操作系统 | 3 | 闭卷 | 2023-12-28 | 课全翘了,懂一些基础 | A |
计算机取证 | 2 | 闭卷 | 2023-12-21 | 课全翘了 | A |
无线网络及安全 | 3 | 开卷 | 2023-12-22 | 课全翘了 | A- |
信息系统安全 | 3 | 闭卷 | 2023-12-18 | 掌握还行 | A |
物理学的新启示 | 1 | 论文 | 课全翘了 | P | |
马克思主义基本原理 | 3 | 开卷 | 2023-12-19 | 上了一些课 | B+ |
注意到其中有八个学分的 B,我认为我自己已经尽力了,但这些困难是很难克服的客观原因。一个是绝对成绩给分(我考完才知道,气死了);一个是考一堆超纲考题,这些超纲的题还是选了别的课的同学会做的;一个是课程难度比别的班都要高,速通确实很难搞定(但选别的班就完全能搞定了艹)。
接下来会按照课程顺序来划分章节。
概率论虽然我的目标是 B+,但我认为是最难的一个考试了。为此,我一开始准备选择首先用猴博士速通,然后进入补刷作业题与对应知识点补齐环节,但是后来发现实际的准备时间只有三天左右……
我的复习就是在去年的卷子(来自开放题库)、猴博士的视频以及教材之间来回切换。我是否要学习教材上的内容取决于去年的卷子有没有考到相关内容、猴博士的视频有没有出现相关内容、以及课后布置的作业相关度。于是在省略了很多内容没有复习的情况下,终于在三天内学完了大部分概率论。
关于最后一两章,由于有太多需要背诵的内容,所以就直接不复习了。
最后考试的时候发现,和去年的卷子几乎考得一模一样,所以我准备的都做出来了,混了个 B。
我觉得如果我们班级和别的班考纲一样的话,我能混个 B+。郭大爷这教得也太多了,作业也多,不愧是高级概率论……
花了一天,借用 DOSBox 的力量速通了。
最后一节课老师说了考试重点,都是死记硬背,没有需要理解的东西。
周一的考试,周六把需要背的东西整理出了一个 word 文档,大概花了两三个小时;周日再复习了一下,手抄了其中部分感觉会考的东西。
考试估分 90+ 肯定有,据老师说 A 类要考 85 以上,考虑到我平时分是 3/10 分的话(就没上过课),理论上我拿 A 类需要 95 以上。所以估计还是 B+ 吧。
然后最后是 B,因为老师按照绝对分给分。当时我为什么想不开选这个课呢,真是依托答辩……
我在组内负责了数据清洗以及部分可视化任务,非常非常边缘,真的感谢队友大佬orz。
数据清洗继承了学长的 PJ 代码,进行了优化重构,把写得稀烂的正则表达式简化了一大截(主要用于知乎)。
1 | def wash(content): |
可视化任务,我使用官方 GPTs Data Analysis 进行辅助,效率惊人,只需要拖入 json 文件并指定需求就可以由 AI 自动写代码进行读取文件、可视化的代码编写和执行,比如下面代码完全由 GPT 生成:
1 | import json |
老师在最后一节课上划了期末重点,根据该重点和去年的卷子(lyr 天使!)复习。资料是一本纸质教材。
主要难题在于,复习时间只有一个晚上,第二天早八就考试了。这一部分复习没办法依靠 AI 了,主要依靠人类的力量速通!!
然后发现这门课考试真的离谱,考点都不考,重点全是说不考的。最后一章说是不考数学模型只要理解原理,结果考了个 26 分的压轴计算题。
听朋友说有很多同学哗哗写完了,应该是在别的课学过公式。但是这门课没教具体算法啊!!! 书上和 PPT 上都没有,我算得来才怪了。
最后遗憾拿 B,死因就是卷子实在是出得太逆反了,我认为我的复习很完美没啥问题了……
我们做的是基础取证工具接入 GPT 智能判断功能,以及基本的数据关联。
基础的取证功能有
使用 GPT 生成基础代码框架和核心功能后,使用 copilot 速通 UI 编写。几小时就速通了。
花了几个小时复习 PPT 。
最后闹出了乌龙,由于我 PPT 是 11 月下的,那时候老师还没传全,所以我漏看了整整 1/4 的 PPT。怪不得考试的时候碰到好多不会的……不过还是拿了 A。
考前老师划了一些重点,还放了个 429 页的复习 PPT……
根据重点和 PPT 进行复习,时间在三天左右。
最后熬了个大夜,终于速通完了那个 PPT,然后把四个往年卷子都看了一下。考试的时候感觉不错,没啥不会的题目,果然拿了 A。
准备课程 PJ 的时候,留给我的只剩下一天+一个晚上了,这个时候正好看到 G.O.S.S.I.P 公众号上发了一篇蓝牙的综述性文章介绍,然后就放弃了实验,写了一个论文阅读笔记一类的报告。
根据课件和去年卷子(感谢 wqh 天使)进行复习。
这确实是一门硬课,所以我的目标是在有限的时间内大致过一下课件,达到知道什么去哪里找的水平之后,将课件整理成便于打印的格式,现场查阅和学习。
考试的时候所有的题都写出来了,开卷考确实还是对记忆力差的人比较友好。
虽然翘课翘了很多,但由于已经对所学知识有所了解,所以选择了速通所有课件并做往年卷子的方法。去年的卷子在开放题库上找到了。
大概花了一天进行速通,知识点和需要记得其实并不多。技巧性的东西集中在栈溢出相关知识,这些我本来就会,毕竟课后时间都花在这上了。
考试一小时速通了,估分满分。
看了四集之后,用 GPT 生成了一段读后感,然后从看得几集里面加了几个例子进去,再加了几句自己的感想。
GPT 半小时速通,虽然质量狗屎。
为了解决没有引用的问题,让 GPT 帮我加了一些例子,然后我去手动给这些例子寻找引用。比如引用什么国家统计局数据。
没啥好说的,就瞎写写。没复习。
GPT-4 支持直接阅读课件。我将每个老师的课件转换为大小较小的 PDF 格式,并扔给 GPT 进行总结。注意为了凑字数,需要一个问题一个问题问 GPT,不能让它一次答全。
最后花了大概两三个小时速通。
确实是非常累,不过感觉我能做的都已经做了,结局也还算满意。不过要是这个学期选课的时候能多获得一些信息的话就更好了。
哈哈,第一次打内核题,虽然是xv6但还是感觉非常酷。比赛结束前才想到了真的可行的思路,赛后结合官方 writeup 调出来了。(本篇博客没有写调试的技巧,就只写了题目相关的一些思路。)
本程序由教学操作系统 xv6 改编而来,是一道 RISC-V 内核漏洞利用题。
在 xv6 中,没有地址随机化机制。但有着页表权限保护,也就是 R/W/X 权限位;并且在 xv6 通过 ecall 进入 supervisor mode 时,会将页表切换到内核页表,从而屏蔽对于用户内存地址的访问。
题目的目标是读出位于内核的数据段中的 flag,出题人贴心地给出了一个 backdoor
函数来帮我们读出 flag:
1 | .text:000000008000620C # public backdoor |
但不幸的是,内核中为 flag 所处的内存提供了额外的 PMP(Physical Memory Protection)保护,这里只是简单介绍一下用途,具体细节可以去阅读 RISC-V 特权手册(riscv/riscv-isa-manual: RISC-V Instruction Set Manual)的对应章节(位于 Machine-Level ISA, Version 1.12 中)。
在 RISC-V 中有三种权限等级:通常机器启动时处于 machine mode、内核运行在 supervisor mode、用户程序运行在 user mode。
PMP 是一种由 machine mode 进行设置和修改的保护,可以给某段内存设置可读、可写、可执行等权限,并对 supervisor mode 和 user mode 生效。
在 start
函数中,内核为 flag 所在内存添加了不可读、不可写、不可执行的权限保护:
1 | .text:00000000800000D8 la a5, flag # "HITCTF2023{true_flag_on_server}" |
因此,就算我们直接在内核中调用 backdoor 函数,也只能看到一个报错而不是 Flag。(做题的时候以为马上出 flag 了,然后就遇到了禁止访问的报错,一时很难绷住)
我们想要读出 flag,就一定需要处于 machine mode 中,或者在 machine mode 中将保护关闭,但可以通过搜索 pmpaddr
的方法发现程序本身并没有提供关闭保护的功能(
第一个比较明显的漏洞,就是新添加的系统调用 sys_encrypt
中存在的栈溢出漏洞,官方 wp 中提供的函数源码:
1 | uint64 sys_encrypt(void){ |
虽然函数的第一个 copyin
处对大小作了检查与限制(0x100),但第二个循环 copyin
很容易就会导致溢出。只要合理构造参数,我们就可以通过 bufff
溢出到高位。
第二个漏洞是页表的权限保护不当问题,原版 xv6 是没有这个问题的,而作者为了让题目能打所以手动改出了一些漏洞。
首先是内核代码可写。映射内核页表的代码位于 vm.c-kvmmake()
中,本来是长这样的:
1 | ... |
经过魔改后变成了这样(C 以及对应汇编):
1 | kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_W | PTE_X); |
1 | .text:00000000800011FA la s2, etext |
其次是进程内核栈可执行,代码位于 proc.c
中,原来长这样:
1 | // Allocate a page for each process's kernel stack. |
经过魔改后变成了这样(伪代码以及汇编):
1 | kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W | PTE_X); |
1 | .text:00000000800018CA li a4, 1110b |
因此,结合 xv6 没有随机化的特性,我们可以在栈上打 shellcode,且连 NOP Sled 都不用嘿嘿。
最初的步骤:如何构造 sys_encrypt
参数、以及劫持返回地址、以及栈上的 shellcode 执行略去不表(算一下调一下就好 hhhh)。这里就假设我们已经可以任意执行代码了。
为了绕过 PMP 保护,我想到了几种思路:
由于 Flag 是硬编码在 kernel 文件中的,因此我首先想到的办法是泄露服务器端的 kernel 文件。但是 xv6 在生成文件系统的时候,并不会将 kernel 放在里面。
我们使用 qemu 启动 xv6 时直接指定了编译好的 kernel,qemu 会把 kernel 直接加载到内存中。
所以在 xv6 系统内,内核只存在于内存中且独一无二,这种方法被证实是不行的(
在阅读 RISC-V 手册时,我注意到在 machine mode 中有一个小章节介绍了 Reset 。此时我已经发现了内核代码是可修改的,因此我想到的办法就是将 start
函数中对 flag
施加保护的代码覆写为 nop
,然后进行重启,这样重启后的系统就不会再有对 flag
的保护。
重启并不是 CPU 负责的事(CPU 负责的都是计算),准确来说并不是一个指令集所关心的事情。通常,重启是通过 CPU 向主板设备发送信号来完成的。
在题目环境中,启动 qemu
时指定使用的主板是 virt
,一个只具有最基础功能的主板,其描述见 ‘virt’ Generic Virtual Platform (virt) — QEMU documentation。
如何知道这个设备能否重启、如何重启呢?反正上面这个文档里我没找到(悲)。
我在 qemu 的源码中找到了该主板设备负责注册重启功能的函数:qemu/hw/riscv/virt.c at master · qemu/qemu (github.com),具体来说是如下几行:
1 | qemu_fdt_setprop_cells(ms->fdt, name, "reg", |
从这里的代码我们可以大致猜到,映射所采用的是 mmio 方法,地址 VIRT_TEST
偏移 0 处,如果写入 FINISHER_RESET
的话就可以进行重启。借助 Github 右栏的引用查找功能,不难找到 VIRT_TEST
的值为 0x100000
,FINISHER_RESET
的值为 0x7777。
经过测试,确实可以通过这个方法来 reset 机器。但是我悲伤地发现在 reset 之后,我对内核代码做的修改也一起 reset 了。看来 qemu 每次 reset 都会重新加载一遍 kernel 文件啊。
在测试完上面那种方法不可行后,我就想到了这个方法,但此时离结束比赛只剩下半小时,因此非常可惜没有做完。(后来看官方的 wp 又得知了一些 trick,说不定我自己调也还要调半天)
既然 PMP 只有 machine mode 可以操控或无视,那么我们的目标就是想方设法进入 machine mode。
正好,xv6 对于 timer interrupt 的处理是位于 machine mode 中的。具体来说,会在 start
函数中调用 timerinit
,来将 timervec
函数注册到 mtvec 中:
1 | // arrange to receive timer interrupts. |
因此,实际上内核在启动进入 supervisor mode 之后,唯一使用 machine mode 执行的代码就是这个 timervec 函数了,实现位于 kernelvec.S
中:
1 | .globl timervec |
这是一个会不定期被触发的、位于 machine mode 中的函数,这个函数的实现位于内核中,且是可以修改的。
所以我们劫持这个函数调用 backdoor,就可以让 backdoor 函数在 machine mode 被执行了,从而打印出 Flag。
(这里本来脑子没转过来,想的是让 timervec 把 PMP 给关了,然后我自己调用 backdoor,但这种方法增加了复杂度,不如直接调用 backdoor 简洁)
此外还有一个注意点,就是在进入 timervec 之后,需要使用 csrw mie, x0
(machine-mode interrupt enable)来关闭 machine mode 的各种中断。否则,在读 flag 读一半触发这个中断就不好了。(看官网 wp 学到的)
我的 exp 如下:
1 | #include "kernel/types.h" |
另外还有一个坑,就是 backdoor 函数前 N 句汇编是一些栈相关操作,我们需要跳过这几句汇编。否则内核会卡住不动!太坑了!
Linux系列第一篇!
本期介绍Linux中的权限模型,从 ls -l
的解释一路科普到内核漏洞的利用(什么!)
由于 Linux 设计时是一个多用户系统,可能有很多人共用一个 Linux 系统,因此 Linux 中存在用户和用户组的概念,每个用户或者用户组都有一个自己的 id,每个用户可以属于多个用户组。
有了用户之间的区分,就可以为文件设置权限,限制不应该访问的用户的访问,于是就有了权限系统。
用户和用户组这两个抽象的概念,其实主要体现在两个地方:
可以使用 getuid()
系列系统调用获取当前进程的用户 id,在 shell 里可以直接输入 id
查看当前 shell 进程的用户和用户组 id。(这里先不提及 id 的区别,后面再进行讲解)
使用命令 ls -l
可以查看文件的详细信息,比如:
1 | $ ll |
我们以第一条为例:
文件类型 | 权限信息 | 连结数 | 拥有者 | 用户组 | 文件大小 | 修改日期 | 文件名 |
---|---|---|---|---|---|---|---|
d |
rwxr-xr-x |
4 |
cameudis |
cameudis |
4096 |
Mar 24 2023 |
.cargo/ |
第一个字段文件类型包括以下这些(从这里也可以看到万物皆文件的思想):
字符 | 文件类型 |
---|---|
- | 普通文件 |
d | 目录 |
| | 符号链接 |
p | named pipe |
c | 字符设备 |
b | 块设备 |
s | socket 文件 |
我们可以看到,每个文件都会有一个所属的用户、一个所属的用户组。相应地,一个文件的权限设置会有三档:对于所属用户的权限、对于所属用户组中用户的权限、对于其他用户的权限。在一些权限设置工具 chmod
中,这三者分别简称为 U
G
O
,即 User、Group、Others。
在 ls -h
看到的信息中,我们看到的 rwxr--r--
字符串,其实就类似一个 bit vector。前三个字符表示对于所属用户的权限,中间三个表示所属用户组中用户的权限,最后三个字符表示对于其他用户的权限。
比如,.cargo
目录归 cameudis 所有,那么 cameudis 作为拥有者,其权限是 rwx
(Read、Write、eXecute);而另一个用户 Jern,若他不属于 cameudis 用户组,那么他的权限是 r--
(Read only)。
你可能会好奇,为什么 Linux 下各种目录大小都显示为 4096:这是硬盘中用来存储目录 metadata 信息的大小,这些 metadata 有:
如果你需要计算目录大小,可以使用du
指令,比如du -sh /tmp
。
一句废话就是:如果想要修改文件的权限,你必须拥有文件的权限。
在命令行中,我们最常使用的修改权限工具是 chmod
。
在 chmod
中,最简单的用法就是:chmod <+/-><r/w/x> <filename>
,这样会给用户、组和其他人通通加上或减去某个权限,比如:chmod +x a.out
就能让所有人都获取执行该文件的权限。
在此基础上,还可以特别指定某一群体:chmod [ugoa...]<+/-><r/w/x> <filename>
。比如 chmod u+x a.out
就可以只给拥有者执行该文件的权限。
不过,根据笔者观察,大家最常用的用法是直接使用数字指定。我们知道每个文件有三组权限,所以可以用三个 3 比特的值来分别表示一个文件的三组权限。在这个 3 比特的值中,约定最高位表示 r,中间一位表示 w,最后一位表示 x。所以,111
就对应 rwx
,010
就对应 -w-
。
然后,我们再将其写为 8 进制,111
就会变成 7
(如果你硬要说是更大的进制也可以),这样我们就可以用一个阿拉伯数字表示一组权限。
再将其推广一下之后,就可以用三个数字表达三组权限,我们列出一些经常用到的权限作为例子:
权限编码 | 权限说明 |
---|---|
755 | rwxr-xr-x |
600 | rw——- |
644 | rw-r–r– |
777 | rwxrwxrwx |
chmod 使用这种语法来让我们快速指定权限:chmod 755 ./a.out
冷知识:
在著名动漫《新世纪福音战士新剧场版》中,明日香操纵 EVA 二号机进入野兽模式时,使用的指令是 “Code 777”,这说明 EVA 二号机运行的是 Linux 操作系统⊂彡☆))∀`)
常见的文件权限
目录权限通常设置为755。其中7表示rwx,5表示rx。这里,x权限用于进入目录,r权限用于读取目录;换句话说,若去掉某个目录dir的x权限,则cd dir会报错;若去掉r保留x,则可以进入这个目录,但在目录中运行ls会出错;没有w权限,表示不能在目录中删除或新建文件。注意,删掉一个文件并不需要该文件的w权限,而只需要文件所在目录的w权限。一个文档文件的权限通常设置为422,即没有x权限。符号链接文件的权限为777,因为真正起作用的是链接所指向文件的权限。(来自银杏书)
文件的权限被修改,对已被打开的文件会立即生效么?
考虑如下情况:在进程 A 打开某个文件时,该文件具有可写权限,因此进程 A 以可读可写权限打开了文件;然后,文件的权限被拥有者修改为只读,那么之后当进程 A 对文件进行写操作时,会成功还是失败呢?根据前一段的描述,进程 A 会一直拥有对文件的写权限,直到关闭该文件。若系统希望对文件的权限更新立即生效,则需要在更新权限的同时,遍历所有打开文件的 fd 并做相应的处理,例如直接关闭所有权限不匹配的 fd,这样进程 A 下次进行文件操作时就会出现错误。(来自银杏书)
每个进程都会有 UID 和 GID,且相关数据会继承给子进程(当然满足条件就可以修改自己的 UID 和 GID,只要符合一些要求,可以到对应系统调用的 man page 中查看具体要求):
1 | $ cat getuid.c |
一个进程并不是只有一个 UID(用户 ID)和一个 GID(用户组 ID),而是根据不同用途有多个。
在操作系统为进程准备的结构体中,一个进程包含如下几种 ID:
eUID 和 eGID 最为常用,进程是否能够打开文件等权限检查都使用 eUID 和 eGID,因此 id
指令默认显示的也是 eUID 和 eGID。(不过可以用 -r
参数指定显示 real UID/GID)。
之所以需要区分 effective ID 和 read ID,是因为在某些场景中,需要区分这两个 ID。我们设想这样一个场景(纯虚构,细节问题不要在意):
Jern 安装了一个 Web 服务器软件(假设服务器软件的所有者和用户组都是 Jern),想要让运维 Cameudis 也能够执行该软件,因此他就把软件文件的权限设置为
r-x
。
Cameudis 开心地启动!了 Web 服务器,但是访问网站时发现无法正常访问网页。原来由 Cameudis 执行的 Web 服务器进程,其 eUID 和 eGID 都是 Cameudis 的,因此这个进程没办法访问 Jern 放在目录中的 html 网页文件!
为了解决场景中的这一问题,一个方法就是再给 Cameudis 目录中所有的文件的权限。但这种方法的缺陷在于,万一程序还需要访问未知位置的一些目录,我们可能不能一直及时地给 Cameudis 权限。
另一个方法就是将 Cameudis 加入 Jern 所在的用户组。这种方法挺好的,不过需要具体情况具体分析下加入之后有没有潜在危害。
此外,还有一种 Linux 提供的方法,这种机制允许用户在文件中加入 SUID
、SGID
权限位(就和 RWX 一样),如下所示:
SUID
(Set-UID):当前文件被执行时,以文件拥有者 UID 作为 eUID 而不是父进程的 eUID。SGID
(Set-GID):当前文件被执行时,以文件拥有组 GID 作为 eGID 而不是父进程的 eGID。这里说当前文件被执行,显然默认了文件是可执行文件。不可执行文件被设置这两个位是可行的但并没有意义。
因此,Jern 可以给 Web 服务器的程序文件加上 SUID
bit,这样 Cameudis 执行 Web 服务器时,服务器进程会以 Jern 的 eUID
运行,从而就能够访问所有 Jern 本人可以访问的文件。
一个具体的例子就是 sudo
程序,我们可以这样查看其权限信息:
1 | $ which sudo # which 让shell查找某个程序的具体位置 |
可以看到,sudo 的 U
权限是 rws
,这里的 s 就表示 SUID
。我们在执行 sudo 时,会以 root 用户的 eUID
执行,从而能够访问高权限的资源。
使用 chmod 给文件加 SUID 和 SGID 的方法:
chmod u+s <file>
chmod g+s <file>
Sticky bit 主要用于目录,对于标记为 Sticky 的目录中的文件,只有文件的所有者与目录的所有者才能重命名或删除文件,其他行为则照常。
通常来说,我们用 Sticky bit 来保护一些共享的文件夹,这里的共享是指多个用户都会在这个文件夹中处理文件。比如,/tmp
文件夹就常常被置为 Sticky
,来防止普通用户删除或者移动其他用户的文件:
1 | $ ll / |
至于给非目录的普通文件置 Sticky bit,各类 Unix 系统的对待方式都不一样,比如 Linux 就是直接忽略置 Sticky 的文件。
使用 chmod 给文件加 Sticky 的方法:
chmod o+t <file>
以上说的各种权限限制,都对 root 用户无效。作为 Linux 系统中的真神,root 用户和用户组都拥有特殊的 ID 0。通常为了使用 root 的力量,我们会借用 sudo
这个 SUID
程序。
root 用户可以:
/proc
中一些高权限的文件如 kallsym
。简单来说,root 用户可以控制整个系统。
由于 root 的力量过于强大,所以任何一个略有安全意识的人,在平时都不会以 root 用户的身份执行指令,除非必要。相关反例实在太多了,几乎每个默认 root 用户登录的人都会因此出现一些问题。(笑死)
既然 root 的力量如此强大,那么可想而知,如果黑客拿到了我们机器的 root 权限,那会是多么可怕的一场安全灾难。因此,接下来我们学习如何作为黑客拿到 root 权限。
提权,一般就是指黑客将他们权限从普通用户提高到 root 的一类攻击,通常的提权流程是这样的:
什么是可以利用的高权限服务呢?
SUID
程序就是一种高权限的服务,如果它存在漏洞的话,我们就可以通过利用漏洞来达成提权,比如 sudo
就有过非常多的 CVE,可以攻击 sudo
的漏洞来拿到 root 权限。SUID
的程序,如果能够以 root 权限运行的话,会带来令人意想不到的安全风险。前者比较容易理解,就是普通的用户态程序利用而已,因此本文中我们主要介绍后两者。
如果你发现 mv
程序是 SUID
的,你可以做到哪些事情?
看起来我们只能移动一些文件,但实际上,每个常见 Linux 程序的功能都可能非常强大。比如就算是简单的 mv
指令,也可以做到彻底的提权。
mv
的提权方法,可以参考The Dark Side of mv
Command. mv, short for MOVE has been one of the… | by Nikhil Jagtap | WorkIndia.in | Medium
更多 binary 的提权方法,可以见 GTFOBins。
强烈推荐读者去 pwn.college 实地打几道题来试试看。
如果 /bin/sh 作为一个 SUID 程序运行,即 eUID 和 rUID 不同,那么它会主动降权限,将 eUID 设置成 rUID。这就是一种应对 SUID 提权的非常简单的缓解措施。
遇到这种情况,只要加上-p
参数即可。
我们知道,操作系统不过就是一个用户程序与资源的管理器而已,它同样也是一个由程序员写成的程序,操作系统也会有漏洞。
我们平常说的操作系统,通常包括了许多东西,比如桌面系统。不过,这里我们要关注的是一个操作系统真正重要的东西——内核(kernel,台湾称为核心)。
内核是一个运行在更高级别的程序,我们刚刚提到的各种机制,包括文件系统、进程系统,这些系统的实现统统位于内核之中。比如我们刚刚提到了 eUID
、Real UID
,这些东西统统都是内核中为每个进程准备的结构体中的一个字段。
如果读者学过 OS,那么就会知道用户态程序和内核交互的最常见的方法就是通过系统调用。因此,如果系统调用涉及的某段代码中存在漏洞,我们就能从用户程序攻击内核。(这就是为什么内核 pwn 的 exp 都是一个 C 程序,自己编写软件来攻击内核显然是最方便的)
提权是攻击内核最常见的目的之一。如果我们控制了内核,比如劫持了控制流,我们就可以去调用内核中存在的函数,将当前进程的权限提高至 root。
具体来说,我们一般会控制内核执行 commit_creds(prepare_kernel_cred(0))
,其中 prepare_kernel_cred(0)
会创建一个各个字段都是 0 的 cred 结构体,然后 commit_creds()
可以将当前进程的 cred 替换为参数。我们前面知道 root 拥有特殊的 id —— 0,因此调用完这两个函数后,就可以将进程权限切换为 root
。
此后进程就可以想干啥就干啥了,比如起一个 root shell。
当然,这里只是一个小科普,内核漏洞笔者还没有入门,希望读者里能有未来挖掘或利用内核漏洞的大能|∀`)
参考资料:pwn.college
本地打通了,远程……台湾太远了……爆破到一半就会不知道谁把我连接掐掉……
本题主要有两个漏洞,一个是检查密码时,根据用户的输入的大小(strlen)作为 strncmp 的参数进行比较,然而这样会导致用户输入 NULL Byte 就通过检查,同时还允许了一字节一字节爆破得到正确的密码;甚至泄露密码后面的别的数据——在本题中就是程序基址。
另一个是一个没有检查大小的 strcpy。
本题的流程就是先利用第一个漏洞来爆破得到栈上的密码以及 saved rbp,然后利用 strcpy 进行控制流劫持。由于 strcpy 限制 null byte 截断,所以我利用程序自己的 read wrapper 函数(CA0 处)来进行第二次写入,这次就可以写入 ROP chain。(这里调试得知 rdi 正好是栈上变量)
第一次写入 ROP chain,我泄露了 libc 的基址,让程序从 start 重头来过;第二次写入 ROP chain,我就直接执行 system("/bin/sh")
来拿到 shell。
1 | def pwn(): |
本题 neta 了星界边境,实现了一个简单的二维探索游戏。
1 | [*] '/mnt/c/Projects/ctf_archive/[pwnable.tw]Starbound/pwn' |
数组下标未检查导致的任意控制流劫持。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
在 main 函数中,有一个对于函数指针数组的调用,index 数据来自于用户输入经 strtol 转化成的数字。我们可以用 cmd_set_name
函数修改 data 段的数据,再让程序 call 我们控制的地址,达成 arbitary call。
有了任意调用,程序又没有开 PIE,接下来就是看看程序本体有哪些东西可以给我们来调用。
我在本体中,并没有找到 win 相关的函数,也没有找到导入的 system 符号,因此似乎没有简单的 ret2text 方法来完成一击必杀。
那就来打个 ROP 吧,我们可以直接用 main 函数 buffer 来存放 ROP 链,只要找一个类似于 add esp, xxx; ret;
的 gadget 即可。
使用这种方法,我们可以先用 puts
泄露 libc 基址,然后就能 system("/bin/sh")
了。具体利用见完整 EXP。
查 libc 版本用的是 libc-database,俄罗斯那个(libc.blukat.me)查到的结果贼少,不知道为什么。
ROP 方法是我不小心从网上看到的,唉我不应该上网查的。
不过我自己也想出了一个非常绝妙的利用,不需要用到 ROP!
我们已有的任意调用,其参数是固定好的,第一个参数是一个我们可控的字符串指针,第二个参数是 0。顺着这个思路,我们可以先看看程序本体中有哪些函数,其第一个参数是 char*
类型的。
首先,此类函数肯定是 printf 最常见也最好利用,我们可以用这种方法将任意调用宽展成任意读写,但程序开启了 FORTIFY 保护,里面甚至只有 _printf_chk
函数没有 printf
函数。两者的区别在于,后者其实是前者的一个 wrapper。
前者的第一个参数是一个安全等级,1 表示开启,0 表示关闭。当开启时,格式化字符串攻击将会被大大削弱,比如不能直接使用 %n$d
了,如果要用到这玩意,必须前面要有 %1$d
%2$d
… %(n-1)$d
这些。
因此,这条路走不通。
但我们就可以找到另外两个首个参数的—— mkdir
和 open
。既然有 open,就可以想想是不是能 orw 把 flag 读出来。但是,程序的漏洞处,相邻的两次触发之间隔了许多个函数调用,这就不允许我们把 open 返回值暂时放在寄存器中,这里就很难进行下一步操作。
但是,我把整个 binary 都审了一边,发现了一个有趣的机制:
1 | int cmd_multiplayer_enable() |
在 cmd_multiplayer_enable
中,有对于一个全局变量 fd
的赋值。而我们知道,进程打开的第一个文件往往是接在 stderr
的后面,也就是 fd == 3。
我们可以观察到,程序在使用 close 关闭 fd 之后,并没有清空 fd 的值,也就是这里依然是 3。实际调用这个函数,发现程序肯定可以走到关闭 fd 的代码。
我们查找 fd 的应用,可以找到这里:
1 | int cmd_multiplayer_recvmap() |
这里程序将会尝试从 fd 中读取内容,每一个字节都使用 rotate_shift_add_decrypt
函数进行加密,然后打印出结果。
于是我们可以想到一条利用链:
cmd_multiplayer_enable
,让 fd 被置为 3;open
函数打开 flag;rotate_shift_add_decrypt
,读取加密后的 flag 并输出;但我们会遇到一个问题:虽然我们可以控制第一个参数这个字符串,但是其开头被限制了是一个数字,因为我们就是用这个数字当作数组下标来实现任意调用的。
为此,我想到了一种借用 mkdir
来加强 open
的方法:
mkdir("-33\0")
在当前目录创建名为 -33 的文件夹;open("-33/../flag\0")
打开任意目录下的 flag。在本地,这种方法是可行的。然而,远程环境中执行 binary 的路径是根目录,而进程并没有在根目录创建文件夹的权限,因此这种方法很遗憾地失效了 : (
1 | #!/usr/bin/python3 |
函数数组和数组下标都是非常危险的东西——前者容易被劫持,后者容易超越边界。
本漏洞修补十分简单,只需要加上一个检查就可以了。
从这道题目的利用中,我们可以发现:任意调用与 gadget 结合或许可以轻松达成栈迁移,允许我们进行 ROP 攻击。