HNCTF PatrolNote
考点Android APK 逆向 SO 原生层逆向 字符串置换 / AES 密码学 正则分组 数组下标置换逆推 AES-128一、阶段 1Java 层定位 Native 本地方法apk文件直接搜索native进入jadx的native可以看见本题核心函数是nativeVerify两个参数str和str2看见了System.loadLibrary(patrolnote)说明是 JNI函数核心校验在apk解压lib文件夹的libxxx.so文件里JIN函数名格式 Java_包名_类名_方法名包名com_hnctf_patrolnote类名NativeBridge方法名nativeVerify所以函数名是Java_com_hnctf_patrolnote_NativeBridge_nativeVerify二、阶段 2SO 原生层逆向求解 nativeVerify 两个入参apk文件核心校验不在Java层在so文件里。apk文件后缀名改成zip并解压将解压后lib文件夹里面so文件放入Ghidra。解压后 lib/arm64-v8a、x86_64、armeabi-v7a 分别对应不同CPU架构的SO文件x86_64架构SO在IDA中反编译可读性最高所以用这个。直接在Ghidra左侧Simple Tree的Filter里Exports窗口搜索JIN函数名Java_com_hnctf_patrolnote_NativeBridge_nativeVerify定位核心函数。分析伪代码undefined8 Java_com_hnctf_patrolnote_NativeBridge_nativeVerify (long *param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4)这是一个 JNI 函数Java 调用 C 的固定格式JNIEnv* env, jobject thiz, jstring str1, jstring str24 个参数固定对应param_1 JNI 环境固定param_2 this 指针固定param_3 Java 传进来的第一个字符串用户名里的 4 位数字param_4 Java 传进来的第二个字符串授权码去横杠后的 12 位因此strparam_3str2param_4{ ulong uVar1; void *pvVar2; char *pcVar3; long lVar4; undefined8 uVar5; char *pcVar6; char *pcVar7; char *pcVar8; byte *pbVar9; byte *pbVar10; char *pcVar11; ulong local_90 [2]; void *local_80; byte local_78 [8]; undefined8 local_70; byte *local_68; byte local_60 [8]; ulong local_58; char *local_50; long local_48;C 申请临时空间全部不用理解lVar4 tpidr_el0; local_48 *(long *)(lVar4 0x28); FUN_00122028(local_60,param_1,param_3); FUN_00122028(local_78,param_1,param_4);把 Java 字符串转成 C 字符串FUN_00122028 系统函数作用把 Java 字符串转成 C 字符串param_3 → local_60 第一个字符串4 位数字param_4 → local_78 第二个字符串12 位授权码因此strlocal_60str2local_78uVar1 (ulong)(local_60[0] 1); //uVar1 字符串长度 if ((local_60[0] 1) ! 0) { uVar1 local_58; //判断是不是 “长字符串”是则真正长度local_58 } if (uVar1 4) { //判断长度是否 4取第一个字符串str1长度 → 必须等于 4local_60[0]→ 字符串的第一个 8 字节数据 1→ 右移 1 位 → 去掉最低位标记得到真实长度local_60[0] 1→ 取最低位pcVar6 (char *)((ulong)local_60 | 3); //如果是短字符串也取 str1[2] 地址 pcVar3 pcVar6; if ((local_60[0] 1) ! 0) { pcVar3 local_50 2; //判断是长字符串-取第三个字符即str1[2]的地址 } if (*pcVar3 1) { //str1[2]1校验第 3 个字符必须是 1str1 [2] 1pcVar11 (char *)((ulong)local_60 | 1); pcVar3 pcVar11; if ((local_60[0] 1) ! 0) { pcVar3 local_50; //str1 [0] } if (*pcVar3 7) {同上校验第 1个字符必须是 7str1 [0] 7pcVar7 (char *)((ulong)local_60 | 4); pcVar3 pcVar7; if ((local_60[0] 1) ! 0) { pcVar3 local_50 3; //str1 [3] } if (*pcVar3 3) {同上校验第 4个字符必须是 3str1 [3] 3pcVar8 (char *)((ulong)local_60 | 2); pcVar3 pcVar8; if ((local_60[0] 1) ! 0) { pcVar3 local_50 1; } if (*pcVar3 4) {同上校验第 2个字符必须是 4str1 [1] 4因此str1 [2] 1str1 [0] 7str1 [3] 3str1 [1] 4→str17413uVar1 (ulong)(local_78[0] 1); if ((local_78[0] 1) ! 0) { uVar1 local_70; } if (uVar1 12) {第二个字符串str2长度必须 12pbVar10 (byte *)((ulong)local_78 | 1); if ((local_78[0] 1) ! 0) { pbVar10 local_68; } if (*pbVar10 9) { pbVar10 (byte *)((ulong)local_78 | 2); if ((local_78[0] 1) ! 0) { pbVar10 local_68 1; } if (*pbVar10 M) { pbVar10 (byte *)((ulong)local_78 | 3); if ((local_78[0] 1) ! 0) { pbVar10 local_68 2; } if (*pbVar10 5) { pbVar10 (byte *)((ulong)local_78 | 4); if ((local_78[0] 1) ! 0) { pbVar10 local_68 3; } if (*pbVar10 Q) { pbVar10 (byte *)((ulong)local_78 | 5); if ((local_78[0] 1) ! 0) { pbVar10 local_68 4; } if (*pbVar10 D) { pbVar10 (byte *)((ulong)local_78 | 6); if ((local_78[0] 1) ! 0) { pbVar10 local_68 5; } if (*pbVar10 2) { pbVar9 (byte *)((ulong)local_78 | 7); pbVar10 pbVar9; if ((local_78[0] 1) ! 0) { pbVar10 local_68 6; } if (*pbVar10 A) { pbVar10 (byte *)local_70; if ((local_78[0] 1) ! 0) { pbVar10 local_68 7; } if (*pbVar10 7) { pbVar10 (byte *)((long)local_70 1); if ((local_78[0] 1) ! 0) { pbVar10 local_68 8; } if (*pbVar10 K) { pbVar10 (byte *)((long)local_70 2); if ((local_78[0] 1) ! 0) { pbVar10 local_68 9; } if (*pbVar10 L) { pbVar10 (byte *)((long)local_70 3); if ((local_78[0] 1) ! 0) { pbVar10 local_68 10; } if (*pbVar10 T) { pbVar10 (byte *)((long)local_70 4); if ((local_78[0] 1) ! 0) { pbVar10 local_68 0xb; } if (*pbVar10 X) {逐字符校验第二个字符串→str29M5QD2A7KLTX校验通过后开始拼返回值push_back就是拼返回值std::__ndk1::basic_string::reserve ((basic_string *)local_90,8);申请 8 字节空间准备返回 8 位字符串local_90。pbVar10 (byte *)((ulong)local_78 | 1); if ((local_78[0] 1) ! 0) { pbVar10 local_68; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pbVar10 ^ 0x77);拼第 1 个字符str2 [0] ^ 0x77local_90→ 9 ^ 0x77 Nif ((local_60[0] 1) ! 0) { pcVar8 local_50 1; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pcVar8);拼第 2 个字符str1 [1]→4pbVar10 (byte *)((long)local_70 3); if ((local_78[0] 1) ! 0) { pbVar10 local_68 10; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pbVar10);拼第 3 个字符str2 [10]→Tif ((local_60[0] 1) ! 0) { pcVar6 local_50 2; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pcVar6); //拼第 4 个字符str1 [2] if ((local_78[0] 1) ! 0) { pbVar9 local_68 6; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pbVar9 ^ 0x17); //拼第 5 个字符str2[6] ^ 0x17 if ((local_60[0] 1) ! 0) { pcVar7 local_50 3; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pcVar7); //拼第 6 个字符str1[3] pbVar10 (byte *)((long)local_70 1); if ((local_78[0] 1) ! 0) { pbVar10 local_68 8; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pbVar10); //拼第 7 个字符str2[8] if ((local_60[0] 1) ! 0) { pcVar11 local_50; } std::__ndk1::basic_string::push_back ((basic_string *)local_90,*pcVar11 \x02); //拼第 8 个字符str1[0] 2所以返回的字符串是→ N4T1V3K9最后校验失败返回空成功返回 N4T1V3K9总结str17413str29M5QD2A7KLTX返回值local_90→ N4T1V3K9三、阶段 3Java 层登录逻辑由已知入参反推用户名和 License先从jadx左侧打开AndroidManifest.xml 找启动 Activity搜maincom.hnctf.patrolnote.LoginActivityLoginActivity就是 App 启动后第一个打开的 Activity设置-显示不一致代码勾选上jadx打开com.hnctf.patrolnote.LoginActivity搜索第一个C0.ViewOnClickListenerC0000a发现它只是读取 assets/offline.noticeToast 显示提示另一个F0.c这里是真正的登录按钮这里有两个按钮buttonLogin 登录按钮buttonImport 导入/下载/附件按钮inputUsername); // 用户名输入框 inputLicense); // 授权码输入框 buttonLogin); // 登录按钮 id.textError); // 错误提示文字onClick登录按钮点击事件这里面1. 获取你输入的内容String string editText.getText().toString(); // 用户名 String string2 editText2.getText().toString(); // 授权码2. 用户名正则校验S0.c cVar2 b.f198a; // (1) 从 F0.b 拿正则 Matcher matcher cVar2.f431a.matcher(upperCase); // (2) 用正则匹配用户名进入F0.bb.f198a 正则 ^EMP-2026-(\d{4})$正则知识点 ^ 字符串开头、 $ 字符串结尾、 \d 数字、括号捕获组。再结合F0.cString str (String) bVar2.get(1); // 3 正则提取出 str7413校验用户名必须是EMP-2026-四位数字所以登录输入的用户名是EMP-2026-str即EMP-2026-7413输入之后传入3. 授权码正则校验F0.c中S0.c cVar4 b.b; // 必须格式XXXX-XXXX-XXXX if (cVar4.f431a.matcher(upperCase2).matches())对应F0.b中public static final S0.c b new S0.c(^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$);所以授权码必须是4位-4位-4位格式4. 授权码去横杠 字符换位String strC0 j.C0(upperCase2, -, ); // 去掉 - for (int i3 : b.f199c) { sb.append(strC0.charAt(i3)); }F0.b置换数组idx [5,2,9,0,11,3,7,1,10,4,8,6]正向规则新串 res [i] raw [ idx [i] ]→ 处理后得到str29M5QD2A7KLTX5. 调用 SO所以是用户输入用户名和授权码先检查用户名和授权码的格式再截取用户名最后的4位内容传给so文件里的str授权码去横杠- 字符换位后传给so文件。so文件校验str 7413string4 9M5QD2A7KLTX是否正确正确的返回strNativeVerifyN4T1V3K9可以求得应该输入的正确的授权码idx [i] 是 raw 下标res [i] 是这个下标对应的字符i0 → idx[0]5 → raw[5]res[0]9i1 → idx[1]2 → raw[2]res[1]Mi2 → idx[2]9 → raw[9]res[2]5i3 → idx[3]0 → raw[0]res[3]Qi4 → idx[4]11 → raw[11]res[4]Di5 → idx[5]3 → raw[3]res[5]2i6 → idx[6]7 → raw[7]res[6]Ai7 → idx[7]1 → raw[1]res[7]7i8 → idx[8]10 → raw[10]res[8]Ki9 → idx[9]4 → raw[4]res[9]Li10→ idx[10]8 → raw[8]res[10]Ti11→ idx[11]6 → raw[6]res[11]Xraw 完整Q7M2L9XAT5KD按 4-4-4 加分隔Q7M2-L9XA-T5KD所以输入应该为用户名EMP-2026-7413 授权码Q7M2-L9XA-T5KD四、阶段 4AES 解密加密文件 report.enc 获取 Flag还是在F0.c中6. 构造解密密钥材料String strConcat EMP2026.concat(str); // EMP20267413 int[] iArr {7,8,9,10,0,3,6,2};按下标截取得→7413E26PaVar new a(string5, strNativeVerify);双击a进入F0a分析可知a就是用拼接两个字符串拼接得aVar7413E26P:N4T1V3K97. 解密 report.encF0.c中try { bVar h.E(loginActivity, aVar.f197c); }aVar.f197c7413E26P:N4T1V3K9双击h.E发现进入了A.h.EE () 函数就是用7413E26P:N4T1V3K9生成密钥来解密report.enc得到 Flag 的完整代码解密import base64 import hashlib from Crypto.Cipher import AES ct base64.b64decode(open(report.enc, rb).read()) auth b7413E26P:N4T1V3K9 key hashlib.sha256(auth).digest()[:16] iv hashlib.md5(bPatrolNote-IV).digest() #创建 AES-CBC 解密器解密 pt AES.new(key, AES.MODE_CBC, iv).decrypt(ct) pad pt[-1] AES-CBC 解出来后末尾有 PKCS5/PKCS7 padding。 最后一个字节表示 padding 长度。 比如最后是 0x05说明最后 5 个字节都是填充。 print(pt[:-pad].decode())把解密脚本与report.enc放在同一个文件夹HNCTF{dd8a63b2bf2ac5d46070553f27f3b09a}

相关新闻

CubeSandbox:面向 AI Agent 的即时、并发、安全轻量沙箱服务

CubeSandbox:面向 AI Agent 的即时、并发、安全轻量沙箱服务

CubeSandbox:面向 AI Agent 的即时、并发、安全轻量沙箱服务 📌 核心观点 CubeSandbox 是腾讯云开源的高性能沙箱服务,专为 AI Agent 代码执行场景设计。它基于 RustVMM KVM 构建,在安全性(硬件级隔离)与…

2026/7/9 8:05:18阅读更多 →
创业自动化平台怎么选

创业自动化平台怎么选

一、一人创业,最容易卡在任务切换与推进节奏 一人创业或独立开发,最大的痛点不是缺想法,而是缺人手、缺时间、缺连贯的推进节奏。从产品想法梳理、原型开发、客户沟通,到内容产出、数据复盘,所有环节都要自己扛。频繁…

2026/7/9 8:05:18阅读更多 →
V93000 WSMX板卡实战:16单元混合信号测试配置与4类测试头选型指南

V93000 WSMX板卡实战:16单元混合信号测试配置与4类测试头选型指南

V93000 WSMX板卡实战:16单元混合信号测试配置与4类测试头选型指南在半导体测试领域,V93000测试平台凭借其卓越的灵活性和扩展性,已成为众多芯片设计公司和测试厂商的首选。作为该平台的核心组件之一,Wave Scale MX(WSM…

2026/7/9 8:05:18阅读更多 →
MyComputerManager终极指南:如何彻底清理Windows“此电脑“中的顽固图标

MyComputerManager终极指南:如何彻底清理Windows“此电脑“中的顽固图标

MyComputerManager终极指南:如何彻底清理Windows"此电脑"中的顽固图标 【免费下载链接】MyComputerManager 管理“此电脑”里删不掉的流氓“快捷方式”(包括侧边栏),同时可自己添加这类“快捷方式” 项目地址: https:…

2026/7/9 9:20:31阅读更多 →
pandas 内存优化:read_csv 的 dtype 参数能省一半内存

pandas 内存优化:read_csv 的 dtype 参数能省一半内存

pandas 内存优化:read_csv 的 dtype 参数能省一半内存公司那台 16G 内存的开发机,被一个 800M 的 CSV 文件干到 OOM。排查了一圈才发现,pandas 默认推断类型这件事,偷偷多吃了一倍的内存。今天用实际案例聊聊 dtype 参数怎么给数据…

2026/7/9 9:20:31阅读更多 →
ESLint 自定义规则实战:用 AST 分析封堵团队特有的代码坏味道

ESLint 自定义规则实战:用 AST 分析封堵团队特有的代码坏味道

ESLint 自定义规则实战:用 AST 分析封堵团队特有的代码坏味道 一、通用 Lint 规则的盲区:为什么团队特有的"坏习惯"永远被漏掉 ESLint 有数百条内置规则和社区插件。no-unused-vars、no-console、prefer-const 这些通用规则覆盖了 80% 的常见…

2026/7/9 9:20:29阅读更多 →
ARIMA模型实战:Python statsmodels 0.14 电商销量预测,MAPE误差降至8%

ARIMA模型实战:Python statsmodels 0.14 电商销量预测,MAPE误差降至8%

ARIMA模型实战:Python statsmodels 0.14 电商销量预测,MAPE误差降至8% 1. 电商销量预测的业务挑战与ARIMA解决方案 在电商运营中,精准的销量预测是库存管理、营销策略和供应链优化的核心。传统方法如移动平均或简单指数平滑往往难以捕捉销售…

2026/7/9 9:20:29阅读更多 →
web应用课程设计最终项目展示

web应用课程设计最终项目展示

云边书屋|基于Spring BootVue3的前后端分离图书管理系统一、项目简介 云边书屋是一款Spring Boot Vue3开发的前后端分离在线书库管理系统,项目寓意“于云端书海之畔,打造极简治愈的线上阅读空间”。系统兼具用户阅读体验与后台管理能力&…

2026/7/9 9:20:28阅读更多 →
京东app 滑块验证码逆向分析

京东app 滑块验证码逆向分析

声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!有相关问题请第一时间点击头像看简介或私…

2026/7/9 9:15:27阅读更多 →
从GitHub安全案例解析常见漏洞与防护实践

从GitHub安全案例解析常见漏洞与防护实践

1. 项目概述:从GitHub Trending看安全实战 最近在GitHub Trending上看到一个项目,叫 skills4/skills ,它因为一些安全漏洞案例被大家讨论。这其实是一个挺典型的场景:一个旨在展示或教授某种技能的仓库,本身却成了安…

2026/7/9 5:56:19阅读更多 →
MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

# MLT 2026启示:因果推理与概率建模驱动下一代LLM应用## 一、背景与挑战:从“黑箱预测”到“可信推理”2026年6月,第7届机器学习与趋势国际会议(MLT 2026)将在悉尼召开。会议议程中,“因果与可解释机器学习…

2026/7/8 7:00:12阅读更多 →
通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

1. 项目概述与漏洞背景最近在梳理一些历史OA系统的安全风险时,通达OA v11.6版本中的一个老漏洞又进入了我的视线。这个漏洞位于/general/bi_design/appcenter/report_bi.func.php文件中,是一个典型的SQL注入点。虽然这个漏洞的利用方式看起来并不复杂&am…

2026/7/9 2:47:07阅读更多 →
Three.js 着色器光效教程

Three.js 着色器光效教程

着色器光效 Shader Light ▶ 在线运行案例 案例合集: 三维可视化功能案例(threehub.cn)开源仓库github地址: https://github.com/z2586300277/three-cesium-examples400个案例代码: 网盘链接 你将学到什么 ShaderMaterial 自…

2026/7/9 0:04:37阅读更多 →
如何5分钟掌握CS2智能库存管理:开源工具CASEMOVE终极指南

如何5分钟掌握CS2智能库存管理:开源工具CASEMOVE终极指南

如何5分钟掌握CS2智能库存管理:开源工具CASEMOVE终极指南 【免费下载链接】casemove A dedicated desktop app that enables you to move items in and out of storage units in CS2. 项目地址: https://gitcode.com/gh_mirrors/ca/casemove 还在为CS2存储单…

2026/7/9 0:04:37阅读更多 →
GBase 8a vs MySQL 8.0:ALTER TABLE语法与限制的5点关键差异对比

GBase 8a vs MySQL 8.0:ALTER TABLE语法与限制的5点关键差异对比

GBase 8a与MySQL 8.0:ALTER TABLE语法差异深度解析与实战指南1. 两种数据库的ALTER TABLE能力全景对比在数据库架构设计和运维过程中,表结构变更(DDL操作)是不可避免的需求。GBase 8a作为国产分析型数据库代表,与开源M…

2026/7/9 0:04:37阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/8 6:59:54阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/8 13:42:39阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/8 13:42:39阅读更多 →