当前位置: 首页 > article >正文

个人曾经ARM64_汇编角度_PLTHOOK的研究

ARM64基础HOOK研究_2024

之前为了实现一个修改器变速器的小功能,结果研究了很多关于ELF的内容,特别是so文件(ARM64的)
还研究了Hook,以及注入进程等操作,以及实现类似IDA那样的断点,汇编转换,以及软硬断点等(实现了CE那种谁写入/访问/读取的检测),这里就不作记录了,只记录一下简单的hook研究经历
我个人是比较喜欢研究底层原理的,所以搞了这些> _ <,之前目标是研究IDA调试App的so的原理,想着实现,边调试边Hook,然后一发不可收拾了…今天整理电脑文件,所以就想发点记录
如今,告别曾经的自己,再也不搞了,太浪费时间了> _ <
在这里插入图片描述

**在这里插入图片描述**

2024年研究的HOOK,现在翻出来,做个记录吧(恭送之前的自己)

inlineHOOK

// 原理
// 替换函数指令实现跳转



[小范围跳转]
//	最大跳转范围	前后64MB大小
uint_fast64_t mask = 0x03ffffffu;


    
//跳转指令 B BL BLR 指令
/*
B 指令:跳转到标签处,不保存返回地址,不设置链接寄存器。
BR 指令:跳转到寄存器中的地址,不保存返回地址,不设置链接寄存器。
BLR 指令:跳转到寄存器中的地址,保存返回地址到链接寄存器(例如 x30),用于函数调用和返回。

*/
// B #pc	一般是无参数 无条件分支指令 [目标:函数内部_标签] (循环,分支...)
0x0:      00 00 00 14            B 0

// BL		一般是有参数 函数调用指令 [目标:导出函数_标签]
0x4:      FF FF FF 97            BL  0

// BLR		 函数调用和返回指令 [目标:指定寄存器里存的地址],并且可以在调用后,返回调用的位置继续执行
0x8:      00 02 3F D6            BLR x16

// BLR		 无条件地跳转 [目标:指定寄存器里存的地址],但是不能回到原来调用位置
0xc:      00 00 1F D6            BR x0
    
    
    // ============= [HOOK 寻址计算] ============= 
//  1.B和BL	指令		(A里面调用B函数)
size_t pc_offset = (B地址 - A里当前指令位置地址)/4;	(地址间相差多少条指令)
// 计算指令
uint_fast64_t mask = 0x03ffffffu;
long b_inst =0x14000000 | ((pc_offset) & mask);
// 使用
    B pc_offset
    BL pc_offset
        
        
// 2.BR指令,original是要HOOK的函数的指针,symbol是要HOOK的函数
        //uint32_t *original = static_cast<uint32_t *>(symbol);
        //地址写入对齐校验
        //奇数:地址写入内存时需要对齐内存,需要补充一个NOP指令,完成对齐
        //偶数:不需要处理,直接写入即可
        int32_t count = ((uint64_t)((uint32_t *)original + 2) & 7u) != 0u ? 5 : 4;
        //把地址写到相对当前位置偏移处
         /**
         * LDR X17, #0x8
         * [分析]
         * LDR X17,#距离当前位置偏移的数值
         * 0x4  LDR X17, #0x8
         * 从LDR下一行开始数,每一行是一个0x4
         * 从LDR下一行开始,第二行写入要跳转的函数的地址
         *
         */
		original[0] = 0x58000051u; // LDR X17, #0x8				(addr:0x0)
		//br	x17
		original[1] = 0xd61f0220u; // BR X17    20 02 1F D6		(addr:0x4)
		//写入要跳转的新函数地址
		original[2] = replace; // 这里对应0x8的位置				(addr:0x8)
        
        

    // ============= [优化处理] ============= 
int edit_count = 修改指令的个数;
// 刷新代码缓存
__flush_cache(symbol, edit_count* sizeof(uint32_t));


    
    

PLT HOOK

#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)

// 解锁内存
int a = mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
    printf("[状态码]:%d\n",a);

// 擦除缓存
    __builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr));

处理libc.so里malloc…这种函数

#include <iostream>



__attribute__((constructor))
void my_init() {
    printf("[A64 HOOK 框架加载成功]\n");
    // 可以在这里执行其他初始化操作
}


__attribute__((destructor()))
void my_cleanup() {
    printf("[A64 HOOK 框架加载成功]\n");
    // 可以在这里执行其他初始化操作
}

一些测试

我的ElfHOOK传说记录

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <dlfcn.h>
#include "test.h"


#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
static void* (*real_malloc)(size_t) = NULL;

void *my_malloc(size_t size)
{
    if (!real_malloc) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
        if (!real_malloc) {
            fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
            exit(EXIT_FAILURE);
        }
    }
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return real_malloc(size);
}

void hook()
{
    char       line[512];
    FILE      *fp;
    uintptr_t  base_addr = 0;
    uintptr_t  addr;

    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "executable") &&
           sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
            break;
    }
    fclose(fp);
    if(0 == base_addr) return;

    //the absolute address
    addr = base_addr + 0x11FA8;

    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);

    //replace the function address
    *(void **)addr =(void *)(my_malloc);

    //clear instruction cache
//    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}

int main()
{
    hook();

    say_hello();
    say_hello();
    say_hello();
    say_hello();
    return 0;
}




参考

https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md

test.h

#ifndef TEST_H
#define TEST_H 1

#ifdef __cplusplus
extern "C" {
#endif

void say_hello();

#ifdef __cplusplus
}
#endif

#endif

tese.c

#include <stdlib.h>
#include <stdio.h>

void say_hello()
{
    char *buf = malloc(1024);
    if(NULL != buf)
    {
        snprintf(buf, 1024, "%s", "hello\n");
        printf("%s", buf);
    }
}

基础HOOK

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include "test.h"


#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)

void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

void hook()
{
    char       line[512];
    FILE      *fp;
    uintptr_t  base_addr = 0;
    uintptr_t  addr;

    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "executable") &&
           sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
            break;
    }
    fclose(fp);
    if(0 == base_addr) return;

    //the absolute address
    addr = base_addr + 0x11FB0;

    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);

    //replace the function address
    *(void **)addr =(void *)(my_malloc);

    //clear instruction cache
    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}

int main()
{
    hook();

    say_hello();
    return 0;
}


1.基于LD_LIBRARY_PATH的加载引导HOOK

原理探索

# 这是最基础的【库函数】hook方法
# 编译上面的libtest.so库  和 main
# 然后
adb push ./libtest.so ./main /data/local/tmp
adb shell "chmod +x /data/local/tmp/main"
# 关键是这里的 export LD_LIBRARY_PATH=/data/local/tmp;
# 这里是延时加载的,在main里调用的函数,会率先从/data/local/tmp/libtest.so里找,找不到的才会去libc.so这样的库里找
adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"

2.ELF的结构分析

为啥要分析这个,因为基于PLT的HOOK需要这个分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.查看基础头信息
# 命令 arm-linux-androideabi-readelf -h 你的.so
# 或者NDK下的 D:\NDK\android-ndk-r19c\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin\readelf -h 你的.so
ELF Header:
  Magic:   7f 45 4c 46 | 02 01 01 00 00 00 00 00 00 00 00 00  #魔术变量ELF|架构(02:64bit 01:32bit) ....   
  Class:                             ELF64 #类型

  Data:                              2's complement, little endian 
  									  # 二进制补码(规范处理正负数),大小端(HEX阅读顺序)信息
  Version:                           1 (current) # (ELF版本号)
  OS/ABI:                            UNIX - System V # (架构)
  ABI Version:                       0 # (架构版本)
  Type:                              DYN (Shared object file) # (共享对象文件,需要其他库支撑运行)
  Machine:                           AArch64 #(AArch64 架构)
  Version:                           0x1 #(ELF 文件格式的版本号)
  Entry point address:               0xb10 
  									#(程序开始执行的入口地址,_start_main的偏移地址,注意并非是C代码里的main函数地址)
  Start of program headers:          64 (bytes into file) #程序头表偏移地址0x40,可以说都是这个位置开始的
  Start of section headers:          14864 (bytes into file) #节头表偏移地址
  Flags:                             0x0 # 文件头的标志字段,这里没有设置任何特定的标志,因此为0x0。
  Size of this header:               64 (bytes) # ELF 文件头的大小为64 (0x40)字节
  Size of program headers:           56 (bytes) # 每个程序头的大小为56字节
  Number of program headers:         9 # 包含9个程序头表
  Size of section headers:           64 (bytes) # 每个节头的大小为64字节
  Number of section headers:         34# 包含34个节头表
  Section header string table index: 31 # 指向节头表字符串表的索引为31。节头表字符串表包含节名称的字符串




D:\NDK\android-ndk-r19c\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-android\bin>
  	# Section header string table index  节表名字在第几个节表内存着
  	# sh_offse + sh_size+sh_num  ->读取所有的节表 其实这个就是个动态分配的数组
    # Elf64_Shdr *shdrs = (Elf64_Shdr*)malloc(elf_header->e_shnum*sizeof(Elf64_Shdr));
	# 取出 节表名字对应的节表 -> Elf64_Shdr shstrtab = shdrs[elf_header->e_shstrndx];
    # 读取节表的所有内容 (其实只是个Tab标签) char *shstrtab_data =(char*) malloc(shstrtab.sh_size);
    # pread64(fd, shstrtab_data,shstrtab.sh_size,shstrtab.sh_offset); 
    # 从字节表名字偏移处读取一个,读取到shstrtab_data里面
    


3.基于PLT的HOOK

原理探索

程序头
节表列表	Elf64_Shdr 	elf_header->e_shentsize * elf_header->e_shnum
节表名单	Elf64_Shdr	shdrs[elf_header->e_shstrndx]



所有[函数]符号表
.symtab	符号列表 [函数...结构体] --- int index = symtab->st_name

.strtab	符号名字列表	[函数所有名字]  char* fuc_name = strtab[index]
很多不同的内容,导入函数 addr: 0000000  Type:FUN		GLOBAL  	(可能有@@LIB)
[过滤所有导入符号]
if(sym->st_value ==0 &&(ELF64_ST_TYPE(sym->st_info) == STT_FUNC) && ELF64_ST_BIND(sym->st_info) == STB_GLOBAL)
// 判断函数名字
记录当前序号i (是addr: 0000000  Type:FUN		GLOBAL这个类型里的序号!!!!!)

.rela.plt	关联表	[libc.so等导入函数的结构体] ------ Elf64_Rel -----	plt_base_offset = rela_plt_tab[0].r_offset 
计算导入函数
target = plt_base_offset + 8*i


HOOK 
	#define PAGE_START(addr) ((addr) & PAGE_MASK)
	#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
	
	// 计算PLT地址
	addr = lib_base+target
	
	// 给予可修改权限
	mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE)
    // 替换
	*(void **)addr =(void *)(replace);
    // 擦除缓存
    __builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr));




Elf64_Ehdr *elf_header = (Elf64_Ehdr *) malloc(sizeof(Elf64_Ehdr));
拿到所有节表
Elf64_Shdr *shdrs = new Elf64_Shdr[elf_header->e_shnum];

节表所有名字保存在 index = elf_header->e_shstrndx 这个节表里
Elf64_Shdr shstrtab = shdrs[elf_header->e_shstrndx];
拿到所有表的名字数组
char *shstrtab_data =(char*) malloc(shstrtab.sh_size);

遍历所有节,获取节真实名字
shstrtab_data + shdrs[i].sh_name



记录 .symtab 序号 ----------- 所有函数详细信息,偏移,类型(名字不在这里)
记录 .strtab 序号 ----------- 所有函数名字
char *dynstr = (char *)malloc(strtab.sh_size);
记录 .rela.plt 序号
 从strtab表获取到(系统函数的名字)
 并从symtab表记录这个类型的索引++
 [sym->st_value] 偏移量:0
 [ELF64_ST_TYPE(sym->st_info)] 类型: STT_FUNC
 [ELF64_ST_BIND(sym->st_info)] 绑定类型:STB_GLOBAL
 记录目标fucname 对应的0&STB_GLOBAL&STT_FUNC的	[目标GLOBAL索引序号]

获取 .plt 表 包含的是Elf64_Rel重定向信息
Elf64_Shdr rela_plt_section = shdrs[rela_plt_index];
拿到第一个 .plt 表元素的偏移量
frist_global_offset = rela_plt_tab[0].r_offset;

根据 [目标GLOBAL索引序号] 计算目标函数偏移
目标函数偏移 = rela_plt_tab[0].r_offset + [目标GLOBAL索引序号]*0x8


--------
为什么这样计算??? 直接获取对应的 [目标GLOBAL索引序号]  取出 rela_plt_tab[index].r_offset不就行了???
	不行,因为可以直接遍历出来的rela_plt_tab[index]是有限个的,不一定会真的获取到所需要的函数
	也就是 [系统函数] 用了10,但是 .plt 里看到的,可能只有5个或者更少,所以要计算

 

--------
获取后 
	uintptr_t addr  = info.lib_base + info.target_offset;
	mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
	
	---------
	#include <inttypes.h>
	#define PAGE_SIZE 4096
	#define PAGE_MASK (~(PAGE_SIZE - 1))
	---------
	
	#define PAGE_START(addr) ((addr) & PAGE_MASK) // 内存对齐
	#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
	
	*(void **)addr =(void *)(replace); // 替换函数
	
	__builtin___clear_cache((char *)PAGE_START(addr), (char *)PAGE_END(addr)); //擦除原函数的缓存





处理libc.so里malloc…这种函数

汇编HOOK的研究

#include <iostream>
#include <cstdio>
#include <cstring>
#include <inttypes.h>
#include <asm-generic/fcntl.h>
#include <fcntl.h>
#include <linux/elf.h>
#include <unistd.h>
#include <elf.h>
#include <asm-generic/mman.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include "Hook/PltHook.h"
#include "Hook/IHOOK.hpp"
#include "Hook/IASM64.h"
#include "Hook/And64InlineHook.hpp"


/**
 * inline HOOK 2种
 * 1.当前so内直接调用 ----- 1
 * 前后 64 MB
 * B  #pc
 * 2.调用其他so内函数 ----- 6
 * STP x8,x0 ,[sp-20]
 * LDR X0,8
 * BR X0
 * [ADDR]
 * LDR X8,[sp]
 * @return
 */








int inline_A() {
    printf("[本so] 函数A\n");
    return 666;
}

void inline_B() {
    printf("[本so] HOOK 到了 函数B\n");
}


// 跳板子
int (*inline_A_tamp)() = NULL;

static __attribute((aligned(PAGE_SIZE))) uint32_t __insns_pool[256][5 * 10];


// 跳板集合




void log(char* tag,ADDR_TYPE value){
    printf("[%s] %llx\n",tag,value);
}

#define __flush_cache(c, n)        __builtin___clear_cache(reinterpret_cast<char *>(c), reinterpret_cast<char *>(c) + n)

void HOOK_inline(const void *symbol, void *replace, void **result) {
    // 创建一个跳板 池子,每次 HOOK都会 在池子里选择一个格子 256次HOOK 每次HOOK里可以存40条指令
    // 最好这样写
    static __attribute((aligned(PAGE_SIZE))) uint32_t __insns_pool[256][4 * 10];
    //    static uint32_t __insns_pool[256][5 * 10];
    // HOOK 次数计数器
    static __attribute((aligned(PAGE_SIZE))) uint32_t tams_index = 0;


    // 计算pc偏移
    INST_IMPORT pc_offset = getPC_ADDR(ADDR_TYPE(symbol), ADDR_TYPE(replace));
    printf("[pc] %llx\n",pc_offset );

    // 如果在+-64MB范围之内,直接使用B跳转
    if (llabs(pc_offset) <= (B_PC_MASK >> 1)) {
        // 一般是在自己程序调试时使用
        printf("hook small\n");
        // 回调原函数
        if (result != NULL) {
            // 开启当前备份 rwxp -> 可读可写可执行 权限
            mwrxp(FUN_CALL(__insns_pool[tams_index]), 1);
            // 获取原函数第一条指令,用作备份
            INST_TYPE raw_frist = getasm32(FUN_CALL(symbol), 0);
            INST_IMPORT backup_pc_offset = getPC_ADDR(ADDR_TYPE(__insns_pool[tams_index]), ADDR_TYPE(symbol));
            __insns_pool[tams_index][0] = raw_frist; // 第一条是原指令
            __insns_pool[tams_index][1] = backup_pc_offset; // B symbol+4 # 调到原函数的第二条指令继续执行
            *result = __insns_pool[tams_index]; // 把当前指令配置给回调函数
            tams_index++;   // 更新池子索引
        }

        // 开启原函数的      rwxp ->可写权限 权限
        mwrxp(FUN_CALL(symbol), 1);
        // 替换原函数第一条指令为 B #replace函数
        editasm32(FUN_CALL(symbol), 0, pc_offset);
    } else {
        // 一般是在自己程序---加载,引用库时,启用
        printf("hook big\n");

        int32_t count = ((uint64_t)((uint32_t *)symbol + 2) & 7u) != 0u ? 5 : 4;
        // 开启原函数的      rwxp ->可写权限 权限
        mwrxp(FUN_CALL(symbol), 1);
        if (count == 5){
            uint32_t *original = (uint32_t *)symbol; // symbol要HOOK的函数

            editasm32(FUN_CALL(symbol), 1*0x4, a64_ldr_x_num(17,0x8)); // LDR X17, #0x8
            editasm32(FUN_CALL(symbol), 2*0x4, a64_br_x(17)); // BR X17
            editasm64(FUN_CALL(symbol), 3*0x4, replace); // BR X17


            __flush_cache((char*)symbol, 5 * sizeof(uint32_t));
        }

//        __flush_cache(symbol, 7* sizeof(uint32_t));












    }

}


void BB() {
    printf("[本so] BB\n");
}

int (*AA)() = NULL;

int (*CC)() = NULL;

int test_inline_hook_local() {

    // 总共6条指令
    printf("----------[调用原A]-----------\n");

    inline_A();


//    A64HookFunction(FUN_CALL(inline_A), FUN_CALL(inline_B), (void **) &AA);
//    A64HookFunction(FUN_CALL(AA), FUN_CALL(BB), (void **) &CC);
//    lookInst(ADDR_TYPE(AA),30);

    HOOK_inline(FUN_CALL(inline_A), FUN_CALL(inline_B), (void **) &AA);
    HOOK_inline(FUN_CALL(AA), FUN_CALL(BB), (void **) &CC);



    printf("----------[A被HOOK后  调用A]-----------\n");

    inline_A();
    if (AA) {
        printf("----------[调用AA]-----------\n");


        AA();

    }


    printf("----------[AA被HOOK后]-----------\n");
    AA();
    if (CC) {
        CC();
        printf("[调用CC]\n");

    }
    return 0;
}


#define 程序入口 main
void (*function_in_file2)() = NULL;


int 程序入口() {


    char* path = "/data/local/tmp/libts_arm64-v8a.so";
    void *lib_handle = dlopen(path,RTLD_LAZY);
    if (!lib_handle) {
        fprintf(stderr, "Failed to open library: %s\n", dlerror());
        return -1;  // 处理打开失败的情况
    }


    printf("\033[37;3m 基础 HOOK 框架 \033[0m\n");

    function_in_file2 = (void (*)()) dlsym(lib_handle,"_Z21Dysm_shared_hook_Testv");

    printf(" 【库】0x%llx\n",  function_in_file2);
    printf("【自己】0x%llx\n",  inline_A);

    printf("----------[调用原hook_Test]-----------\n");

    function_in_file2();


    void* symbol = (void*)function_in_file2;
    void* replace = (void*)inline_A;
    void** results = (void**)AA;


//    A64HookFunction(FUN_CALL(symbol), FUN_CALL(replace), FUN_CALL_RAW(&results));
    int32_t count = ((uint64_t)((uint32_t *)symbol + 2) & 7u) != 0u ? 5 : 4;
    printf("[PC] 0x%llx\n",count);
    mwrxp(symbol,1);
    uint32_t *original = (uint32_t *)symbol; // symbol要HOOK的函数

    HOOK_inline(FUN_CALL(symbol), FUN_CALL(replace), FUN_CALL_RAW(&results));



    printf("----------[HOOK后  调用原hook_Test]-----------\n");
    function_in_file2();





    printf("----------[结束]-----------\n");




    return 0;
}



void iii(){
    uint64_t obb = 0b11110001101010010001101111111010001100011110100;
    size_t s = 30;
    for (int i = 0; i < s; ++i){
        printf("[%d]  %llx\n",i,  obb&i);
    }

}

结束

可能比较乱,没办法,这几年Hook的个人研究的一部分,本来准备早点发的,但是由于CSDN上传图片总是失败,一张一张上传太麻烦,所以直接不上传图了,其它文件太多,也不想发出来

不要提问,因为好久不整了,也不打算整了,不做回复请见谅 > _ <


http://www.kler.cn/a/502695.html

相关文章:

  • CSS | 实现三列布局(两边边定宽 中间自适应,自适应成比)
  • 【Rust】错误处理机制
  • vue3+vite+ts集成第三方js
  • fast-crud select下拉框 实现多选功能及下拉框数据动态获取(通过接口获取)
  • AI浪潮下的IT变革之路:机遇、挑战与重塑未来
  • lobechat搭建本地知识库
  • 深入探讨 Vue.js 的动态组件渲染与性能优化
  • Windows11下OpenCV最新版4.11源码编译
  • 字符串算法篇——字里乾坤,算法织梦,解构字符串的艺术(上)
  • ros2笔记-6.2 使用urdf创建机器人模型
  • Qiskit快速编程探索(基本篇)
  • 深入浅出 Android AES 加密解密:从理论到实战
  • Android 15应用适配指南:所有应用的行为变更
  • 【深度学习基础与pytorch基础】特征与标签
  • 六十九:基于openssl实战验证RSA
  • 大疆机场及无人机上云
  • Maven中的dependencyManagement和dependencies
  • 【初识扫盲】厚尾分布
  • 利用 Python 爬虫获取 1688 商品评论的实践指南
  • 基于Python(Django)+SQLite3实现的(Web)资产管理系统
  • C++内存泄露排查
  • Go Ebiten小游戏开发:井字棋
  • Postgres14.4(Docker安装)
  • 【数据分析】一、初探 Numpy
  • 服务器引导异常,Grub报错: error: ../../grub-core/fs/fshelp.c:258:file xxxx.img not found.
  • 行业案例:高德服务单元化方案和架构实践