初识
附件为一个apk,先拖到模拟器看看,结果报错:
百度一下错误信息,原因是:安装的APP中使用了与当前CPU架构不一致的native libraries。
我的CPU是Intel的,为了模拟器运行速度快,我创建的模拟器使用的是x86指令集:
而一般手机都是ARM指令集的,所以我重新创建了一个ARM指令集的模拟器:
在这个新模拟器下,apk就能成功安装了。
apk的主界面如下:
随便输入一个字符串,点击CHECK,结果为:
直接运行程序,我们能获得的信息就这么多,下面我们还是反编译看看。
反编译
将apk重命名为zip并解压。
使用dex2jar对解压得到的dex文件进行反编译:
使用jd-gui打开反编译得到的jar。
先来到MainActibity类,在构造函数onCreate中我们可以看到注册按下按钮的响应函数的代码:
findViewById(2131427446).setOnClickListener(new View.OnClickListener(this, (Context)this)
{
public void onClick(View param1View)
{
EditText editText = (EditText)((MainActivity)this.a).findViewById(2131427445);
if (MainActivity.a(this.b, editText.getText().toString()))
{
Toast.makeText(this.a, "You are right!", 1).show();
return;
}
Toast.makeText(this.a, "You are wrong! Bye~", 1).show();
}
}
按钮响应函数核心就是一个“if”判断,两个分支分别弹框“You are right!”和“You are wrong! Bye~”。我们在上面输入6个1时,点击“CHECK”,就弹框显示了“You are wrong! Bye~”。
所以正确的输入应该让
MainActivity.a(this.b, editText.getText().toString())
函数返回1。
MainActivity.a函数代码很简单:
private boolean a(String paramString)
{
try
{
return ncheck((new a()).a(paramString.getBytes()));
}catch (Exception exception)
{
return false;
}
}
就是返回ncheck((new a()).a(paramString.getBytes()));函数的返回值。
MainActivity类中ncheck的声明为:
private native boolean ncheck(String paramString);
可以看出这是个native函数,也和这题的题目(easyjni)吻合,调用native函数用的就是jni技术。
但这里调用ncheck函数的参数不是我们输入的字符串,而是类a的a函数的返回值。我们输入的字符串是类a的a函数的参数。
我们下面分别看些类a的a函数和ncheck函数。
ncheck
在zip的解压目录下的lib目录下我们能看到库文件:libnative.so。ncheck函数就在这个文件中。
我们用IDA加载这个文件,在左侧的函数列表中搜索ncheck,找到该函数:
bool __fastcall Java_com_a_easyjni_MainActivity_ncheck(int a1, int a2, int a3)
{
const char *v5; // r6
int i; // r0
char *v7; // r2
char v8; // r1
int v9; // r0
bool v10; // cc
_BOOL4 result; // r0
char v12[32]; // [sp+3h] [bp-35h] BYREF
char v13; // [sp+23h] [bp-15h]
v5 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
if ( strlen(v5) == 32 )
{
for ( i = 0; i != 16; ++i ) // 前16个字符和后16个字符颠倒顺序
{
v7 = &v12[i];
v12[i] = v5[i + 16];
v8 = v5[i];
v7[16] = v8;
}
(*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)a1 + 680))(a1, a3, v5);
v9 = 0;
do
{
v10 = v9 < 30;
v13 = v12[v9];
v12[v9] = v12[v9 + 1];
v12[v9 + 1] = v13;
v9 += 2;
}
while ( v10 ); // 两个字符为一组,颠倒顺序
result = memcmp(v12, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u) == 0;
}
else
{
(*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)a1 + 680))(a1, a3, v5);
result = 0;
}
return result;
}
这里的v5就是传进来的一个字节串,在strlen(v5) == 32这条语句中我们可以知道,这个输入的字节串长度为32。
该函数最核心的就是里面的两个循环:一个for,一个do-while。
for循环的功能是将前16个字节和后16个字节颠倒顺序,也就是:
AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB
变为:
BBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAA
do-while循环将32个字节每两个为一组,前后颠倒顺序,也就是:
ABABABABABABABABABABABABABABABAB
变为:
BABABABABABABABABABABABABABABABA
最后比较变形后的字节串是否为:"MbT3sQgX039i3g==AQOoMQFPskB1Bsc7"。
这里我就懒得写程序了,因为变形很简单。我们直接以结果字符串"MbT3sQgX039i3g==AQOoMQFPskB1Bsc7"倒退正确的输入。
首先是do-while循环,要得到输出"MbT3sQgX039i3g==AQOoMQFPskB1Bsc7",输入应该为“bM3TQsXg30i9g3==QAoOQMPFks1BsB7c”。
其次是for循环,要得到输出“bM3TQsXg30i9g3==QAoOQMPFks1BsB7c”,输入应该为“QAoOQMPFks1BsB7cbM3TQsXg30i9g3==”。
也就是ncheck函数的输入应该为“QAoOQMPFks1BsB7cbM3TQsXg30i9g3==”。
ncheck的参数来自类a的a函数的输出,我们接下来看看类a的a函数。
a:a()
类a在jar中,代码为:
public class a {
private static final char[] 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' };
public String a(byte[] paramArrayOfbyte) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i <= paramArrayOfbyte.length - 1; i += 3) {
byte[] arrayOfByte = new byte[4];
int j = 0;
byte b = 0;
while (j <= 2) {
if (i + j <= paramArrayOfbyte.length - 1) {
arrayOfByte[j] = (byte)(b | (paramArrayOfbyte[i + j] & 0xFF) >>> j * 2 + 2);
b = (byte)(((paramArrayOfbyte[i + j] & 0xFF) << (2 - j) * 2 + 2 & 0xFF) >>> 2);
} else {
arrayOfByte[j] = b;
b = 64;
}
j++;
}
arrayOfByte[3] = b;
for (j = 0; j <= 3; j++) {
if (arrayOfByte[j] <= 63) {
stringBuilder.append(a[arrayOfByte[j]]);
} else {
stringBuilder.append('=');
}
}
}
return stringBuilder.toString();
}
}
这和我们之前遇到过的题目XCTF_MOBILE5_easy-apk类似,就是一个修改了替换表的BASE64。我们还是使用之前文章中的BASE64解码程序,将其中的替换表改为本题目的:
#include <stdio.h>
#include <windows.h>
const char BASE_CODE[] = {
'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' };
//子函数 - 取密文的索引
inline char GetCharIndex(char c) //内联函数可以省去函数调用过程,提速
{
for (int i = 0; i < strlen(BASE_CODE); i++)
{
if (BASE_CODE[i] == c)
{
return i;
}
}
return 0;
}
//解码,参数:结果,密文,密文长度
int fnBase64Decode(char *lpString, char *lpSrc, int sLen) //解码函数
{
static char lpCode[4];
register int vLen = 0;
if (sLen % 4) //Base64编码长度必定是4的倍数,包括'='
{
lpString[0] = '\0';
return -1;
}
while (sLen > 2) //不足三个字符,忽略
{
lpCode[0] = GetCharIndex(lpSrc[0]);
lpCode[1] = GetCharIndex(lpSrc[1]);
lpCode[2] = GetCharIndex(lpSrc[2]);
lpCode[3] = GetCharIndex(lpSrc[3]);
*lpString++ = (lpCode[0] << 2) | (lpCode[1] >> 4);
*lpString++ = (lpCode[1] << 4) | (lpCode[2] >> 2);
*lpString++ = (lpCode[2] << 6) | (lpCode[3]);
lpSrc += 4;
sLen -= 4;
vLen += 3;
}
return vLen;
}
int main()
{
char es[] = "QAoOQMPFks1BsB7cbM3TQsXg30i9g3==";
char ds[1000] = { 0x00 };
fnBase64Decode(ds, es, strlen(es));
}
最终就得到结果了~~~
———————————————————————————————————————————
欢迎关注我的微博:大雄_RE。专注软件逆向,分享最新的好文章、好工具,追踪行业大佬的研究成果。