news 2026/2/8 23:22:44

攻防世界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/2/3 8:37:26

挖洞经验之一个绕过某SRC厂商三处XSS过滤的payload

前言 在某次src的漏洞挖掘过程中&#xff0c;发现了一个payload绕过了三处xss过滤&#xff0c;个人觉得还是挺有意思的&#xff0c;所以记录一下。 从一个被忽略的self xss说起 在某页面信息如下&#xff0c;我决定对回复内容进行xss测试&#xff1a;插入一个<img/src1>以…

作者头像 李华
网站建设 2026/2/8 13:10:00

从文本中精准提取关键信息|RaNER模型实战应用

从文本中精准提取关键信息&#xff5c;RaNER模型实战应用 在当今信息爆炸的时代&#xff0c;非结构化文本数据呈指数级增长。新闻报道、社交媒体内容、企业文档等海量文本中蕴藏着大量有价值的信息&#xff0c;但如何高效地从中提取出关键实体&#xff08;如人名、地名、机构名…

作者头像 李华
网站建设 2026/2/3 13:55:38

MiDaS模型实战案例:宠物照片深度估计

MiDaS模型实战案例&#xff1a;宠物照片深度估计 1. 引言&#xff1a;AI 单目深度估计的现实价值 在计算机视觉领域&#xff0c;从单张2D图像中恢复3D空间结构一直是极具挑战性的任务。传统方法依赖多视角几何或激光雷达等硬件设备&#xff0c;成本高且部署复杂。近年来&…

作者头像 李华
网站建设 2026/2/8 0:56:32

MiDaS模型创新:实时深度估计系统搭建

MiDaS模型创新&#xff1a;实时深度估计系统搭建 1. 引言&#xff1a;AI 单目深度估计的现实意义 在计算机视觉领域&#xff0c;从单张2D图像中恢复3D空间结构一直是极具挑战性的任务。传统方法依赖双目立体匹配或多视角几何约束&#xff0c;但这些方案对硬件要求高、部署复杂…

作者头像 李华
网站建设 2026/2/4 11:28:33

图像处理新维度:MiDaS深度估计快速入门

图像处理新维度&#xff1a;MiDaS深度估计快速入门 1. 引言&#xff1a;AI 单目深度估计的现实意义 在计算机视觉领域&#xff0c;从二维图像中理解三维空间结构一直是核心挑战之一。传统方法依赖双目立体视觉或多传感器融合&#xff08;如激光雷达&#xff09;&#xff0c;但…

作者头像 李华
网站建设 2026/2/6 14:21:13

MiDaS深度估计教程:提升热力图效果

MiDaS深度估计教程&#xff1a;提升热力图效果 1. 引言&#xff1a;AI 单目深度估计的现实价值 在计算机视觉领域&#xff0c;从单张2D图像中恢复3D空间结构一直是极具挑战性的任务。传统方法依赖多视角几何或激光雷达等硬件设备&#xff0c;成本高且部署复杂。近年来&#x…

作者头像 李华