2017 SSCTF Login 200

Java层上了一堆中文变量

在一顿修复之后,大概的能梳理出校验过程

校验的地方

一层一层分析,这是一个native函数,需要使用IDA分析

JniTest.getValue(strInput.getBytes("utf-8"))

使用IDA载入对应的so文件,有一个关键的encode()函数

跟入发现白茫茫一片

使用流程图查看,发现调用关系断开

先去掉下面这个块的函数尾

.text:000025CC ; START OF FUNCTION CHUNK FOR encode
.text:000025CC CODE16
.text:000025CC
.text:000025CC loc_25CC
.text:000025CC BX      PC
.text:000025CC ; END OF FUNCTION CHUNK FOR 

然后Force BL Call

即可修复,F5之后的代码也比较正常可读

伪代码

入口处看到了长度由12变为16位,也就是三位扩充成4位,猜测是Base64算法

if ( v4 )
    dwDivideJudge = j_j___divsi3(_lenOfbaRegCode, 3) + 1;
else
    dwDivideJudge = j_j___divsi3(_lenOfbaRegCode, 3);// Java层注册码要求12位
lpStringBuffer = j_j_malloc(4 * dwDivideJudge);// 长度由12位扩充为16位

找到一个转换表

使用WinHex找到对应偏移,C Source模式拷贝出来

unsigned char data[66] = {
    0x21, 0x3A, 0x23, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2B, 0x2D, 0x2A, 0x2F, 0x60, 0x7E, 0x5F, 0x5B, 
    0x5D, 0x7B, 0x7D, 0x3F, 0x3C, 0x3E, 0x2C, 0x2E, 0x40, 0x5E, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 
    0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 
    0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x5C, 0x27, 
    0x3B, 0x00
};

IDA里没显示完全,其实应该是下面这样的

!:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\'; 

正常的Base64编码表如下

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

所以在这里写还原代码的时候不能使用下面这种常规编码表的方式

static char Base64Table[256] = {
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0   - 15
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16  - 31
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 32  - 47
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 48  - 63
    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, // 64  - 79
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 80  - 95
    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96  - 111
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, // 112 - 127
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 - 143
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 144 - 159
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 160 - 175
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 176 - 191
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 192 - 207
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 208 - 223
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 224 - 239
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 240 - 255
};

而是根据对应的char来查找index,然后拼接到一个int型数据里去,由于编码表是变形的,所以只能使用循环来查找

这里我放一份比较常规的Base64编码的C语言实现,可以跟反编译后的代码进行对比

const char Base64Code[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

void Base64_encode(char *lpString)
{
    char *lpBuffer;
    int lpString_len = strlen(lpString);
    while(lpString_len > 0)
    {
        *lpBuffer++ = Base64Code[(lpString[0] >> 2 ) & 0x3F];
        if(lpString_len > 2)
        {
            *lpBuffer++ = Base64Code[((lpString[0] & 3) << 4) | (lpString[1] >> 4)];
            *lpBuffer++ = Base64Code[((lpString[1] & 0xF) << 2) | (lpString[2] >> 6)];
            *lpBuffer++ = Base64Code[lpString[2] & 0x3F];
        }
        else
        {
            switch(lpString_len)
            {
            case 1:
                *lpBuffer ++ = Base64Code[(lpString[0] & 3) << 4 ];
                *lpBuffer ++ = '=';
                *lpBuffer ++ = '=';
                break;
            case 2:
                *lpBuffer ++ = Base64Code[((lpString[0] & 3) << 4) | (lpString[1] >> 4)];
                *lpBuffer ++ = Base64Code[((lpString[1] & 0x0F) << 2) | (lpString[2] >> 6)];
                *lpBuffer ++ = '=';
                break;
            }
        }
        lpString += 3;
        lpString_len -= 3;
    }
    *lpBuffer = '\0';
}

Base64编码基于的一个原理就是6位01即可表示64个不同的字符在取第一个字符的时候,只需要取前6位,转为index,找到编码表对应的字符即可,但是这里的算法还进行了一次异或,所以最终的编码在计算的时候,需要把每4个字节的第一个字节跟0x3F进行异或,再去找对应的字符下标index

 *lpStringBuffer_chBy4b = a__Abcdefghijkl[temp >> 2] ^ 0x3F;

第二字节同样做了异或操作

lpStringBuffer_chBy4b[1] = *(&a__Abcdefghijkl[16 * temp & 0x3F] + (j >> 4)) ^ 0xF;

第三和第四字节常规的编码,没有其余操作

结合输入的注册码为12位,那么我们还原的代码如下,对于后面的判断我就省略了

public class MyClass {
	
	static byte[] dataEncoded = {0x01, 0x63, 0x5e, 0x6c, 0x5f, 0x23, 0x78, 0x25, 0x5f, 0x27, 0x35, 0x6c, 0x11, 0x66, 0x31, 0x65};
	
	static byte[] Base64Table = {
		    0x21, 0x3A, 0x23, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2B, 0x2D, 0x2A, 0x2F, 0x60, 0x7E, 0x5F, 0x5B, 
		    0x5D, 0x7B, 0x7D, 0x3F, 0x3C, 0x3E, 0x2C, 0x2E, 0x40, 0x5E, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 
		    0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 
		    0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x5C, 0x27, 
		    0x3B, 0x00
		};
	
	private static int GetCharIndex(byte ch) {
		for(int i = 0; i < Base64Table.length; i++) {
			if (ch == Base64Table[i]) {
				return i;
			}
		}
		return -1;
	}
	
	public static void main(String[] args) throws Exception {
		int len = dataEncoded.length * 3 / 4;
		byte[] dataDecoded = new byte[len];
		int j = 0;
		for(int i = 0; i < dataEncoded.length; i += 4) {
			dataDecoded[j++] = (byte)((GetCharIndex((byte)(dataEncoded[i] ^ 0x3F)) << 2) + ((GetCharIndex((byte)(dataEncoded[i + 1] ^ 0x0F)) >> 4) & 0x03));
			dataDecoded[j++] = (byte)(((GetCharIndex((byte)(dataEncoded[i + 1] ^ 0x0F)) & 0x0F) << 4) + ((GetCharIndex(dataEncoded[i + 2]) >> 2) & 0x0F));
			dataDecoded[j++] = (byte)((GetCharIndex(dataEncoded[i + 2]) << 6) + (GetCharIndex(dataEncoded[i + 3])));
		}
		System.out.println(new String(dataDecoded));
	}
}

因为上一题的套路,我猜后面那个32位字符串就是应该用来解密的数据,所以直接拿来测试

输出

VVe1lD0ne^-^

为了确定整个流程,再次回到Java层进行流程的分析

DecryptUtils.checkRegCode(DecryptUtils.extensionTo32(JniTest.getValue(strInput.getBytes("utf-8"))), "01635e6c5f2378255f27356c11663165")

首先是DecryptUtils.extensionTo32(),用于将byte[]转为32位字符串

public static String extensionTo32(byte[] Data) {
    StringBuilder stringBuilder = new StringBuilder();
    int lenOfData = Data.length;
    int i;
    for(i = 0; i < lenOfData; ++i) {
        stringBuilder.append(String.format("%02x", Integer.valueOf(Data[i] & 255)));
    }

    return stringBuilder.toString();
}

最终的DecryptUtils.checkRegCode()很有意思,一不小心就会被带到坑里去

public static boolean checkRegCode(String strCalcu, String strJUDGE) {
    boolean boolResult = false;
    try {
        String v4 = DecryptUtils.CTF1(DecryptUtils.Encrypt_MD5(strCalcu), strCalcu);
        String v5 = DecryptUtils.CTF1(DecryptUtils.Encrypt_MD5(strJUDGE), strJUDGE);
        if(v4.length() < v5.length()) {
            return boolResult;
        }

        int i = 0;
        int j = 0;
        while(i < v4.length()) {
            int temp = (v5.charAt(i) ^ v4.charAt(i)) + i ^ i;
            j += temp;
            while(temp > 0) {
                j += v5.charAt(temp) + v4.charAt(temp);
                --temp;
            }

            ++i;
        }

        boolResult = DecryptUtils.Encrypt_MD5(String.valueOf(j)).equals("cfcd208495d565ef66e7dff9f98764da");
    }
    catch(Exception v1) {
        v1.printStackTrace();
    }

    return boolResult;
}

如果反推,马上就可以得到答案,如果正向走下来

我们正向走一遍,首先是CTF1(),将每个字符拆成2个4位,然后再挑数据拼接

接下来进行对比计算

int i = 0;
int j = 0;
while(i < v4.length()) {
    int temp = (v5.charAt(i) ^ v4.charAt(i)) + i ^ i;
    j += temp;
    while(temp > 0) {
        j += v5.charAt(temp) + v4.charAt(temp);
        --temp;
    }

    ++i;
}
boolResult = DecryptUtils.Encrypt_MD5(String.valueOf(j)).equals("cfcd208495d565ef66e7dff9f98764da");

计算完后有一个MD5值的对比,尝试解密这个MD5

您查询的字符串是“cfcd208495d565ef66e7dff9f98764da”,解密的结果为“0”!

那么说明计算完j是0

回头看计算过程,j一直是相加的计算,想让j是0,那么在异或的时候两个值一定要相等,说明两个字符串相等,回溯到最前面,说明传进来的两个字符串相等,那么直接拿上面的32位字符串去解密,就可以得到正确的输入数据,输入那个数据,就可以弹出flag

输入VVe1lD0ne^-^,弹出flag

Last updated