开启adb root

直接看adb源码:

  • __android_log_is_debuggable就是判断ro.debuggable属性值,感兴趣可以在 源码下grep下实现看看。
  • auth_required :在adb源码下定义的全局变量,默认等于true,。看名字就是是否需要用户授权的flag, 这里不再继续跟代码,只要知道这个值在ro.debuggable=1时,会等于ro.adb.secure这个属性即可。我们不想开启adb时弹出用户授权,因此要将这个属性改为0.
// /packages/modules/adb/daemon/main.cpp
int adbd_main(int server_port) {...#if defined(__ANDROID__)// If we're on userdebug/eng or the device is unlocked, permit no-authentication.bool device_unlocked = "orange" == android::base::GetProperty("ro.boot.verifiedbootstate", "");if (__android_log_is_debuggable() || device_unlocked) {auth_required = android::base::GetBoolProperty("ro.adb.secure", false);}
#endif...
}

当在PC端执行adb root时,android系统会走到下面代码,可以看到如果__android_log_is_debuggable不为true的话,就return了。
函数最后设置了service.adb.root=1,实际测试了下,adb root后再adb shell,
这个属性=1,直接adb shell进去,这个属性=0

// /packages/modules/adb/daemon/restart_service.cppvoid restart_root_service(unique_fd fd) {if (getuid() == 0) {WriteFdExactly(fd.get(), "adbd is already running as root\n");return;}if (!__android_log_is_debuggable()) {WriteFdExactly(fd.get(), "adbd cannot run as root in production builds\n");return;}LOG(INFO) << "adbd restarting as root";android::base::SetProperty("service.adb.root", "1");WriteFdExactly(fd.get(), "restarting adbd as root\n");
}

service.adb.root使用的地方:
PC端执行adb shell后,会走到下面代码,在main函数里,会判断should_drop_privileges,然后通过linux capability机制
给进程降低权限,感兴趣可以搜一下linux capability机制。这里推测就是是否从root降低权限到shell用户。

// packages/modules/adb/daemon/main.cpp
int adbd_main(int server_port) {
...
#if defined(__ANDROID__)drop_privileges(server_port);
#endif
...
}
static void drop_privileges(int server_port) {
...if (should_drop_privileges()) {const bool should_drop_caps = !__android_log_is_debuggable();if (should_drop_caps) {minijail_use_caps(jail.get(), CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID));}minijail_change_gid(jail.get(), AID_SHELL);minijail_change_uid(jail.get(), AID_SHELL);// minijail_enter() will abort if any priv-dropping step fails.minijail_enter(jail.get());// Whenever ambient capabilities are being used, minijail cannot// simultaneously drop the bounding capability set to just// CAP_SETUID|CAP_SETGID while clearing the inheritable, effective,// and permitted sets. So we need to do that in two steps.using ScopedCaps =std::unique_ptr<std::remove_pointer<cap_t>::type, std::function<void(cap_t)>>;ScopedCaps caps(cap_get_proc(), &cap_free);...
}

关键在should_drop_privileges:
ro.secure在user,userdebug版本=1,也就是说adb shell进来 默认是降权的,非root的。
如果**__android_log_is_debuggable && service.adb.root 为true**,就不会降权为非root. 前面说了PC端输入adb root后, service.adb.root 这个值就会等于1,
综合上面分析:只要把ro.debuggable设置为1,就可以在user版本下实现adb root

static bool should_drop_privileges() {// The properties that affect `adb root` and `adb unroot` are ro.secure and// ro.debuggable. In this context the names don't make the expected behavior// particularly obvious.//// ro.debuggable://   Allowed to become root, but not necessarily the default. Set to 1 on//   eng and userdebug builds.//// ro.secure://   Drop privileges by default. Set to 1 on userdebug and user builds.bool ro_secure = android::base::GetBoolProperty("ro.secure", true);bool ro_debuggable = __android_log_is_debuggable();// Drop privileges if ro.secure is set...bool drop = ro_secure;// ... except "adb root" lets you keep privileges in a debuggable build.std::string prop = android::base::GetProperty("service.adb.root", "");bool adb_root = (prop == "1");bool adb_unroot = (prop == "0");if (ro_debuggable && adb_root) {drop = false;}// ... and "adb unroot" lets you explicitly drop privileges.if (adb_unroot) {drop = true;}

具体的修改策略:
在编译版本时,定义一个全局变量,可以把它定义在编译脚本中,

export USE_ROOT_ADB =1

USE_ROOT_ADB 代表在user版本中启用adb root

# /build/make/core/main.mk
## user/userdebug ##
# +++ 这里加一个编译过程中对USE_ROOT_ADB的值的打印。
$(warning USE_ROOT_ADB $(USE_ROOT_ADB))
# user_variant:不是user,就是userdebug
user_variant := $(filter user userdebug,$(TARGET_BUILD_VARIANT))
enable_target_debugging := true
tags_to_install :=
ifneq (,$(user_variant))# Target is secure in user builds.ADDITIONAL_SYSTEM_PROPERTIES += ro.secure=1ADDITIONAL_SYSTEM_PROPERTIES += security.perf_harden=1ifeq ($(user_variant),user)# +++ 如果是user版本,并且USE_ROOT_ADB=1,将ro.adb.secure设置为0ifeq ($(USE_ROOT_ADB),true)ADDITIONAL_SYSTEM_PROPERTIES += ro.adb.secure=0elseADDITIONAL_SYSTEM_PROPERTIES += ro.adb.secure=1endifendififeq ($(user_variant),userdebug)# Pick up some extra useful toolstags_to_install += debugelse# +++ 如果是user版本,并且USE_ROOT_ADB=1,将tags_to_install 追加debug字段,# +++ 还有要注意,源码未修改前,如果是user版本,enable_target_debugging会被置为空# +++ 这里改为:如果是user版本,enable_target_debugging依然是true.# @@@ 后面继续分析tags_to_install 和 enable_target_debuggingifeq ($(USE_ROOT_ADB),true)# Pick up some extra useful toolstags_to_install += debugelse# Disable debugging in plain user builds.enable_target_debugging :=endifendif   # Disallow mock locations by default for user buildsADDITIONAL_SYSTEM_PROPERTIES += ro.allow.mock.location=0else # !user_variant# Turn on checkjni for non-user builds.ADDITIONAL_SYSTEM_PROPERTIES += ro.kernel.android.checkjni=1# Set device insecure for non-user builds.ADDITIONAL_SYSTEM_PROPERTIES += ro.secure=0# Allow mock locations by default for non user buildsADDITIONAL_SYSTEM_PROPERTIES += ro.allow.mock.location=1
endif # !user_variant
# @@@ 如果enable_target_debugging为true,ro.debuggable=1
# @@@ 达到了我们想要的效果
ifeq (true,$(strip $(enable_target_debugging)))# Target is more debuggable and adbd is on by defaultADDITIONAL_SYSTEM_PROPERTIES += ro.debuggable=1# Enable Dalvik lock contention logging.ADDITIONAL_SYSTEM_PROPERTIES += dalvik.vm.lockprof.threshold=500
else # !enable_target_debugging# Target is less debuggable and adbd is off by defaultADDITIONAL_SYSTEM_PROPERTIES += ro.debuggable=0
endif # !enable_target_debugging

adb remount 和 su

这两个放到一起说,上一节说到将tags_to_install 追加debug字段。
还是在/build/make/core/main.mk中,tags_to_install=true,编译时会带上PRODUCT_PACKAGES_DEBUG

 
# $(1): product makefile
define product-installed-files$(eval _pif_modules := \$(call get-product-var,$(1),PRODUCT_PACKAGES) \$(if $(filter eng,$(tags_to_install)),$(call get-product-var,$(1),PRODUCT_PACKAGES_ENG)) \# @@@ 当tags_to_install有debug字段,编译时会带上PRODUCT_PACKAGES_DEBUG$(if $(filter debug,$(tags_to_install)),$(call get-product-var,$(1),PRODUCT_PACKAGES_DEBUG)) \$(if $(filter tests,$(tags_to_install)),$(call get-product-var,$(1),PRODUCT_PACKAGES_TESTS)) \$(if $(filter asan,$(tags_to_install)),$(call get-product-var,$(1),PRODUCT_PACKAGES_DEBUG_ASAN)) \$(if $(filter java_coverage,$(tags_to_install)),$(call get-product-var,$(1),PRODUCT_PACKAGES_DEBUG_JAVA_COVERAGE)) \$(call auto-included-modules) \) \

PRODUCT_PACKAGES_DEBUG定义在/build/make/target/product/base_system.mk, 当然也可以在自己的mk中对PRODUCT_PACKAGES_DEBUG进行追加。su 就在PRODUCT_PACKAGES_DEBUG中,如果再加上remount, 就实现了user版本下使用su和adb remount

383 # Packages included only for eng or userdebug builds, previously debug tagged
384 PRODUCT_PACKAGES_DEBUG := \
385     adb_keys \
386     arping \
387     dmuserd \
388     idlcli \
389     init-debug.rc \
390     iotop \
391     iperf3 \
392     iw \
393     layertracegenerator \
394     libclang_rt.ubsan_standalone \
395     logpersist.start \
396     logtagd.rc \
397     procrank \
398     profcollectd \
399     profcollectctl \
400     record_binder \
401     servicedispatcher \
402     showmap \
403     sqlite3 \
404     ss \
405     start_with_lockagent \
406     strace \# @@@ 已经有su
407     su \
408     sanitizer-status \
409     tracepath \
410     tracepath6 \
411     traceroute6 \
412     unwind_info \
413     unwind_reg_info \
414     unwind_symbols \# +++ 加上remountremount \

另外这里还涉及到selinux,
在system/sepolicy/private下,可以看到su.te中对su的定义:

permissive su;

su被定义为permissive domain,这是什么?
参考redhat上的定义:

Permissive Domains

When SELinux is running in permissive mode, SELinux does not deny access, but denials are logged for actions that would have been denied if running in enforcing mode. Previously, it was not possible to make a single domain permissive (remember: processes run in domains). In certain situations, this led to making the whole system permissive to troubleshoot issues.
Permissive domains allow an administrator to configure a single process (domain) to run permissive, rather than making the whole system permissive. SELinux checks are still performed for permissive domains; however, the kernel allows access and reports an AVC denial for situations where SELinux would have denied access.
Permissive domains have the following uses:
They can be used for making a single process (domain) run permissive to troubleshoot an issue without putting the entire system at risk by making it permissive.
They allow an administrator to create policies for new applications. Previously, it was recommended that a minimal policy be created, and then the entire machine put into permissive mode, so that the application could run, but SELinux denials still logged. audit2allow could then be used to help write the policy. This put the whole system at risk. With permissive domains, only the domain in the new policy can be marked permissive, without putting the whole system at risk.

理解就是可以在强制模式(enforcing mode)下,定义一个宽容模式的domain,跑在Permissive domain下的进程,只会打印avc log,而不会限制它的行为。su这么牛逼的进程,当然是Permissive domain。
但是在user版本下,是不允许定义Permissive domain的。 为此需要修改如下代码:

# /system/sepolicy/Android.mk$(LOCAL_BUILT_MODULE): $(sepolicy.recovery.conf) $(HOST_OUT_EXECUTABLES)/checkpolicy \$(HOST_OUT_EXECUTABLES)/sepolicy-analyze@mkdir -p $(dir $@)$(hide) $(CHECKPOLICY_ASAN_OPTIONS) $(HOST_OUT_EXECUTABLES)/checkpolicy -M -c \$(POLICYVERS) -o $@.tmp $<$(hide) $(HOST_OUT_EXECUTABLES)/sepolicy-analyze $@.tmp permissive > $@.permissivedomains# +++ 下面的if表示如果是user版本,并且permissivedomains存在的话,就会输出如下错误,并退出。因此需要用USE_ROOT_ADB宏来排除这部分处理。注意:该文件下有2处检查permissivedomains 的地方,需要用同样的方式注释掉。
ifndef  USE_ROOT_ADB$(hide) if [ "$(TARGET_BUILD_VARIANT)" = "user" -a -s $@.permissivedomains ]; then \echo "==========" 1>&2; \echo "ERROR: permissive domains not allowed in user builds" 1>&2; \echo "List of invalid domains:" 1>&2; \cat $@.permissivedomains 1>&2; \exit 1; \fi
endif$(hide) mv $@.tmp $@

关闭selinux

临时关闭selinux,只要设置setenforce 0即可,但重启后就会恢复为强制模式。
这里讨论永久关闭。
如果不想深究,直接修改IsEnforcing(),让它永久返回false, 这样走到security_setenforce,永久关闭selinux。

// /system/core/init/selinux.cppvoid SelinuxSetEnforcement() {bool kernel_enforcing = (security_getenforce() == 1);bool is_enforcing = IsEnforcing();if (kernel_enforcing != is_enforcing) {if (security_setenforce(is_enforcing)) {PLOG(FATAL) << "security_setenforce(" << (is_enforcing ? "true" : "false")<< ") failed";}}if (auto result = WriteFile("/sys/fs/selinux/checkreqprot", "0"); !result.ok()) {LOG(FATAL) << "Unable to write to /sys/fs/selinux/checkreqprot: " << result.error();}
}
bool IsEnforcing() {// MODIFY START 永久返回falsereturn false;// MODIFY ENDif (ALLOW_PERMISSIVE_SELINUX) {return StatusFromProperty() == SELINUX_ENFORCING;}return true;
}

如果想继续看下是怎么实现的,我们继续分析,先看security_getenforce函数,这里不贴代码实现了,源码在external/selinux下,主要就是读取/sys/fs/selinux/enforce 的值。可以是1 or 0
/sys/fs/selinux/enforce中的值,又是根据内核的cmdline中的androidboot.selinux来的。可以在自己的mk中修改这个值:

BOARD_KERNEL_CMDLINE += androidboot.selinux=enforcing or permissive

再分析IsEnforcing函数,如果ALLOW_PERMISSIVE_SELINUX为false时,直接返回true。ALLOW_PERMISSIVE_SELINUX在mk中可以看到,如果是user版本,该值就是false, 这里可以看出,如果编译user版本,一定会开启selinux,即使上面配置内核启动参数cmdline中的androidboot.selinux=permissive,也无济于事。

#/system/core/init/Android.mk# userdebug,eng版本走if,user走else
ifneq (,$(filter userdebug eng,$(TARGET_BUILD_VARIANT)))init_options += \-DALLOW_FIRST_STAGE_CONSOLE=1 \-DALLOW_LOCAL_PROP_OVERRIDE=1 \-DALLOW_PERMISSIVE_SELINUX=1 \-DREBOOT_BOOTLOADER_ON_PANIC=1 \-DWORLD_WRITABLE_KMSG=1 \-DDUMP_ON_UMOUNT_FAILURE=1elseinit_options += \-DALLOW_FIRST_STAGE_CONSOLE=0 \-DALLOW_LOCAL_PROP_OVERRIDE=0 \-DALLOW_PERMISSIVE_SELINUX=0 \-DREBOOT_BOOTLOADER_ON_PANIC=0 \-DWORLD_WRITABLE_KMSG=0 \-DDUMP_ON_UMOUNT_FAILURE=0endif

试想,如果我们修改上面的mk,让ALLOW_PERMISSIVE_SELINUX在user版本下也等于1,会不会也可以实现关闭selinux。继续分析:
如果ALLOW_PERMISSIVE_SELINUX=1,IsEnforcing函数返回值就依赖
StatusFromProperty() == SELINUX_ENFORCING;
在android12之前,StatusFromProperty函数只读取cmdline的androidboot.selinux
那么在android12之前,可以通过修改:
ALLOW_PERMISSIVE_SELINUX=1 并且 BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive
实现user版本永久关闭selinux。

从android12开始,StatusFromProperty函数还会判断Bootconfig的androidboot.selinux值。内核cmdline和Bootconfig的androidboot.selinux值都等于enforcing时,StatusFromProperty会返回enforcing。
那么从android12开始,可以通过修改:
ALLOW_PERMISSIVE_SELINUX=1 并且 ( BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive 或者 BOARD_BOOTCONFIG += androidboot..selinux=permissive)
实现user版本永久关闭selinux。

EnforcingStatus StatusFromProperty() {EnforcingStatus status = SELINUX_ENFORCING;ImportKernelCmdline([&](const std::string& key, const std::string& value) {if (key == "androidboot.selinux" && value == "permissive") {status = SELINUX_PERMISSIVE;}});if (status == SELINUX_ENFORCING) {ImportBootconfig([&](const std::string& key, const std::string& value) {if (key == "androidboot.selinux" && value == "permissive") {status = SELINUX_PERMISSIVE;}});}return status;}

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

金融专业高分简历撰写指南

一、金融求职简历原则&#xff1a;深度与亮点并存在金融行业求职时&#xff0c;一份出色的简历需突出经历深度与亮点。01 教育背景需如实填写毕业院校、专业、GPA及所学课程。金融行业不少公司对求职者学校和学历有严格标准&#xff0c;如“985”“211”院校或硕士以上学历等。…

专题:2025生命科学与生物制药全景报告:产业图谱、投资方向及策略洞察|附130+份报告PDF、原数据表汇总下载

原文链接&#xff1a;https://tecdat.cn/?p43526 过去一年&#xff0c;全球生命科学VC融资回暖至1021.5亿美元&#xff0c;并购交易虽下滑23%却聚焦关键赛道&#xff0c;创新药管线中GLP-1受体激动剂以170亿美元市场规模领跑&#xff0c;AI技术将研发周期缩短60%……这些数据背…

Compose笔记(四十)--ClickableText

这一节主要了解一下Compose中的ClickableText&#xff0c;在Jetpack Compose中&#xff0c;ClickableText是用于创建可点击文本的组件&#xff0c;其核心功能是通过声明式语法将文本设置为交互式元素&#xff0c;用户点击时可触发特定操作。简单总结如下:API含义 text&#xff…

面试必刷的数组三连:原地删除与合并

坚持用 清晰易懂的图解 多语言代码&#xff0c;让每道题变得简单&#xff01; 呆头个人主页详情 呆头个人Gitee代码仓库 呆头详细专栏系列 座右铭&#xff1a; “不患无位&#xff0c;患所以立。” 面试必刷的数组三连&#xff1a;原地删除与合并前言目录1.移除元素2.删除有序…

力扣经典算法篇-41-旋转图像(辅助数组法,原地旋转法)

1、题干 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a;输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]]…

译|用户增长策略如何使用因果机器学习的案例

来自上传文件中的文章《[Causal Machine Learning for Growth: Loyalty Programs, LTV, and What to Do When You Can’t Experiment | by Torty Sivill | Towards AI]》 本文探讨了当 A/B 测试不可行时&#xff0c;如何利用因果推断从历史数据中获取洞察。技术亮点在于通过构建…

java~final关键字

final关键字final基本介绍final的使用细节final基本介绍 final是最终的意思&#xff0c;可以修饰类&#xff0c;属性&#xff0c;方法&#xff0c;局部变量什么时候会要使用到final呢&#xff1f; 1.想要类不被继承时 2.不希望类的某个属性的值被改变时 3.不想父类的某个方法被…

Node.js(四)之数据库与身份认证

数据库与身份认证 目录 数据库与身份认证 十三、数据库的基本概念 13.1 什么是数据库 13.2 常见的数据库及分类 13.3 传统型数据库的数据组织结构 1. Excel 的数据组织结构 2. 传统型数据库的数据组织结构 3. 实际开发中库、表、行、字段的关系 十四、安装并配置MySQ…

SpringBoot+SpringMVC常用注解

文章目录发展历程项目创建项目结构入门案例配置文件的两种方式&#xff1a;只能使用一种创建项目二入门案例常用知识及注解Controller:类上面加&#xff0c;SpringMVC的注解GetMapping:方法上面加Spring框架的两项核心功能Component:组件。控制反转&#xff0c;加在业务类上面&…

标准GS相位恢复算法

标准GS相位恢复算法详解与MATLAB实现 Gerchberg-Saxton (GS) 算法是一种经典的相位恢复方法&#xff0c;广泛应用于光学成像、衍射成像和全息技术等领域。该算法通过迭代过程从未知相位的强度测量中恢复相位信息。 算法原理 GS算法的核心思想是利用傅里叶变换关系在空间域和频率…

【Linux网络编程基础--socket地址API】

一、主机字节序和网络字节序主机字节序&#xff08;Host Byte Order&#xff09;&#xff1a;你当前电脑的内存字节顺序&#xff08;比如 x86 是小端&#xff09;网络字节序&#xff08;Network Byte Order&#xff09;&#xff1a;统一规定为大端序&#xff08;高位字节在高位…

Linux路径MTU发现(Path MTU Discovery, PMTU)

Linux路径MTU发现&#xff08;Path MTU Discovery, PMTU&#xff09;机制是TCP/IP协议栈中确保数据包高效传输的核心技术。其核心目标是动态探测源主机到目的主机路径上的最小MTU&#xff08;Maximum Transmission Unit&#xff09;&#xff0c;从而避免IP分片&#xff0c;提升…

【MySQL进阶】------MySQL程序

MySQL程序简介 MySQL安装完成通常会包含如下程序&#xff1a; Linux系统程序⼀般在 /usr/bin⽬录下&#xff0c;可以通过命令查看&#xff1a; windows系统⽬录&#xff1a;你的安装路径\MySQL Server 8.0\bin&#xff0c;可以通过命令查看&#xff1a; 每个 MySQL 程序都有许…

Linux大页内存导致服务内存不足

Linux大页内存导致服务内存不足的解决方法 大页内存&#xff08;Huge Pages&#xff09;是Linux内核提供的一种机制&#xff0c;用于减少TLB&#xff08;转换后备缓冲区&#xff09;的压力&#xff0c;提高内存访问性能。然而&#xff0c;如果配置不当&#xff0c;大页内存可能…

超宽带测距+测角+无线通信一体化模组:智能门锁、智能遥控器、AR头戴、智能穿戴

超宽带测距测角无线通信一体化模组&#xff1a;智能门锁、智能遥控器、AR头戴、智能穿戴UWB测距测角技术&#xff0c;因其高精度、低延迟、抗干扰能力&#xff0c;正广泛应用于“人-物-设备”的空间感知场景&#xff0c;成为构建智能空间和精准互动的重要底层技术。代表厂商与产…

基于单片机空气质量检测/气体检测系统

传送门 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目速选一览表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目功能速览 概述 随着环境污染问题日益严重&#xff0c;空气质量监测成为社会关注的焦点。基于单片机的空气质量检…

网络安全 | 从 0 到 1 了解 WAF:Web 应用防火墙到底是什么?

&#x1f914; 写在前面 2020年 我参加公司的安全技能大赛&#xff0c;队友在实操环节启用了 WAF 防火墙&#xff0c;这是我第一次接触到 Web 应用防火墙。作为一个 Web 开发老鸟&#xff0c;真是羞愧呀&#x1f602;。 &#x1f510; Web应用防火墙 WAF 全称是 Web Applica…

服务器突然之间特别卡,什么原因?

原因总结&#xff1a;1.一般是本地网速的问题&#xff0c;服务器网速的问题&#xff0c;服务器CPU被占满的问题今天发现另一个会导致特别卡的问题&#xff0c;是主存占满也会导致卡顿。解释如下&#xff1a;当服务器的主存&#xff08;物理内存&#xff09;被完全占满时&#x…

AI应用标准详解:A2A MCP AG-UI

"OpenAI接入MCP&#xff0c;Google推出A2A&#xff0c;微软与OpenAI紧密绑定"标志着云计算竞争焦点已从"算力"和"模型参数"转向‌Agent标准协议控制权‌。在AI快速演进的今天&#xff0c;我们不再仅关注单个AI的智能水平&#xff0c;而是探索多个…

Web安全学习步骤

以下是Web安全专项学习步骤&#xff0c;聚焦实战能力培养&#xff0c;分为4个阶段资源清单**&#xff0c;适合从入门到进阶。重点培养漏洞挖掘能力与防御方案设计双重视角&#xff1a;---阶段1&#xff1a;Web技术筑基&#xff08;1-2个月&#xff09; | 领域 | 关键…