记一次AWD-PWN出题经历
前言
这篇文章大概是 24 年四月写的,也是老笔记了。当初因为一些原因没有发,现在 CTF 打的越来越少,已经渐渐淡出了竞赛。这些笔记也分享出来(虽然真的很水🥹),让有需要的师傅有个参考(太菜勿喷😭)…
一直很好奇 AWD-PWN
如何出,但困难在于 check
脚本的编写。网上这方面的资料很少,就一直鸽了😶🌫️ 。最近因为要校内自己玩 AWD
,于是专门花时间去研究了一下。发现了 Q1IQ 师傅在 github
上开源了 AWD-PWN-Checker ,于是抱着学习的心态阅读了整个代码。在受益匪浅的同时,又在原基础上增加了一些功能… 下面是对这次 AWD-PWN
出题过程进行的记录😎
这次 AWD-PWN
的题目部署主要参考了两个开源项目 Cardinal AWD-PWN-Checker
平台部分的部署是由我同学 Timochan 完成,我只负责了漏洞程序的编写、 check
脚本的编写、以及 exp
的编写 。
漏洞程序的出题过程
这里先介绍下我出二进制题目的想法,因为考虑到是面向学校内大一和大二的学弟,而且是第一次校内搞 AWD
,于是计划就考察基础漏洞为主,并且代码量设计的也只有三百多行,整体难度为中等偏易。
设计漏洞点如下:
- 留一个后门函数(避免太明显,使用
popen
函数) - 格式化字符串漏洞(栈上)
- 格式化字符串漏洞(堆上)
- UAF漏洞
- read函数造成的栈溢出
保护策略:
不开 PIE
开启NX
可以篡改 got 表
开启 canary
去除符号表
整个逻辑以菜单的形式展开,漏洞留了较多,可以攻击的组合方式也很多(我写了七种 exp
),同时放了一些迷惑性功能。
代码如下
//gcc pwn.c -o pwn -no-pie |
整个攻击的思路有两种,可以利用后门或者不利用后门。因为没开 PIE
,因此无论是溢出或者格式化字符串漏洞都可以不泄露出 libc
的情况下进行攻击。这样的速度更快,可能在一些队伍没有修复好的情况下就进行得分。而一旦防御方将后门破坏,就只能通过 ROP
或格式化字符串来泄露 libc
进行拿 shell
了。而劫持点可以是 got
表、__free_hook
、返回地址等等。攻击方式很多样,感觉不容易注意到的是 show
函数中的非栈上格式化字符串?毕竟这个功能点可能没注意看的话,发现不了格式化字符串漏洞。并且他的利用还需要布置栈链,相对来说这里是耗时最长利用的漏洞。
EXP如下
from pwn import * |
针对二进制程序本身的设计上面简单提及了一下,因为我觉得最重要的收获是阅读了 AWD-PWN-Checker
代码所学习到 check
脚本的编写思路,以及对代码编写能力的提升。
check 脚本的设计思路
Q1IQ 师傅在 AWD-PWN-Checker
中的设计思路是用主模块 main.py
加载具体做 check
的模块 pwncheck.py
,在裁判端存储一个正常的程序,check
哪个靶机时就将远端的程序下载到裁判端。用 lief
库可以解析 ELF
文件,通过和正常的程序比较去判断目标文件本身是否做了非法的修改。利用 zio
库与远端的程序进行交互,判断服务是否正常。
在原本的 check
点中检测了
服务是否还存在(能否正常连接并交互)
检查服务的基础功能是否正常
文件的
size
是否被修改got
plt
表是否被修改free
函数是否被改成了nop
在阅读完整个代码逻辑后,我打算增加一些检测点。首先是在 AWD
中上通防,我认为是很影响公平性的行为,使用 evilPatcher 可以很轻易给程序加一个沙箱,禁用掉关键的系统调用,让攻击者即使利用漏洞成功,在执行 execve
系统调用时,进程也会被 kill
。导致防御者在没有任何修复操作的情况下也不会丢分,这违背了 AWD
比赛的初衷。
而 evilPatcher
这个工具思路是劫持 _libc_start_main
函数的一个参数 main
为 .eh_frame
段,在程序调用 main
函数之前就已经执行了 prctl
系统调用。针对这个特征我增加了两个检测点,首先是 start
函数中机器码不可以被修改(这样入口函数是可以正常触发的),即使 fix
也不可能在 main
函数调用前就进行修复操作。另外就是提取 .eh_frame
段中数据,进行特征码匹配。有些漏洞的修补是需要在 .eh_frame
段中写入汇编代码的,但无论如何也不会执行系统调用 prctl
,因此控制 al
寄存器并执行 int 0x80
或 syscall
的可以判定上了通防。
sys_prctl ==> mov al,0x9d ; syscall ==> B0 9D 0F 05 |
同时我针对程序节的大小,起始地址,数量也都做了检查,防止有其他操作在没有改变文件大小的情况下对节进行一些修改。
另外我改写了下载靶机程序的函数,原本 Q1IQ 师傅是利用的 ssh
公私钥文件进行的文件免密下载。但我们部署这个平台是给每个靶机两个用户,一个是 ctf
低权限普通用户(参赛者使用的),一个是 root
。我实现的方式是直接通过 root
的密码进行文件下载。
原本测试交互的函数肯定是要去改写成适应当前二进制程序的,除此之外我还修改了主模块中的逻辑。原本使用方法是 python3 main.py pwncheck.py --host 1.2.3.4 --port 9999
,只能指定一个靶机检测。我新增了一个 ip
列表,可以自动去检测所有的靶机状态。同时因为网络原因可能造成的误判,又新增了一个二次裁决的功能。所有第一次判断异常的靶机不会扣分,而是再进行两次 check
,接下来两次 check
结果一样的话,才确定靶机的防御异常进行扣分。
阅读 AWD-PWN-Checker 的收获
importlib模块导包
创建 demo.py
,代码如下
import importlib.util |
创建 test.py
,代码如下
def hello(): |
二者位于同一目录,运行 demo.py
可以成功输出 is is test
跟 import
导包类似,但是将固定的模块名称设置为函数参数,可以在面对不同需求时,增加了程序的灵活性。
down_from_remote函数(修改)
该函数利用 ssh
公私钥文件实现从远程服务端下载文件
def down_from_remote(host, remotepath, localpath, user="root", port=22): |
- 先用
ssh-keygen -t rsa
生成一组公私钥对 - 执行
ssh-copy-id username@xx.xx.xx.xx
将公钥文件上传至目标主机 - 将私钥文件备份,拷贝至当前目录名为
awd_rsa
- 配置好
down_from_remote
函数的参数,进行文件下载
lief库
这个库可以去解析 ELF
文件,包括但不限于获取节信息,符号表信息,指定地址中的数据等等。利用该库中提供的各种函数,可以去来检查二进制文件是否进行了非法修改。
以检查 got
表是否被修改为例,可以先将靶机中的目标文件下载到裁判端,然后同时获取 .got.plt
节的地址。通过该地址解析出整个节的数据,让目标文件和原文件提取的数据作比较,就能判断出 got
表是否被修改。
def compare_data(check_elf, ordinary_elf, address, size): |
还可以用特征码,静态检测的方式来检查 call free
的代码是否被 nop
掉(目前这里还可以改进,现在是手动传参 call free
指令的地址和指令字节数),以及是否在 .eh_frame
段中加入了执行 prctl
的汇编指令等等。
增加
check_section
用来判断待 check
文件和原文件二者节的数量、节的地址、节的大小是否有被篡改
def check_section(check_elf, ordinary_elf): |
check_func
用来判断指定函数的代码是否被修改,通常 function_name
指定为 _start
函数,因为使用 evilPatcher
工具上通防的话,会去修改 _start
函数中调用 __libc_start_main
时的第一个参数(原本是 main
)为 .eh_frame
段地址,执行 prctrl
系统调用加沙箱。正常修补程序漏洞的话是无需对 _start
函数进行任何修改,因此判断 _start
函数代码是否被修改可以有效检测防御者是否采取了合适的方法修补漏洞
def check_func(check_elf, ordinary_elf, function_name = "_start"): |
check_sandbox
check_sandbox
函数主要是对 check_prctl_in_ehframe
函数的封装,使用 evilPatcher
工具或者自己上沙箱的最好方式都是在 ehframe
段中写入 prctl
系统调用的汇编代码,并劫持执行流到此处执行。我选择特征码识别的静态检测,来识别 prctl
系统调用(考虑到加沙箱,大部分都会使用 evilPatcher
,因此是针对该工具加的系统调用特征码进行识别)
def check_sandbox(check_elf): |
def check_prctl_in_ehframe(check_elf, address, size): |
download_file_via_ssh
原本下载文件的方式是通过 ssh
公钥来下载的,新增一个函数通过密码来下载
def download_file_via_ssh(hostname, port, username, password, remote_file_path, local_file_path): |
总结
具体检测的功能,其实并不难写。通过阅读 AWD-PWN-Checker
代码,给我最大的收获是整个代码的编写思路。
在整个 main.py
模块中的代码很简洁,就是去解析命令行参数并调用指定模块中的 check
函数进行后续的处理。这里将具体 check
的代码封装在了另一个 py
文件中,定义了 Checker
类,并在里面封装了具体检测文件的函数。其中将 check
函数作为入口
# coding=utf-8 |
而在后续的改写中,我希望在应对不同题目时 pwncheck.py
脚本尽可能的不去改动(也就是通用型),而对于不同题目的基础功能 check
时肯定都要进行定制。于是我将不同的功能检查函数放在了 file_check
文件中。
总结:自己代码能力太弱了🥲 。我将 docker、checker以及题目源码和 exp.py都上传到了百度网盘,需要的师傅可以自己拿。链接: https://pan.baidu.com/s/1lFIPX0TDxsg7Dh074VtJbQ?pwd=zikh