Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/release-notes/versions/1.7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### 运行时界面与调试环境保护

- APK/AAB 的 CLI 与 GUI 新增可选防截图、Activity 豁免、ADB 策略及前台环境复查;所有新选项默认关闭,已有配置保持原行为。
- 防截图覆盖 Activity、Dialog 和平台 AlertDialog.Builder,保护调用保留业务虚方法分派;业务通过 Window API 清除安全标记时会重新保留该标记。
- ADB 支持关闭、仅报告、限制运行三种策略。限制运行会在调试开启或无法确认关闭时拒绝启动,并在前台复查;这是主动阻断策略,调试设备应使用报告模式。
- APK 增加保守的资源名称混淆,只改写可证明安全的 id 名称并保留资源数值 ID;检测到 Native 库、assets、动态查找或不支持的资源格式时跳过。AAB 暂不支持资源名称混淆。
- 将通用线程名 gmain/gdbus 调整为弱信号,降低普通应用被误判的风险。旧运行时资源会明确拒绝不支持的新选项。

### 使用与验证范围

- 窗口调用改写和资源名称混淆需要 JDK 17;普通加固与签名仍保持原有 Java 要求。
- 已在 Android 15 / arm64 荣耀设备验证界面保护、Dialog、平台 Builder、页面豁免、资源 ID 兼容和 ADB 启动阻断;默认及严格加固模式验证包含双 DEX、业务 SO、资源 openFd、覆盖安装、首次启动与缓存后启动。
- 防截图会影响截屏、录屏、投屏和任务缩略图,应为分享等必要页面设置豁免。FLAG_SECURE 不隐藏无障碍视图树,也不阻止 ADB 输入操作。
- 自测不代表所有业务包、ROM 或 Android 版本均已验证。无线调试切换、第三方窗口实现、多窗口及业务全流程仍需按实际应用回归;启用前请阅读 [界面保护说明](https://github.com/kairowan/Shellsmith/blob/main/docs/design/runtime-ui-protection.md)。
- DEXB v6 仍不提供针对离线静态分析的载荷机密性,本次没有改变这一边界。
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/shield-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "shield-cli"
version = "1.6.3"
version = "1.7.0"
edition = "2021"
authors = ["MocikaDev"]
license = "MIT OR Apache-2.0"
Expand Down
77 changes: 77 additions & 0 deletions apps/shield-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub(crate) struct ProtectIosArgs {

#[derive(Args, Default)]
pub struct ProtectAabArgs {
#[command(flatten)]
pub runtime_ui: RuntimeUiArgs,
#[arg(short, long, value_name = "AAB")]
pub input: PathBuf,
#[arg(short, long, value_name = "AAB")]
Expand Down Expand Up @@ -334,6 +336,8 @@ pub(crate) struct ProtectXopArgs {

#[derive(Args, Default)]
pub(crate) struct ProtectArgs {
#[command(flatten)]
pub runtime_ui: RuntimeUiArgs,
/// 明确排除不需要且不受支持的 ABI(逗号分隔),仅对本次任务有效。
#[arg(long, value_delimiter = ',')]
pub exclude_abis: Vec<String>,
Expand Down Expand Up @@ -402,3 +406,76 @@ pub(crate) struct SignArgs {
#[arg(long)]
pub json: bool,
}

/// APK/AAB 共用界面保护参数;Option 保留命令行覆盖配置文件的语义。
#[derive(Args, Default)]
pub struct RuntimeUiArgs {
/// 防止受保护窗口被系统截图;不阻止视图树读取。
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub secure_screen: Option<bool>,
/// 防截图例外,使用完整 Activity 类名;逗号分隔。
#[arg(long, value_delimiter = ',')]
pub screen_exempt_activities: Option<Vec<String>>,
/// 调试环境策略:off、report、block;block 对未知状态也拒绝运行。
#[arg(long)]
pub adb_policy: Option<shield_core::AdbPolicy>,
/// 在应用前台定期复查已有反调试/Root 策略。
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub runtime_recheck: Option<bool>,
/// 保守混淆可安全改名的资源 id 条目(暂仅 APK)。
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub obfuscate_resource_names: Option<bool>,
/// 资源条目保留名;逗号分隔。
#[arg(long, value_delimiter = ',')]
pub resource_name_keep: Option<Vec<String>>,
/// 界面/资源变换器 JAR;默认查找随包分发的 xop-pvm2-packer.jar。
#[arg(long, value_name = "JAR")]
pub ui_packer: Option<PathBuf>,
}

impl RuntimeUiArgs {
pub fn resolve(
&self,
mut base: shield_core::RuntimeUiProtection,
) -> anyhow::Result<shield_core::RuntimeUiProtection> {
if let Some(v) = self.secure_screen {
base.secure_screen = v;
if !v {
base.screen_exempt_activities.clear();
}
}
if let Some(v) = &self.screen_exempt_activities {
base.screen_exempt_activities = v.clone();
}
if let Some(v) = self.adb_policy {
base.adb_policy = v;
}
if let Some(v) = self.runtime_recheck {
base.runtime_recheck = v;
}
if let Some(v) = self.obfuscate_resource_names {
base.obfuscate_resource_names = v;
if !v {
base.resource_name_keep.clear();
}
}
if let Some(v) = &self.resource_name_keep {
base.resource_name_keep = v.clone();
}
base.validate()?;
Ok(base)
}

#[allow(dead_code)] // GUI 通过库入口使用,CLI 二进制不调用。
pub fn from_options(v: shield_core::RuntimeUiProtection, ui_packer: Option<PathBuf>) -> Self {
Self {
secure_screen: Some(v.secure_screen),
screen_exempt_activities: Some(v.screen_exempt_activities),
adb_policy: Some(v.adb_policy),
runtime_recheck: Some(v.runtime_recheck),
obfuscate_resource_names: Some(v.obfuscate_resource_names),
resource_name_keep: Some(v.resource_name_keep),
ui_packer,
}
}
}
11 changes: 11 additions & 0 deletions apps/shield-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub(crate) fn run_protect(args: ResolvedProtectArgs) -> Result<()> {
}

let opts = ProtectOptions {
runtime_ui: args.runtime_ui,
ui_packer_path: args.ui_packer,
excluded_abis: args.excluded_abis,
input: args.input,
output: args.output,
Expand Down Expand Up @@ -165,6 +167,13 @@ pub fn run_protect_aab_with_control(
on_progress: impl Fn(&str, &str) + Send + Sync + 'static,
cancel: Arc<AtomicBool>,
) -> Result<()> {
let runtime_ui = args.runtime_ui.resolve(Default::default())?;
if runtime_ui.enabled() && matches!(args.engine, ProtectionEngineArg::Xop) {
anyhow::bail!("独立 Xop 壳尚不支持运行时界面保护选项");
}
if runtime_ui.obfuscate_resource_names {
anyhow::bail!("资源名称混淆暂仅支持 APK,AAB 跨模块资源名称一致性尚未验证");
}
let on_progress = Arc::new(on_progress);
on_progress("CheckTools", "检查 AAB、签名和构建工具");
ensure_aab_not_cancelled(&cancel)?;
Expand Down Expand Up @@ -386,6 +395,8 @@ pub fn run_protect_aab_with_control(
match args.engine {
ProtectionEngineArg::Mocika => {
let protect_opts = ProtectOptions {
runtime_ui: runtime_ui.clone(),
ui_packer_path: args.runtime_ui.ui_packer.clone(),
excluded_abis: args.exclude_abis.clone(),
input: universal_apk,
output: protected_unsigned.clone(),
Expand Down
49 changes: 49 additions & 0 deletions apps/shield-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub(crate) struct CliConfig {
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ProtectConfig {
#[serde(default)]
runtime_ui: shield_core::RuntimeUiProtection,
ui_packer: Option<PathBuf>,
input: Option<PathBuf>,
output: Option<PathBuf>,
apktool: Option<PathBuf>,
Expand Down Expand Up @@ -68,6 +71,12 @@ impl CliConfig {

pub(crate) fn merge_protect(&self, args: ProtectArgs) -> Result<ResolvedProtectArgs> {
Ok(ResolvedProtectArgs {
runtime_ui: args.runtime_ui.resolve(self.protect.runtime_ui.clone())?,
ui_packer: args
.runtime_ui
.ui_packer
.clone()
.or_else(|| self.protect.ui_packer.clone()),
excluded_abis: args.exclude_abis,
input: required_path(args.input.or_else(|| self.protect.input.clone()), "--input")?,
output: required_path(
Expand Down Expand Up @@ -130,6 +139,7 @@ impl CliConfig {
&mut self.protect.input,
&mut self.protect.output,
&mut self.protect.apktool,
&mut self.protect.ui_packer,
&mut self.protect.resources,
&mut self.protect.apksigner,
&mut self.sign.input,
Expand All @@ -145,6 +155,8 @@ impl CliConfig {
}

pub(crate) struct ResolvedProtectArgs {
pub runtime_ui: shield_core::RuntimeUiProtection,
pub ui_packer: Option<PathBuf>,
pub excluded_abis: Vec<String>,
pub input: PathBuf,
pub output: PathBuf,
Expand Down Expand Up @@ -224,6 +236,43 @@ fn parse_keystore_type(value: Option<&str>) -> Result<Option<KeystoreTypeArg>> {

#[cfg(test)]
mod tests {
#[test]
fn 界面策略命令行关闭可覆盖配置名单且非法值拒绝() {
use clap::Parser;
let config: CliConfig = toml::from_str(
r#"
schema_version = 1
[protect]
input = "input.apk"
output = "output.apk"
[protect.runtime_ui]
secure_screen = true
screen_exempt_activities = ["com.example.ShareActivity"]
adb_policy = "report"
"#,
)
.unwrap();
let cli = crate::args::Cli::try_parse_from([
"shield",
"protect",
"--secure-screen=false",
"--adb-policy",
"block",
])
.unwrap();
let crate::args::Commands::Protect(args) = cli.command else {
panic!("命令类型错误")
};
let options = config.merge_protect(args).unwrap().runtime_ui;
assert!(!options.secure_screen);
assert!(options.screen_exempt_activities.is_empty());
assert_eq!(options.adb_policy, shield_core::AdbPolicy::Block);
assert!(
crate::args::Cli::try_parse_from(["shield", "protect", "--adb-policy", "allow"])
.is_err()
);
}

#[test]
fn 排除架构参数仅对本次命令有效() {
use clap::Parser;
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod config;

pub use args::{
AiResistanceArg, EnvironmentPolicyArg, KeystoreTypeArg, ProtectAabArgs, ProtectionEngineArg,
ProtectionProfileArg, XopProfileArg,
ProtectionProfileArg, RuntimeUiArgs, XopProfileArg,
};
pub use commands::{run_check_aab, run_protect_aab_with_control};
pub use shield_core::*;
4 changes: 2 additions & 2 deletions apps/shield-gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/shield-gui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mocika-shield-gui",
"version": "1.6.3",
"version": "1.7.0",
"license": "MIT OR Apache-2.0",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/shield-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mocika-shield"
version = "1.6.3"
version = "1.7.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
Expand Down
4 changes: 4 additions & 0 deletions apps/shield-gui/src-tauri/src/aab_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use tauri::Manager;
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AabProtectRequest {
#[serde(default)]
pub(crate) runtime_ui: shield_core::RuntimeUiProtection,
pub(crate) task_id: String,
pub(crate) input: String,
pub(crate) output: String,
Expand Down Expand Up @@ -73,6 +75,7 @@ pub(crate) async fn execute_protect_aab(
.ok_or_else(|| ExecutionFailure::from("当前安装包缺少 Android 运行时资源".to_string()))?;
let apksigner = find_apksigner_path(&app)
.ok_or_else(|| ExecutionFailure::from("当前安装包缺少 apksigner".to_string()))?;
let ui_packer = find_xop_pvm2_packer_path(&app);
let pvm2 = if request.xop_true_vmp_prefixes.is_empty() {
None
} else {
Expand All @@ -89,6 +92,7 @@ pub(crate) async fn execute_protect_aab(
let key_password =
(!certificate.key_password.is_empty()).then_some(certificate.key_password);
let args = ProtectAabArgs {
runtime_ui: shield_cli::RuntimeUiArgs::from_options(request.runtime_ui, ui_packer),
input: strip_unc_prefix(PathBuf::from(request.input)),
output: strip_unc_prefix(PathBuf::from(request.output)),
bundletool,
Expand Down
4 changes: 4 additions & 0 deletions apps/shield-gui/src-tauri/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl ApplicationSharingConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub(crate) struct ProtectDefaults {
pub runtime_ui: shield_core::RuntimeUiProtection,
pub runtime_mode: String,
pub environment_policy: String,
pub protection_profile: String,
Expand All @@ -85,6 +86,7 @@ pub(crate) struct ProtectDefaults {
impl Default for ProtectDefaults {
fn default() -> Self {
Self {
runtime_ui: Default::default(),
runtime_mode: "standard".to_string(),
environment_policy: "compatible".to_string(),
protection_profile: "balanced".to_string(),
Expand Down Expand Up @@ -228,6 +230,7 @@ pub(crate) fn normalize_theme_mode(value: &str) -> String {

pub(crate) fn normalize_protect_defaults(value: &ProtectDefaults) -> ProtectDefaults {
ProtectDefaults {
runtime_ui: value.runtime_ui.clone(),
runtime_mode: match value.runtime_mode.as_str() {
"android_api19" => "android_api19",
_ => "standard",
Expand Down Expand Up @@ -435,6 +438,7 @@ theme_mode = "system"
#[test]
fn 非法加固默认项会被规范化() {
let normalized = normalize_protect_defaults(&ProtectDefaults {
runtime_ui: Default::default(),
runtime_mode: "unknown".to_string(),
environment_policy: "unknown".to_string(),
protection_profile: "unknown".to_string(),
Expand Down
3 changes: 3 additions & 0 deletions apps/shield-gui/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ use updates::{check_update_impl, UpdateCheckResult};
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProtectRequest {
#[serde(default)]
runtime_ui: shield_core::RuntimeUiProtection,
sharing: Option<application_sharing::SharingChoice>,
#[serde(default)]
excluded_abis: Vec<String>,
Expand Down Expand Up @@ -213,6 +215,7 @@ async fn protect_apk(
apktool_path: request.apktool_path,
runtime_mode: request.runtime_mode,
environment_policy: request.environment_policy,
runtime_ui: request.runtime_ui,
protection_profile: request.protection_profile,
ai_resistance: request.ai_resistance,
xop_pvm2_packer_path: request.xop_pvm2_packer_path,
Expand Down
Loading
Loading