23、iOS逆向防护

news2024/11/24 19:37:09

一、ptrace

1.1 、ptrace概述

ptrace: process trace,提供一个进程监察和控制另一个进程.并且可以读取和改变被控制进程的内存和寄存器里面的数据.它就可以用来实现断点调试和系统调用跟踪.

App可以被lldb动态调试,因为App被设备中的debugserver附加,它会跟踪我们的应用进程(process trace), 而这一过程利用的就是ptrace 函数.

ptrace是系统内核函数,它可以决定应用能否被debugserver附加.如果我们在项目中,调用ptrace函数,将程序设置为拒绝附加,即可对lldb动态调试进行有效的防护.

ptrace在iOS系统中存在,但是无法直接使用,需要导入头文件.但是在Mac OS系统下,存在于

  • ptrace函数的定义:
int ptrace(int _request, pid _pid, caddr_t _addr, int _data);
    • request: 请求ptrace执行的操作
    • pid: 目标进程的ID
    • addr: 目标进程的地址值,和request参数有关
    • data: 根据request的不同而变化.如果需要向目标进程中写入数据,data存放的是需要写入的数据.如果从目标进程中读数据,data将存放返回的数据.

1.2 ptrace防护应用

  • 搭建App项目AntiDebug
  • 导入MyPtraceHeader.h头文件.
  • 写入调用代码
#import "ViewController.h"
#import "MyPtraceHeader.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
    • 使用Xcode运行项目,启动后立即退出.使用ptrace设置为拒绝附加,只能手动启动App,也就是说,用户在使用App时,不会有任何影响.一旦被debugserver附加,就会闪退.
  • 如果在越狱环境,手动对App进行debugserver附加呢?
    • 找到AntiDebug进程,手动对App进行debugserver附加
Holothurian6P:~ root# ps -A | grep AntiDebug
48329 ??         0:00.23 /var/containers/Bundle/Application/3D3083F3-295D-4F55-83DB-40887FF4FF46/AntiDebug.app/AntiDebug
48336 ttys000    0:00.00 grep AntiDebug
Holothurian6P:~ root# debugserver localhost:22 -a 48329
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-900.3.87
 for arm64.
Attaching to process 48329...
Segmentation fault: 11
    • 同样附加失败,无论以何种方式,都会被ptrace函数阻止.

1.3 ptrace防护破解

  • ptrace是系统内核函数,被开发者所熟知.ptrace的防护痕迹也很明显,手动运行程序正常,Xcode运行程序闪退
  • 我们在逆向一款App时,遇到上述情况,第一时间就会想到ptrace防护
  • 由于ptrace是系统函数,需要间接符号表,我们可以尝试下ptrace的符号断点

    • ptrace的断点命中,确定了对方的防护手段,接下来尝试破解
  • 采用AntiDebug项目,模拟应用重签名,注入动态库
  • 创建 Inject动态库,创建 InjectCode类
  • 在Inject动态类中,导入fishhook,导入MyPtraceHeader.h头文件
#import "InjectCode.h"
#import "MyPtraceHeader.h"
#import "fishhook.h"

@implementation InjectCode

int (*sys_ptrace)(int _request, pid_t _pid, caddr_t _addr, int _data);

int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
    if (_request == PT_DENY_ATTACH) {
        return 0;
    }
    return sys_ptrace(_request,_pid,_addr,_data);
}
+ (void)load {
    struct rebinding rebPtrace;
    rebPtrace.name = "ptrace";
    rebPtrace.replacement = my_ptrace;
    rebPtrace.replaced    = (void *)&sys_ptrace;
    struct rebinding rebs[] = {rebPtrace};
    rebind_symbols(rebs, 1);
}

@end
  • 在my_ptrace函数中,如果是 PT_DENY_ATTACH 宏定义值,直接返回,如果是其他类型,系统由特定的作用,需要执行ptrace原始函数.
  • 运行项目,进入lldb动态调试,ptrace防护破解成功

二、sysctl

sysctl可以用来检测进程状态,判断当前是否是debug状态,sysctl可以自己控制接下来要干什么事情.

int sysctl(int *, u_int, void *, size_t *, void *, size_t);

2.1 sysctl使用

    • 参数1:   查询信息数组
    • 参数2: 数组中数据类型的大小
    • 参数3: 接收信息结构体指针
    • 参数4: 接收信息结构体的大小
BOOL isDebugger() {
    int name[4]; //里面放字节码.查询的信息
    name[0] = CTL_KERN;//内核查询
    name[1] = KERN_PROC;//查询进程
    name[2] = KERN_PROC_PID;//传递的参数是进程的ID(PID)
    name[3] = getpid();//获取PID的值

    //接收进程信息的结构体,定义时为空,当内核函数调用的时候,在堆区开辟空间,创建一个真正的结构体.
    struct kinfo_proc info;
    //接收信息结构体的大小
    size_t info_size = sizeof(info);
    /*
     参数解释
     1、查询信息数组
     2、数组中数据类型的大小
     3、接收信息结构体指针
     4、接收信息结构体的大小的指针
     */
    int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0);
    //info.kp_proc.p_flag; 是当前的进程标记值
    //P_TRACED 是正在跟踪的调试进程
    bool ret = (info.kp_proc.p_flag & P_TRACED) != 0;
    return ret;
}
    • 调用及日志输出
- (void)viewDidLoad {
    [super viewDidLoad];
    if (isDebugger()) {
        NSLog(@"检测到有调试");
    } else {
        NSLog(@"程序正常运行");
    }
}
    • 运行项目后:日志输出
2023-04-16 20:54:02.344892+0800 SysctlDemo[1938:89970] 检测到有调试

2.2 sysctl调用

  • 在正常的反调试项目中,只检测一次是明显不合理的,我们需要定时监测,当前项目是否被调试
  • 因此需要添加一个计时器
static dispatch_source_t timer;
void debugCheck() {
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        if (isDebugger()) {
            NSLog(@"检测到有调试");
        } else {
            NSLog(@"程序正常运行");
        }
    });
    dispatch_resume(timer);
}
- (void)viewDidLoad {
    [super viewDidLoad];
    debugCheck();
}
    • 当检测到有调试行为后,可以直接exit(0)退出程序
    • 当手动启动App后,控制台输出: 程序正常运行. 达到目的

2.3 sysctl破解

  • 因为是系统函数,那么存在间接符号表,考虑使用fishhook来HOOK破解.在原SysctlDemo直接添加动态库,模拟被HOOK情况.在Inject中,实现sysctl破解
#import "InjectCode.h"
#import <sys/sysctl.h>
#import "fishhook.h"

@implementation InjectCode

//原始函数地址
int (*sysctl_p)(int *, u_int, void *, size_t *, void *, size_t);

//定义新的函数
int my_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *newInfo, size_t newInfoSize){
    if (namelen == 4  &&
        name[0] == CTL_KERN &&
        name[1] == KERN_PROC &&
        name[2] == KERN_PROC_PID &&
        info) {
        int err = sysctl_p(name, namelen, info, infosize, newInfo, newInfoSize);
        struct kinfo_proc *myInfo = (struct kinfo_proc *)info;
        if (myInfo->kp_proc.p_flag & P_TRACED) {
            // 使用异或进行取反
            myInfo->kp_proc.p_flag ^= P_TRACED;
        }
        return err;
    }
    return sysctl_p(name, namelen, info, infosize, newInfo, newInfoSize);
}

+ (void)load {
    struct rebinding sysctlBind;
    sysctlBind.name = "sysctl";
    sysctlBind.replacement = my_sysctl;
    sysctlBind.replaced    = (void *)&sysctl_p;
    struct rebinding rbs[] = {sysctlBind};
    rebind_symbols(rbs, 1);
//    rebind_symbols((struct rebinding[1]){{"sysctl",my_sysctl,(void *)&sysctl_p}}, 1);
}

@end
  • hook结果
2023-04-16 21:50:44.629585+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:45.629685+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:46.629694+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:47.629471+0800 SysctlDemo[2115:102944] 程序正常运行

三、sysctl&ptrace再防护(防止Hook)

  • 利用库的加载顺序,在别人Hook前,让防护代码先执行.就可以直线防护手段
  • 将VC中的防护去除,创建一个动态库,将VC中做的防护操作放入其中.
  • 这个时候运行项目,程序中断、但是手动运行项目查看控制台手机日志,运行正常.

#import "MyPtraceHeader.h" #import <sys/sysctl.h> @implementation AntiCode + (void)load { debugCheck(); ptrace(PT_DENY_ATTACH, 0, 0, 0); } static dispatch_source_t timer; void debugCheck() { timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC); dispatch_source_set_event_handler(timer, ^{ if (isDebugger()) { NSLog(@"检测到有调试"); // exit(0); } else { NSLog(@"程序正常运行"); } }); dispatch_resume(timer); } BOOL isDebugger() { int name[4]; //里面放字节码.查询的信息 name[0] = CTL_KERN;//内核查询 name[1] = KERN_PROC;//查询进程 name[2] = KERN_PROC_PID;//传递的参数是进程的ID(PID) name[3] = getpid();//获取PID的值 //接收进程信息的结构体,定义时为空,当内核函数调用的时候,在堆区开辟空间,创建一个真正的结构体. struct kinfo_proc info; //接收信息结构体的大小 size_t info_size = sizeof(info); /* 参数解释 1、查询信息数组 2、数组中数据类型的大小 3、接收信息结构体指针 4、接收信息结构体的大小的指针 */ int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0); //info.kp_proc.p_flag; 是当前的进程标记值 //P_TRACED 是正在跟踪的调试进程 return (info.kp_proc.p_flag & P_TRACED) != 0; } @end

四、修改二进制手段破解ptrace

  • 当目标App采用库加载的方式进行了反调试,那么就需要我们通过更高级的手段去破解.
  • 采用修改二进制文件的方式,来针对这种情况.

具体操作步骤为:

1、创建一个MonkeyDev的Demo、然后将刚才的AntiHook的包做为目标App放入TargetApp中

2、下一个ptrace符号断点.可以通过当前bt显示出调用栈.找到调用ptrace的库及方法

3、取出AntiCode这个动态库,利用Hopper查看二进制文件,找到 AntiCode动态库,调用ptrace的地方

4、修改二进制文件中此行,选中后 Alt+a

    • 1、将该处直接修改为 nop

    • 2、将该处修改为直接跳转到下一句汇编执行代码

5、导出新的二进制文件 (需要破解版Hopper或付费版)

File --> Produce New Executable

6、覆盖之前.app文件里面的MachO,重新运行就可以.

五、dlopen+dlsym

  • 为了隐藏在MachO中明显的ptrace字符串常量,我们采用异或一个固定Key字符,再根据指针指向字符串初始值,再次异或,就能得到原来的字符串
  • 通过dlopen拿到句柄函数 handle
  • 定义ptrace函数指针: (*ptrace_p)
  • 通过dlsym 拿到ptrace函数指针 ptrace_p
  • 调用所需函数: ptrace_p
    #import <dlfcn.h>
    #define KEY 0xAC
    //使用一个char数组拼接一个ptrace字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串)
    unsigned char funcName[] = {
        (KEY ^ 'p'),
        (KEY ^ 't'),
        (KEY ^ 'r'),
        (KEY ^ 'a'),
        (KEY ^ 'c'),
        (KEY ^ 'e'),
        (KEY ^ '\0'),
    };
    unsigned char * p = funcName;
    //再次异或之后恢复原本的值
    while (((*p) ^= KEY) != '\0') p++;
    //通过dlopen拿到句柄
    void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
    //定义函数指针
    int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
    //如果拿到句柄
    if (handle) {
        //通过dlsym拿到函数指针
        ptrace_p = dlsym(handle, (const char *)funcName);
        //如果拿到函数指针
        if (ptrace_p) {
            //调用所需函数
            ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
        }
    }
  • 使用dlopen+dlsym方式调用,lldb对ptrace下断点还是可以断住的. 这样就可以通过汇编跟符号的方式,找到跳转函数,对其修改.

六、 利用syscall调用系统函数

  • 编写调用代码(中可以查找系统函数编号)
/**
参数一:参数是函数编号
其它参数:给参数一的函数提供参数
*/
syscall(SYS_ptrace,PT_DENY_ATTACH,0,0);
//上边等价于
//syscall(26,31,0,0,0);
  • syscall方式调用,lldb对ptrace下断点是无法断住的。但是syscall本身可以被符号断点断住.

七、 最终手段: 利用汇编调用系统函数

中可以查找系统函数编号

  • 根据上一章syscall的直接调用值 syscall(26,31,0,0,0), 联想到汇编调用. volatile代表不优化此汇编代码
asm volatile(
    "mov x0,#26\n"
    "mov x1,#31\n"
    "mov x2,#0\n"
    "mov x3,#0\n"
    "mov x16,#0\n"//这里就是syscall的函数编号
    "svc #0x80\n"//这条指令就是触发中断(系统级别的跳转)
);
  • 再回想ptrace的函数调用:  
ptrace(PT_DENY_ATTACH, 0, 0, 0);
  •  也就是 ptrace(31, 0, 0, 0); 那么汇编代码为(CPU架构是arm64)
asm volatile(
     "mov x0,#31\n"//参数1
     "mov x1,#0\n"//参数2
     "mov x2,#0\n"//参数3
     "mov x3,#0\n"//参数4
     "mov x16,#26\n"//中断根据x16 里面的值,跳转ptrace
     "svc #0x80\n"//这条指令就是触发中断去找x16执行(系统级别的跳转!)
     );

八、exit(0)的汇编

  • 当检测到当前为调试环境后,采用exit(0)退出程序,但是这段代码想隐藏起来,不那么直观的通过二进制就被查找到.所以可以考虑汇编实现;
  • 根据arm64和32位架构的区别,汇编上进行区分
#ifdef __arm64__
    asm(
        "mov x0,#0\n"
        "mov x16,#1\n"//这里相当于 Sys_exit,调用exit函数
        "svc #0x80\n"    
    );
#endif
#ifdef __arm__
    asm(
        "mov r0,#0\n"
        "mov r16,#1\n" //这里相当于 Sys_exit
        "svc #80\n"    
    );
#endif

九、补充

        防护: 通过对App包中的embeded.描述文件中TeamID进行唯一值判定、也是一种防护手段;

        破解思路: 找到指定TeamID、在汇编中修改TeamID值即可.

十、总结

  • 反调试
    • ptrace(31,0,0,0) ; 拒绝进程附加
    • 该函数防护的特点
      • 程序被Xcode安装运行直接闪退
      • 终端附加会失败
      • 用户正常启动能运行
  • 破解ptrace
    • 通过fishhook函数勾住ptrace
    • 判断参数1: 如果是拒绝就直接return
  • 反调试sysctl
    • 这个函数里面去检查进程状态
    • sysctl(查询信息的数组,数组大小,接受信息结构体指针,结构体大小的指针,和3、4参数一样)
    • 在接受的结构体中,kp_proc属性中有一个标记 p_flag;
    • 查看二进制位第12位(P_TRACED)是否为1(为1代表有调试器附加).
  • 破解sysctl
    • 通过fishhook
    • 取出标记,然后异或取反.进行修改标记,达到破解效果
  • 提前执行防护代码
    • 将防护代码用Framework库的形式优先执行.
  • 静态修改: 二进制修改
    • hopper、IDA
    • 找到关键函数执行代码.直接修改汇编,改变流程:
      • 跳过ptrace执行函数,b 函数地址 ;
      • 直接去掉: nop
  • dlopen+dlsym
    • 隐藏ptrace的字符串函数调用,在二进制中不可见.
    • 但是ptrace本身还是会被符号断点断住
  • syscall
    • syscall(26,31,0,0,0);
    • lldb对ptrace下断点是无法断住的。但是syscall本身可以被符号断点断住.
  • 利用汇编调用系统函数
    • syscall(26,31,0,0,0) 汇编形式
    • ptrace(31, 0, 0, 0) 汇编形式
  • exit(0)的汇编
    • arm64
    • arm32

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/456916.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java阶段二Day08

Java阶段二Day08 文章目录 Java阶段二Day08DML语言-数据操作语言INSERTUPDATEDELETE 数据类型整数类型浮点类型字符类型日期类型 约束条件主键约束&#xff08;PRIMARY KEY&#xff09;非空约束&#xff08;NOT NULL&#xff09;唯一性约束&#xff08;UNIQUE&#xff09;检查约…

安科瑞:列头柜、监控系统、触摸屏的数据中心机房配电方案

摘要 安科瑞精密配电系统是安科瑞针对数据中心集中监控要求提供的多回路监控装置&#xff0c;监控多回路电参量并可对各种故障进行告警。主要适用于各类列头柜、精密配电柜、电源分配柜、UPS输出柜等末端配电设备的监控。在阐述数据中心机房机柜配电目标要求的基础上&#xff…

Redis分布式缓存方案

分布式缓存 单节点Redis问题 数据丢失&#xff1a;数据持久化并发能力弱&#xff1a;搭建主从集群&#xff0c;实现读写分离故障恢复问题&#xff1a;哨兵实现健康检测&#xff0c;自动恢复存储能力&#xff1a;搭建分片集群&#xff0c;利用插槽机制实现动态扩容 Redis持久…

Windows逆向安全(一)之基础知识(十八)

指针作为参数 代码 #include "stdafx.h" void function(int num){num52; } void function2(int* num){*num52; } int main(int argc, char* argv[]) {int num610;function(num);printf("%d\n",num);function2(&num);printf("%d\n",num);re…

KVM NAT 模型

目录 NAT原理 virbr0-nic作用 关于kvm中的网桥和virbr0-nic网卡需注意以下几点&#xff1a; NAT原理 virbr0-nic作用 在kvm中每次通过kvm创建一个网桥都会自动在该网桥下创建两个接口&#xff0c;与网桥同名的virbr0接口代表这个虚拟网桥&#xff0c;可以通过它来配置网桥…

vue2的生命周期

生命周期就是记录数据的状态。对数据进行操作 刚开始 new Vue() 创建了一个实例对象 beforeCreate() 数据还没有创建出来 created() 数据创建出来了&#xff0c;可以访问 判断有没有el 或 template 后 将模板编译成渲染函数 beforeMount() 数据还没有挂在到页面上面 mou…

引入tiff.js报错

当我们安装引入tiff.js时&#xff0c;会遇到这些问题&#xff0c;相关命令以及错误&#xff1a; 1、安装tiff依赖 npm install tiff.js 2、引入tiff import Tiff from tiff.js 3、错误展示&#xff1a; 这个错误是因为没有在vue.config.js以及webpack.base.conf.js(在build文…

C4D的GPU渲染器Octane和Redshift的渲染对比

对CG圈创作人员来说&#xff0c;除制作软件外渲染器是平时接触最多的一类软件&#xff0c;用渲染器进行渲染的过程&#xff0c;就是把制作软件里的预览效果变到融合材质、光照、物理特性的最终效果的这个过程&#xff0c;这是CG制作中最重要的一步&#xff0c;关乎着最终效果的…

头歌--shell脚本入门 变量、字符串--第3关:shell 字符串

任务描述 本关带领大家熟悉 shell 的变量并掌握其使用。 相关知识 字符串概念 字符串是 shell 编程中最常用最有用的数据类型&#xff08;除了数字和字符串&#xff0c;也没啥其它类型好用了&#xff09;&#xff0c;字符串可以用单引号&#xff0c;也可以用双引号&#xff…

数据库系统概论--期末复习

目录 一.绪论 一.数据库系统概述 二.数据模型 三.数据库系统的结构 四.数据库系统的组成 二.关系数据库 一.关系数据结构及形式化定义 二.关系操作 三.关系的完整性 四.关系代数 三.SQL语言 一.数据定义 二.数据查询 三.数据更新 四.视图 四.数据库安全性 1.不安…

Access Token 访问令牌 如何获取?

一、引用 三方库导入时&#xff0c;通常需要输入账号和令牌进行鉴权。账号为指定平台的 HTTP 克隆账号&#xff0c;访问令牌即 Access Token&#xff0c;本文介绍如何获取常见三方代码平台的Access Token。 Access Token 通常在代码平台的个人账号设置内进行管理和配置&#xf…

给konva加个刻度尺

给konva加个刻度尺 最近在用konva做一些&#xff0c;一开始写了不少辅助函数。帮助自己给物体定位 &#xff0c;现在贡献出来给大家用。 给图层增加刻度尺 顾名思义就是加个刻度显示&#xff0c;效果如下&#xff1a; 代码&#xff1a; 第一个参数时layer&#xff0c;第二个…

居民配电所远程监控解决方案

一、项目背景 随着城市建设提速发展、能源利用日益提高、环保节能成为了城市发展的新趋势&#xff0c;配电站逐渐成为企业和居民生活中不可或缺的组成部分。居民的生活用电需求也日益增大。如果没有及时处理好用电安全很容易出现电力中断等情况发生。因此及时高效地为用电客户…

区域LIS源码,基于云计算B/S架构医学实验室检验系统源码

基于B/S架构的医学实验室检验系统源码&#xff0c;整个系统的运行基于WEB层面&#xff0c;只需要在对应的工作台安装一个浏览器软件有外网即可访问。全套系统采用云部署模式&#xff0c;部署一套可支持多家医院检验科共同使用。 采用.Net Core新的技术框架、DEV报表、前端js封…

CASS方格网法土石方量计算

1、打开软件&#xff0c;在“绘图处理”-“展野外测点点号”&#xff0c;默认比例尺为1:500&#xff0c;如下&#xff1a; 2、打开后&#xff0c;在命令行输入pl&#xff0c;绘制范围线&#xff0c;将所有点大致圈起来&#xff0c;如下&#xff1a; 展点结果 范围线绘制结果 3…

【软件测试面试】性能测试常问面试题?不备这些真不敢去面了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试的应用领…

给图片加颜色边框怎么弄,3种好用方法

给图片加颜色边框怎么弄&#xff1f;图片是我们每个人平时都必不可少的文件之一&#xff0c;尤其是在工作中的使用&#xff0c;不同的使用场景对于图片的要求也是不一样的&#xff0c;这就要求我们具有一定的图片处理技能&#xff0c;现在任何事情都离不开电脑&#xff0c;所以…

操作系统原理 —— 进程状态切换具体做了哪些事情?(八)

什么是进程控制&#xff1f; 进程控制的主要功能是&#xff1a;对系统中的所有进程实施有效的管理&#xff0c;它具有创建新进程、撤销已有进程、实现进程状态转换等功能。 简单来说&#xff0c;就是进程控制就是要现实进程状态的转换。 那如何实现进程的控制呢&#xff1f;…

案例01-tlias智能学习辅助系统02-文件上传

4.6 文件上传 若要实现下方的文件上传页面&#xff0c;需要前后端做如下操作&#xff1a; 后端&#xff1a; 本地存储 云存储 阿里云官方文档说明 具体实现参考上方连接&#xff0c;不再详细说明 阿里云OSS快速入门请参考文档&#xff1a; AliOSS.md &#xff08;在talis文…

不同场景下的并发容器选择

在并发编程中&#xff0c;我们经常会使用容器来存储数据或对象&#xff0c;可以依据场景的变化选择多种容器。 Map并发容器 因为在 JDK1.7 之前&#xff0c;在并发场景下使用 HashMap 会出现死循环&#xff0c;从而导致 CPU 使用率居高不下&#xff0c;而扩容是导致死循环的主…