news 2026/4/15 17:37:17

攻防世界Mobile5 EasyJNI 安卓逆向CTF

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
攻防世界Mobile5 EasyJNI 安卓逆向CTF

EasyJNI

最近正好在出写JNI,正好看到了一道JNI相关的较为简单明了的CTF,就一时兴起的写了,不得不说逆向工程和正向开发确实是可以互补互相加深的

JNI

JNI(Java Native Interface)即java本地接口,众所周知,android有四层结构(也有说五层结构,即多了一个抽象层,这里不予讨论),应用层与应用接口层是用Java写的,而C/C++核心库和linux内核层由C/C++写的,既然知道了这一点,那理解JNI就很简单了,Java和C/C++肯定是不能直接互相调用的,那么应用层肯定就不能直接调用底层的东西,比如从应用层直接用Java想调用底层C/C++开发的启动相机或NFC等肯定是不能直接实现的。

这就是JNI的作用了,它充当桥梁,向C/C++转译Java中的方法,向Java转译C/C++的函数,而NDK就是JNI开发工具。

开始解题

首先不考虑那么多的先AndroidKiller试试

既然都AK(AndroidKiller)了就索性看看JADE

实不相瞒的说在看到这两个东西之前我还想过会不会就是名字放着看的,不是调用的JNI,既然看到这里了就知道了确确实实调用了JNI,那就一步一步的分析吧,首先看OnCreate()

就这一句话是有用的,别的没啥用,这段代码中设置了一个点击事件,即创立了一个Button,并通过ID找到Button,然后设置SetOnClickListener设置监听,点击之后调用上图中的a()方法,传入的String是输入的String,通过a()方法返回一个布尔值

那就回去看a方法就好了,先放个整体图出来吧

这样就能看到这题恶心人的地方了,他的MainActivity中有a方法,但是还有一个a类,而且在a类中,还有一个静态的Byte[]也叫a,但没啥影响,降维理解一下这里吧:

初始化一个a类locala,并把输入进去的String类型的字符串穿换成Byte[]类型组传入a类locala的a方法。看着可能绕,自己做的就还行了。

那就看a类吧

不知道是不是我jade版本的问题,看这个a数组里边的东西,明明是char型的东西,没有自动转换成字符,这就很烦了,于是我换成了JEB

1 package com.a.easyjni; 2 3 public class a { 4 private static final char[] a; 5 6 static { 7 a.a = new char[]{'i', '5', 'j', 'L', 'W', '7', 'S', '0', 'G', 'X', '6', 'u', 'f', '1', 'c', 'v', '3', 'n', 'y', '4', 'q', '8', 'e', 's', '2', 'Q', '+', 'b', 'd', 'k', 'Y', 'g', 'K', 'O', 'I', 'T', '/', 't', 'A', 'x', 'U', 'r', 'F', 'l', 'V', 'P', 'z', 'h', 'm', 'o', 'w', '9', 'B', 'H', 'C', 'M', 'D', 'p', 'E', 'a', 'J', 'R', 'Z', 'N'}; 8 } 9 10 public a() { 11 super(); 12 } 13 14 public String a(byte[] arg10) { 15 int v8 = 3; 16 StringBuilder v4 = new StringBuilder(); 17 int v0; 18 for(v0 = 0; v0 <= arg10.length - 1; v0 += 3) { 19 byte[] v5 = new byte[4]; 20 int v3 = 0; 21 byte v2 = 0; 22 while(v3 <= 2) { 23 if(v0 + v3 <= arg10.length - 1) { 24 v5[v3] = ((byte)(v2 | (arg10[v0 + v3] & 0xFF) >>> v3 * 2 + 2)); 25 v2 = ((byte)(((arg10[v0 + v3] & 0xFF) << (2 - v3) * 2 + 2 & 0xFF) >>> 2)); 26 } 27 else { 28 v5[v3] = v2; 29 v2 = 0x40; 30 } 31 32 ++v3; 33 } 34 35 v5[v8] = v2; 36 int v2_1; 37 for(v2_1 = 0; v2_1 <= v8; ++v2_1) { 38 if(v5[v2_1] <= 0x3F) { 39 v4.append(a.a[v5[v2_1]]); 40 } 41 else { 42 v4.append('='); 43 } 44 } 45 } 46 47 return v4.toString(); 48 } 49 }

实不相瞒的说,要不是我去看别人的WP我都没有意识到这是变种的Base64加密,不过这个a作为密码表还是很明显的,那再重新理一下逻辑,输入了一个字符串Str,这个字符串在a.a中被加密成立Str1,Str1被传到了布尔型的native层的ncheck中经过检测, 并由返回的布尔值判断输出。

那就简单了,看ncheck就好了,在ncheck中有判断需要逆向回去的字符串

将apk解压

so库就在lib下了

IDA大法好

Ctrl+F搜索ncheck就得到了想要的函数,分析一下这个函数吧,在v6之前说的都是废话,不用管

1 v6 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0); 2 if ( strlen(v6) == 32 ) 3 { 4 v7 = 0; 5 do 6 { 7 v8 = &s1[v7]; 8 s1[v7] = v6[v7 + 16]; 9 v9 = v6[v7++]; 10 v8[16] = v9; 11 } 12 while ( v7 != 16 ); 13 (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v5, v6); 14 v10 = 0; 15 do 16 { 17 v12 = __OFSUB__(v10, 30); 18 v11 = v10 - 30 < 0; 19 v16 = s1[v10]; 20 s1[v10] = s1[v10 + 1]; 21 s1[v10 + 1] = v16; 22 v10 += 2; 23 } 24 while ( v11 ^ v12 ); 25 v13 = memcmp(s1, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u); 26 result = 0; 27 if ( !v13 ) 28 result = 1; 29 } 30 else 31 { 32 (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v5, v6); 33 result = 0; 34 } 35 return result;

不管别的,至少我第一眼看到的就是“MbT3sQgX039i3g==AQOoMQFPskB1Bsc7”,在这我大概就知道是Base64了,这个两个等号太明显了,而且前面的密码表很明显跟base64不一样,那就确定是base64的变种了。在这个函数中也有加密,先看一下这个加密吧,两个do…while,先看两个while,一个是v7由0变到16,一个是计算一个异或值。

先看第一个do…while,就是将前16位与后16位交换位置,没啥好多说的

看看第二个do…while,V12是一个判断溢出,OFSUB(int a,int b)经过查询之后的作用是判断a-b是否会产生溢出,即a+(-b)是否溢出,-30补码为11100010,如果要产生溢出,那最小的补码为00100000,十进制就是16,跟前面那个就有点意思了,也是16。并且在0到16期间也不可能减去30大于等于0,即V11永远为0。那么V11^V12的第一个跳出条件就是,当V10为16的时候。也能看出来作用就是两两交换位置。

“v13 = memcmp(s1, “MbT3sQgX039i3g==AQOoMQFPskB1Bsc7”, 0x20u)”这里,memcmp(String str1,String str2,int n)作用判断为判断str1与str2的前n位,而0x20就是32。

从吾爱白嫖来的解密脚本:

#白嫖来的base64解密函数 base64_charset = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN" def decode(base64_str): base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if s != '='] resp = bytearray() nums = len(base64_bytes) // 4 remain = len(base64_bytes) % 4 integral_part = base64_bytes[0:4 * nums] while integral_part: tmp_unit = ''.join(integral_part[0:4]) tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]] for i in tmp_unit: resp.append(i) integral_part = integral_part[4:] if remain: remain_part = ''.join(base64_bytes[nums * 4:]) tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)] for i in tmp_unit: resp.append(i) return resp #两两交换位置,还原so内的加密 flag = "AQOoMQFPskB1Bsc7MbT3sQgX039i3g==" tmp="" for i in range(len(flag)//2): tmp += flag[i*2+1] + flag[i*2] print(tmp) print(decode(tmp))
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/10 19:45:42

智能体核心技术落地|AI 智能实体侦测服务详解

智能体核心技术落地&#xff5c;AI 智能实体侦测服务详解 在智能体技术的演进中&#xff0c;自然语言理解&#xff08;NLU&#xff09;能力是其感知与交互的核心支柱。而命名实体识别&#xff08;Named Entity Recognition, NER&#xff09;作为NLU的关键子任务&#xff0c;承…

作者头像 李华
网站建设 2026/3/25 6:17:33

数据采集必备的5个低代码爬虫软件

爬虫&#xff0c;又称为网络爬虫或网页爬虫&#xff0c;是一种自动浏览互联网的程序&#xff0c;它按照一定的算法顺序访问网页&#xff0c;并从中提取有用信息。爬虫软件通常由以下几部分组成&#xff1a; - 用户代理&#xff08;User-Agent&#xff09;&#xff1a;模拟浏览…

作者头像 李华
网站建设 2026/3/27 15:30:07

AI智能实体侦测服务核心优势解析|附RaNER模型实战案例

AI智能实体侦测服务核心优势解析&#xff5c;附RaNER模型实战案例 在信息爆炸的时代&#xff0c;非结构化文本数据&#xff08;如新闻、社交媒体内容、用户评论&#xff09;呈指数级增长。如何从这些海量文本中快速提取关键信息&#xff0c;成为企业智能化转型的核心需求之一。…

作者头像 李华
网站建设 2026/4/15 10:42:44

中文实体识别新利器|AI智能实体侦测服务镜像上线

中文实体识别新利器&#xff5c;AI智能实体侦测服务镜像上线 1. 背景与需求&#xff1a;中文信息抽取的现实挑战 在当今大数据时代&#xff0c;非结构化文本数据占据了信息总量的80%以上。新闻报道、社交媒体、政府公文、企业文档中蕴含着大量关键信息——人名、地名、机构名…

作者头像 李华
网站建设 2026/4/9 1:06:49

吐血推荐专科生必用10款AI论文工具深度测评

吐血推荐专科生必用10款AI论文工具深度测评 2026年专科生论文写作工具测评&#xff1a;为何值得一看 随着人工智能技术的不断进步&#xff0c;AI论文工具逐渐成为高校学生&#xff0c;尤其是专科生群体的重要辅助工具。然而&#xff0c;面对市场上琳琅满目的产品&#xff0c;如…

作者头像 李华
网站建设 2026/4/8 17:13:57

HarmonyOS 选择器禁用拍照功能

HarmonyOS 选择器禁用拍照功能 在使用 HarmonyOS 的图片选择器时&#xff0c;有时我们需要限制用户的操作选项&#xff0c;例如禁用拍照功能&#xff0c;只允许用户从相册中选择照片。本文介绍了如何实现这一功能。 默认行为 默认情况下&#xff0c;当我们使用 select 方法获取…

作者头像 李华