#逆向-CBCTF2024-复现(4/)

👻5ea1


鸽鸽的礼物

拖入jdax,根据AndroidManifest.xml文件,可知入口为 com.cbctf.click.MainActivity,反编译这个类,进入按钮点击事件观察逻辑
1
进入按钮点击事件后可以看到以下加密步骤

if (mainActivity.f2008y == 802) {
            char[] charArray = "lrgm~;<lgk:j:hj:8594;<739<g6i3;37jh8g\u0080".toCharArray();
            StringBuilder sb = new StringBuilder();
            for (char c2 : charArray) {
                sb.append((char) (c2 - 3));
            }
            String sb2 = sb.toString();
            StringBuilder sb3 = new StringBuilder();
            for (char c3 : sb2.toCharArray()) {
                if (Character.isUpperCase(c3)) {
                    sb3.append((char) (((c3 - '*') % 26) + 65));
                } else if (Character.isLowerCase(c3)) {
                    sb3.append((char) (((c3 - 'J') % 26) + 97));
                } else {
                    sb3.append(c3);
                }
            }
            textView.setText(sb3.toString());

所以当点击次数到达802时,会正向解密得到明文flag
故有如下解

  1. 暴力点击,直接在软件中点802次
  2. 写c++脚本解密
#include<stdio.h>
int main()
{
	char a[100]="lrgm~;<lgk:j:hj:8594;<739<g6i3;37jh8g\u0080";
	int i;
	for(i=0;i<=43;i++)
	{
		a[i]-=3;
		if(a[i]>=65&&a[i]<=65+26-1)
	    {
			a[i]=(a[i]-'*'+26+26+26)%26+65;
		}
		if(a[i]>=97&&a[i]<=97+26-1)
		{
			a[i]=(a[i]-'J'+26+26+26)%26+97;
		}
	}
	printf("%s",a);
	return 0;
}
  1. 运行这一段java代码,输出解密结果
  2. 使用安卓逆向技术,借助MT管理器修改代码,把802改成0

打开MT管理器,获取APK文件,在Dex文件中查找密文,从而找到解密代码块,在该块中搜索802数据,无果,猜测可能用了16位存储,由于(802)d=(322)h,再次搜索322,成功,修改成0,重新签名后导出安装,运行获得flag
2


inside

先将文件拖进die里,看到有一个tmd壳
1
直接甩进Themid脱壳机里面,得到脱壳后的文件
再丢进ida里面,直接f5看
2
可以猜测v10是明文加密后的结果,v7是密文,v8是flag,sub_7FF77F211037(v8, v10)即为加密过程
进入观察,发现是一个换盒base64,外加一个按四位密钥的循环异或加密
3
直接写脚本解答

v7=[0 for i in range(100)]
v7[0]=ord('D')
v7[1]=ord('e')
v7[2] = 15;
v7[3] = 119;
v7[4] = 67;
v7[5] = 16;
v7[6] = 28;
v7[7] = 4;
v7[8] = 113;
v7[9] = 17;
v7[10] = 41;
v7[11] = 88;
v7[12] = 77;
v7[13] = 102;
v7[14] = 34;
v7[15] = 1;
v7[16] = 118;
v7[17] = 57;
v7[18] = 106;
v7[19] = 90;
v7[20] = 88;
v7[21] = 56;
v7[22] = 3;
v7[23] = 73;
v7[24] = 88;
v7[25] = 57;
v7[26] = 106;
v7[27] = 64;
v7[28] = 91;
v7[29] = 102;
v7[30] = 23;
v7[31] = 85;
v7[32] = 76;
v7[33] = 63;
v7[34] = 3;
v7[35] = 85;
v7[36] = 76;
v7[37] = 103;
v7[38] = 106;
v7[39] = 28;

for i in range(40):
    if(i%4==0):
        v7[i]^=0x15;
    if(i%4==1):
        v7[i]^=0x55;
    if(i%4==2):
        v7[i]^=0x45;
    if(i%4==3):
        v7[i]^=0x33;
for i in range(40):
    print(chr(v7[i]),end='')

得到Q0JDVEY7dDlkX3g2cl/iMmFzMl/sN3RfYjFfY2//,放进换盒的赛博厨子中解密,得到flag
4


让我康康!

无壳,直接拖进ida里面,跳转到主程序,发现经过了OLLVM平坦化混淆,所以拿到main函数的地址,用deflat.py脚本做一次去平坦化。
5
python deflat.py --f cancanneed --addr 0x401C20
运行脚本后获得cancanneed_recovered文件,为避免混淆,重命名为cancanneed_re1,拖进ida看main函数
6
去平坦化成功,反汇编看伪代码
7
伪代码中可以看到几处特征块,aYesyesyesyesye,aNononononono,__isoc99_scanf,所以在主程序中实现了flag的输入以及与密文的比较,故可获取密文为unk_409080处的数据
由于只调用了一个函数,所以加密函数大概率就是sub_402160,进去看看,又是OLLVM平坦化混淆,再次去平坦化
8
python deflat.py --f cancanneed_re1 --addr 0x402160
得到cancanneed_re1_re2文件
再次拖进ida,看加密函数
9
去平坦化成功
发现一个名为sub_405420的函数,进入查看,发现%256操作,结合后面的异或,由于密文只变化了一次,猜测是RC4加密
10
有了方向,从头开始分析
11
12
13
所以有

unsigned char byte_409050[] =
{
  0x36, 0x1C, 0x74, 0x95, 0x2B, 0x27, 0x0A, 0x9E, 0x3B, 0x3A, 
  0x0D, 0x1C, 0x24, 0xC3, 0x64, 0x27, 0xEC, 0x74, 0x87, 0xA9
};

v7是byte_409050的循环
v8是range(256)

x=(v7[i]+v8[i]+x)%256
y=i

swap(v8[x],v8[y])

--------------------

x=x+v8[i]

y=v8[(v8[x]+v8[i])%256]^rand()

enc^=y

unsigned char enc1[] =
{
  0x21, 0xEE, 0x74, 0xF0, 0x2C, 0x76, 0xC4, 0x29, 0x33, 0x8F, 
  0xA2, 0xC2, 0x25, 0x18, 0x57, 0xA0, 0xAB, 0x42, 0x94, 0x76, 
  0x4F, 0x57, 0x47, 0x1B, 0x58, 0xE4, 0xD9, 0xE3, 0x1D, 0x35, 
  0x4E, 0xA4
};

按照已有信息写脚本,发现无法解密,再次回到题目中,按x对密钥进行交叉引用
14
15
发现在使用密钥前还对密钥有一段加密,但是这一段加密仍然在OLLVM平坦化的保护下
所以再次去平坦化
python deflat.py --f cancanneed_re1_re2 --addr 0x401590
当然,这个加密函数外的控制部分也是OLLVM,可以按需去平坦化
python deflat.py --f cancanneed_re1_re2_re3 --addr 0x401170
可以看到这里有一个反调试,导致之前调试的时候dump的数据是错的

现在有两种解决方法,

  1. 去反调试
  2. 直接解密
    下面来一一解答

去反调试

看到关键部分
16
v14是一个标志值,而v14在ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);的调用中被赋值,结合一些上网查询,可以发现这个函数的作用就是查询进程的调试情况
因为这个地方有if语句,所以直接进到汇编里面,找到if对应的跳转,发现使用了jnz指令来跳转,那么仅需要把跳转指令改为jz即可,或者直接改为jmp
17
查询汇编语言和机器指令转换的对应表,知道只要把0x85改为0x84即可
18
修改完成
此时调试可以直接拿到密钥值

unsigned char ida_chars[] =
{
  0x73, 0x68, 0x6F, 0x77, 0x5F, 0x6D, 0x65, 0x5F, 0x79, 0x6F, 
  0x75, 0x72, 0x5F, 0x70, 0x6F, 0x77, 0x65, 0x72, 0x21, 0x21
};

Show me you power!!

直接解密

直接跟进解密函数查看
19
xxtea
由于是解密,所以实际上可以直接把解密代码复制下来贴到代码里

	unsigned char key_key[] =
	{
		0x36, 0x1C, 0x74, 0x95, 0x2B, 0x27, 0x0A, 0x9E, 0x3B, 0x3A,
		0x0D, 0x1C, 0x24, 0xC3, 0x64, 0x27, 0xEC, 0x74, 0x87, 0xA9
	};
	unsigned char enc[] =
	{
		0x21, 0xEE, 0x74, 0xF0, 0x2C, 0x76, 0xC4, 0x29, 0x33, 0x8F,
		0xA2, 0xC2, 0x25, 0x18, 0x57, 0xA0, 0xAB, 0x42, 0x94, 0x76,
		0x4F, 0x57, 0x47, 0x1B, 0x58, 0xE4, 0xD9, 0xE3, 0x1D, 0x35,
		0x4E, 0xA4
	};
	unsigned char tea[] =
	{
		0x0B, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x0E, 0x00,
		0x00, 0x00, 0x0B, 0x00, 0x00, 0x00
	};
	int i, j, x, y;
	unsigned int* xxtea_key = (unsigned int*)key_key;
	unsigned int* xxtea = (unsigned int*)tea;

	int v3, v6;

	v3 = 16;
	i = 0x14514000;
	while (v3-- > 0)
	{
		for (j = 4; j >= 0; j--)
		{
			xxtea_key[j] -= ((xxtea_key[(j + 4) % 5] ^ (xxtea[(((i >> 2) & 3 ^ j) & 3)]))
				+ (xxtea_key[(j + 1) % 5] ^ i)) ^ ((16 * xxtea_key[(j + 4) % 5]) ^ (xxtea_key[(j + 1) % 5] >> 3))
					+ ((4 * xxtea_key[(j + 1) % 5]) ^ (xxtea_key[(j + 4) % 5] >> 5));

		}
		i -= 0x11451400;

	}

最终拼起来可以得到一个最终解密脚本

#include<stdio.h>
#include<cstdlib>
int main()
{
	unsigned char key_key[] =
	{
		0x36, 0x1C, 0x74, 0x95, 0x2B, 0x27, 0x0A, 0x9E, 0x3B, 0x3A,
		0x0D, 0x1C, 0x24, 0xC3, 0x64, 0x27, 0xEC, 0x74, 0x87, 0xA9
	};
	unsigned char enc[] =
	{
		0x21, 0xEE, 0x74, 0xF0, 0x2C, 0x76, 0xC4, 0x29, 0x33, 0x8F,
		0xA2, 0xC2, 0x25, 0x18, 0x57, 0xA0, 0xAB, 0x42, 0x94, 0x76,
		0x4F, 0x57, 0x47, 0x1B, 0x58, 0xE4, 0xD9, 0xE3, 0x1D, 0x35,
		0x4E, 0xA4
	};
	unsigned char tea[] =
	{
		0x0B, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x0E, 0x00,
		0x00, 0x00, 0x0B, 0x00, 0x00, 0x00
	};
	int i, j, x, y;
	unsigned int* xxtea_key = (unsigned int*)key_key;
	unsigned int* xxtea = (unsigned int*)tea;

	int v3, v6;

	v3 = 16;
	i = 0x14514000;
	while (v3-- > 0)
	{
		for (j = 4; j >= 0; j--)
		{
			xxtea_key[j] -= ((xxtea_key[(j + 4) % 5] ^ (xxtea[(((i >> 2) & 3 ^ j) & 3)]))
				+ (xxtea_key[(j + 1) % 5] ^ i)) ^ ((16 * xxtea_key[(j + 4) % 5]) ^ (xxtea_key[(j + 1) % 5] >> 3))
					+ ((4 * xxtea_key[(j + 1) % 5]) ^ (xxtea_key[(j + 4) % 5] >> 5));

		}
		i -= 0x11451400;

	}
	unsigned char key[256], s_box[256];
	for (i = 0; i < 256; i++)
	{
		key[i] = key_key[i % 20];
		s_box[i] = i;
	}

	x = 0;
	for (i = 0; i < 256; i++)
	{
		x = (key[i] + s_box[i] + x) % 256;
		y = i;
		s_box[x] ^= s_box[y];
		s_box[y] ^= s_box[x];
		s_box[x] ^= s_box[y];
	}
	x = 0;
	y = 0;
	srand(0x1BF52);
	for (i = 1; i <= 32; i++)
	{
		x += s_box[i];
		y = s_box[(s_box[x % 256] + s_box[i]) % 256] ^ rand();
		enc[i-1] ^= y;
		printf("%c",enc[i-1]);
	}

	return 0;
}

注意,这个代码中由于有随机数,在双系统下结果不同,所以要在linux系统下编译运行
g++ -o VS_exp VS_esp.cpp
运行后得到flag
20


ezHarmony

在安卓逆向中,我们已经接触过APK文件,这种文件实际上是由zip压缩包文件修改后缀名而来;而在鸿蒙系统中,安装包的类型则是APP文件,这种类型的文件也可以被视作一个压缩包,但有所区别。
ArkTS是HarmonyOS优选的主力应用开发语言。方舟字节码(ArkCompiler Bytecode)文件,是ArkCompiler的编译工具链以源代码作为输入编译生成的产物,其文件后缀名为.abc。在发布时,abc文件会被打包到HAP中。
当我们对APP文件作第一次解压以后,可以解压出一个HAP文件,根据ArkTs打包的相关定义,我们要反汇编的abc文件就在这个HAP文件中。故对原安装包作两次解压,得到abc字节码文件。
针对方舟字节码的反编译,目前已有一些工具可以使用,例如:abcde和基于jadx的abc-decompiler。由于abc字节码文件在格式上与安卓的DEX文件类似,所以可以使用jadx的插件版abc-decompiler来作相关逆向


拿到题目,先作两次解压,得到modules.abc文件,将该文件放入abc-decompiler中逆向
21
可以看到代码的一个树状结构
首先观察EntryAbility,从名字上看猜测与进入程序相关,那就主要去看在这个类中的跳转
22
发现跳转至/pages/Index类中,进入观察
在该类底部发现了flag判断相关的语句,以及正误的输出,所以这个类就是要找的加密类
23
观察主程序的调用段和判断段,发现该flag主要被分为了3段,分别为0-6 substring6-13 substring213-tail substring3,然后对substring2和substring3作加密操作与判定
简单分析,易得,substring是flag的前6位,即CBCTF{,故不作判断,而后面两段字符串各自作了一段加密操作 ,所以要找到加密逻辑并进行逆向

substring2

判断语句为i2 = (_lexenv_1_1_("7a747160774b", substring2) == substring ? 1 : 0);
首先把这些数据dump下来,已知的是"7a747160774b"CBCTF{两个字符串
然后本人在做的时候先是看到前面一个字符串每一个字节都比较接近,然后就将其分成了6个字节,字节数和密文长度相等,所以尝试了一下异或解密

unsigned char c[]={0x7a,0x74,0x71,0x60,0x77,0x4b};
unsigned char key[]="CBCTF{";
unsigned char flag[50];
for(int i=0;i<6;i++)
{
	flag[i]=key[i];
}
for(int i=0;i<6;i++)
{
	flag[i+6]=key[i]^c[i];
}

发现刚好可以解出6位规整的数字!即flag的第二部分的前六位为962410
然后再返回去看代码,发现了这样一段约束
24
所以flag的第二部分的数字和为25,可得flag的第二部分为9624103


正解:
因为加密函数为_lexenv_1_1_,在上下文寻找,发现了这样一部分的定义

 _lexenv_0_12_ = newTarget;
        _lexenv_0_13_ = this;
        _lexenv_0_3_ = #*#f1;
        _lexenv_0_1_ = #*#e1;
        _lexenv_0_2_ = #*#d1;
        _lexenv_0_8_ = #*#a1;
        _lexenv_0_0_ = #*#u;
        _lexenv_0_5_ = #*#t;
        _lexenv_0_9_ = #*#c1;
        _lexenv_0_7_ = #*#o;
        _lexenv_0_4_ = #*#j;
        _lexenv_0_6_ = #*#m;
        _lexenv_0_10_ = #*#b1;

故猜测该加密函数即e1函数
25
是异或加密
接着寻找_lexenv_0_1_,在函数u中找到定义:_lexenv_0_1_ = this;
26
所以可以得出flag第二部分的逻辑:

  1. 七位数,质数,数字不重复,数字之和为 25,首位数字为奇数,加 375924为 8 位数
  2. 使用了异或加密

可解得flag第二部分

substring3

同理,flag的第三部分的加密使用了_lexenv_1_3_函数和_lexenv_1_2_函数,还是回到上面定义函数的地方,知道要观察a1函数和b1函数
27
28
显然,这是一个xxtea加密函数,修改了DELTA参数和加密次数,将DELTA改为了上一个的密文

unsigned int key[]={883967, 8556999, 116847, 926247};
unsigned int delta=962410;
unsigned int enc[]={2341984810, 799981539, 2235051828};
unsigned int n=3;

unsigned int mx(unsigned int sum_, unsigned int y, unsigned int z, unsigned int p, unsigned int e) {
    return (( (z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum_ ^ y) + (key[(p & 3) ^ e] ^ z)));
}

void decrypt() 
{
    unsigned int rounds = 13 + 52 / n;
    unsigned int sum_ = (rounds * 9624103) & 0xFFFFFFFF;
    unsigned int y = enc[0];
    while (rounds > 0) {
        rounds--;
        unsigned int e = (sum_ >> 2) & 3;
        for (int p = n - 1; p > 0; p--) {
            unsigned int z = enc[p - 1];
            enc[p] = (enc[p] - mx(sum_, y, z, p, e)) & 0xFFFFFFFF;
            y=enc[p];
        }
        unsigned int z = enc[n - 1];
        enc[0] = (enc[0] - mx(sum_, y, z, 0, e)) & 0xFFFFFFFF;
        y=enc[0];
        sum_ = (sum_ - 9624103) & 0xFFFFFFFF;
    }
}

故可写出xxtea的解密脚本

两者结合可得exp:

#include<stdio.h>

unsigned int key[]={883967, 8556999, 116847, 926247};
unsigned int delta=962410;
unsigned int enc[]={2341984810, 799981539, 2235051828};
unsigned int n=3;

unsigned int mx(unsigned int sum_, unsigned int y, unsigned int z, unsigned int p, unsigned int e) {
    return (( (z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum_ ^ y) + (key[(p & 3) ^ e] ^ z)));
}

void decrypt() 
{
    unsigned int rounds = 13 + 52 / n;
    unsigned int sum_ = (rounds * 9624103) & 0xFFFFFFFF;
    unsigned int y = enc[0];
    while (rounds > 0) {
        rounds--;
        unsigned int e = (sum_ >> 2) & 3;
        for (int p = n - 1; p > 0; p--) {
            unsigned int z = enc[p - 1];
            enc[p] = (enc[p] - mx(sum_, y, z, p, e)) & 0xFFFFFFFF;
            y=enc[p];
        }
        unsigned int z = enc[n - 1];
        enc[0] = (enc[0] - mx(sum_, y, z, 0, e)) & 0xFFFFFFFF;
        y=enc[0];
        sum_ = (sum_ - 9624103) & 0xFFFFFFFF;
    }
}


int main()
{
	unsigned char c[]={0x7a,0x74,0x71,0x60,0x77,0x4b};
	unsigned char key[]="CBCTF{Welcome_to_CBCTF_2024}";
	unsigned char flag[50];
	for(int i=0;i<6;i++)
	{
		flag[i]=key[i];
	}
	for(int i=0;i<6;i++)
	{
		flag[i+6]=key[i]^c[i];
	}
	flag[12]='3';
	for(int i=0;i<13;i++)
	{
		printf("%c",flag[i]);
	}
	
	decrypt();
	
	unsigned char *flag1=(unsigned char *)enc;
	for(int i=0;i<12 ;i++)
	{
		printf("%c",flag1[i]);
	}
	printf("}\n");
	return 0;
}

ininside