题目

CSDN免积分下载

查壳

先查壳(PKiD等),有壳脱壳,没壳用AndroidKiller等反编译工具打开查看JAVA代码
无壳APK

Java Decompiler - MainActivity-onCreate

image.png

答案

flag{just_ANot#er_@p3}

Writeup

解题

运行

image.png

解题思路

JNI

Java调用C/C++在Java语言里面本来就有的,并非Android自创的,即JNI。JNI就是Java调用C++的规范。
JNI,全称为Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。
通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。
由于JNI是JVM规范的一部分,因此可以将我们写的JNI的程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码JNI是一种在Java虚拟机机制下的执行代码的标准机制。代码被编写成汇编程序或者C/C++程序,并组装为动态库。也就允许非静态绑定用法。这提供了一个在Java平台上调用C/C++的一种途径,反之亦然。

搜索字符串

使用反编译工具的字符串搜索功能,搜索运行后失败的提示字符串“You are wrong”:
image.png
在Java Decompiler中,找到该class文件:
image.png

MainActivity

搜不出来或者没有思路时,应该先看Android程序的入口——也就是MainActivity函数。

代码逻辑

MainActivity

主要代码为一个if判断,条件中调用了MainActivity.a处理输入的数据:

  1. if (MainActivity.a(MainActivity.this, paramAnonymousView.getText().toString()))
  2. {
  3. Toast.makeText(jdField_this, "You are right!", 1).show();
  4. }

a函数

查看MainActivity的a函数,有效代码为:

try
{
    a locala = new com/a/easyjni/a;
    locala.<init>();
    bool = ncheck(locala.a(paramString.getBytes()));
    return bool;
}

在代码中调用了2️⃣ncheck处理了1️⃣“com/a/easyjni/a”类的返回值:
image.png

1️⃣a类

根据64个值的码表,特殊值“3”,“63 / 64”,运算操作“& 0xFF ) >>> ( 2+2j )”,还有以前多个Base64换表的题,很容易得出结论这是一个Base64编码的函数,只不过和标准的Base64编码不同的是,使用的码表char[] a的数值不是标准的码表“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”:

public class a
{
    private static final char[] a = { 105, 53, 106, 76, 87, 55, 83, 48, 71, 88, 54, 117, 102, 49, 99, 118, 51, 110, 121, 52, 113, 56, 101, 115, 50, 81, 43, 98, 100, 107, 89, 103, 75, 79, 73, 84, 47, 116, 65, 120, 85, 114, 70, 108, 86, 80, 122, 104, 109, 111, 119, 57, 66, 72, 67, 77, 68, 112, 69, 97, 74, 82, 90, 78 };

    public String a(byte[] paramArrayOfByte)
    {
        StringBuilder localStringBuilder = new StringBuilder();
        for (int i = 0; i <= paramArrayOfByte.length - 1; i += 3)
        {
            byte[] arrayOfByte = new byte[4];
            int j = 0;
            int k = 0;
            if (j <= 2)
            {
                if (i + j <= paramArrayOfByte.length - 1)
                {
                    arrayOfByte[j] = ((byte)(byte)(k | (paramArrayOfByte[(i + j)] & 0xFF) >>> j * 2 + 2));
                }
                for (k = (byte)(((paramArrayOfByte[(i + j)] & 0xFF) << (2 - j) * 2 + 2 & 0xFF) >>> 2);; k = 64)
                {
                    j++;
                    break;
                    arrayOfByte[j] = ((byte)k);
                }
            }
            arrayOfByte[3] = ((byte)k);
            k = 0;
            if (k <= 3)
            {
                if (arrayOfByte[k] <= 63)
                {
                    localStringBuilder.append(a[arrayOfByte[k]]);
                }
                for (;;)
                {
                    k++;
                    break;
                    localStringBuilder.append('=');
                }
            }
        }
        return localStringBuilder.toString();
    }
}

Base64

此时需要对Base64算法有一个基础的了解。
5️⃣easyjni - 调用Native方法 | 字符串位置转换 | 🔢Base64换表🔄 - 图6

数据特点

  1. 出现64个字节的字符串(其实应该叫做表)作为数组取值,并且(经常,不是一定)出现“/”和“+”符号
  2. =作为补位,体现为对3求余(%3)

    Base64算法特征

    Base64表格

    64个字符串
    5️⃣easyjni - 调用Native方法 | 字符串位置转换 | 🔢Base64换表🔄 - 图7

    位移

    >> 0/6/12/18 & 0x3F

    等号补位

    上面提到了,这里就不赘述了

    代码

    def base(string:str)->str:
     oldstr = ''
     newstr = []
     base = ''
     base64_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                     'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
                     'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
                     'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']
    
     #原始字符串转换为二进制,用bin转换后是0b开头的,所以把b替换了,首位补0补齐8位
     for i in string:
         oldstr += '{:08}'.format(int(str(bin(ord(i))).replace('0b', '')))
    
     #转换好的二进制按照6位一组分好,最后一组不足6位的后面补0
     for j in range(0, len(oldstr), 6):
         newstr.append('{:<06}'.format(oldstr[j:j + 6]))
    
     #在base_list中找到对应的字符,拼接
     for l in range(len(newstr)):
         base += base64_list[int(newstr[l], 2)]
    
     #判断base字符结尾补几个‘=’
     if len(string) % 3 == 1:
         base += '=='
     elif len(string) % 3 == 2:
         base += '='
    
     return base
    

    结论

    Base64变体 - 换表

    新码表 = { 105, 53, 106, 76, 87, 55, 83, 48, 71, 88, 54, 117, 102, 49, 99, 118, 51, 110, 121, 52, 113, 56, 101, 115, 50, 81, 43, 98, 100, 107, 89, 103, 75, 79, 73, 84, 47, 116, 65, 120, 85, 114, 70, 108, 86, 80, 122, 104, 109, 111, 119, 57, 66, 72, 67, 77, 68, 112, 69, 97, 74, 82, 90, 78 }
    码表ASCII字符串化 = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"
    

    Base64New函数

    把原本Base64使用的码表“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”换成“i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN后进行Base64编码处理”。

    Base64变体 - 题外话

    标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
    为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充’=’号,并将标准Base64中的“+”和“/”分别改成了“-”和“”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。 另有一种用于正则表达式的改进Base64变种,它将“+”和“/”改成了“!”和“-”,因为“+”,“*”以及前面在IRCu中用到的“[”和“]”在正则表达式中都可能具有特殊含义。 此外还有一些变种,它们将“+/”改为“-”或“.”(用作编程语言中的标识符名称)或“.-”(用于XML中的Nmtoken)甚至“:”(用于XML中的Name)。

2️⃣ncheck方法

System.loadLibrary动态加载库文件

System.loadLibrary(String libname)则只会从指定lib目录下查找,并加上lib前缀和.so后缀

System.loadLibrary("native");
……省略……
private native boolean ncheck(String paramString);

传进来的“native”,处理后返回的是libnative.so:
image.png

IDA+.so库文件

将.so库文件拖入IDA中反编译,查看ncheck函数,代码逻辑为将传入的字符串,先前后互换,再将互换后的字符串两两按个对换:
image.png

⚠⚠⚠危险危险危险⚠⚠⚠

在打草稿的时候,我设置原字符串为1234,前后一半对换后为:3412,再按个前后对换为:4321,我这时得出的结论是字符串逆序(Python用reverse),结果一直不对。
再设置原字符串为01234567,前后一半对换后为:45670123,再按个前后对换为:54761032——很明显并不是逆序😥
我还是强行计算了一下规律:

下标从0开始 下标<一半(half = len(str)/2 = 4) 下标=>一半(half = len(str)/2 = 4)
偶数下标 原下标+half+1 [0] = 0+4+1 = 5 原下标-half+1 [4] = 4-4+1 = 1
奇数下标 原下标+half-1 [1] = 1+4-1 = 4 原下标-half-1 [5] = 5-4-1 = 0
#字符串处理
def ArraySwap(str2Change):
    time = 0
    half = int(len(str2Change)/2)
    arrBefore = list(str2Change)
    arrAfter = list(str2Change)

    # 0 < 下标 < 一半
    while time < half :
        arrAfter[time] = arrBefore[time + half + 1]
        arrAfter[time + 1] = arrBefore[time + half]
        time += 2
    else:
        # 一半 < 下标 < 总长
        while half <= time < len(str2Change) :
            arrAfter[time] = arrBefore[time - half + 1]
            arrAfter[time + 1] = arrBefore[time - half]
            time += 2
    return "".join(arrAfter)

str2Change = "01234567"
strChanged = ArraySwap(str2Change)

print("原字符串:", str2Change)
print("转换后:", strChanged)

image.png
结果,危险再次出现!😡😡😡
如果字符串设置为len(str) = 10,也就是half = 5,此时上述代码会出现超出范围的报错。因为字长不是4的整数倍,也就是half是奇数,步长为2会造成溢出的问题:
image.png
还是建议老老实实按照代码还原,安全一点😵

解法

【👩‍💻练Python推荐⌨】Python脚本

Python - 切片

Python - decode()方法

import base64

str2Swap = "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7"
tableBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
tableNew = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"

#1. 字符串处理
def ArraySwap(str2Swap):
    time = 0
    half = int(len(str2Swap)/2)

    #1.1 前后两半互换
    str2Swap = str2Swap[half:] + str2Swap[:half]
    arr = list(str2Swap)

    #1.2 前后两个互换
    while ( time < len(str2Swap) ):
        arr[time],arr[time+1] = arr[time+1],arr[time]
        time += 2
    return "".join(arr)

str2NewDeBase64 = ArraySwap(str2Swap)
print("用来DeBase64的字符串:", str2NewDeBase64)

#2. Base64换表

'''
maketrans():用于创建字符映射的转换表,对于接受两个参数的最简单的调用方式,第一个参数是字符串,表示需要转换的字符,第二个参数也是字符串表示转换的目标;
translate():法根据参数table给出的表(包含 256 个字符)转换字符串的字符, 要过滤掉的字符放到 del 参数中;
decode():以encoding指定的编码格式解码字符串。
'''

def BaseChangeTable(str,table):
    return base64.b64decode(str2NewDeBase64.translate(str.maketrans(table,tableBase64)))

flag = BaseChangeTable(str2NewDeBase64,tableNew).decode("utf-8")
print("Flag:" + flag)

💯You are right!💯

image.png