国赛分区赛Reverse

国赛分区赛Reverse

havetea

flag{c616454f52a6334273b5f455a10ef818}

拿到题目,exeinfope查壳,无壳32位文件,放入IDA分析。我们可以大体梳理出程序逻辑:先对输入的secret key进行长度判断,16字节,然后进行加密,然后判断。判断成功则进行下一次对serect message的输入,进行长度判断,32字节,然后进行加密,判断。

我们来看对key的加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int __thiscall sub_B920F0(unsigned int *this)
{
int v1; // edi
unsigned int v2; // esi
unsigned int result; // eax
int v4; // ebx

v1 = 0;
v2 = *this;
result = this[1];
v4 = 32;
do
{
v1 -= 0x61C88647;
v2 += (v1 + result) ^ (dword_BBAA90 + 16 * result) ^ (dword_BBAA94 + (result >> 5));
result += (v1 + v2) ^ (dword_BBAA98 + 16 * v2) ^ (dword_BBAA9C + (v2 >> 5));
--v4;
}
while ( v4 );
*this = v2;
this[1] = result;
return result;
}

减去0x61C88647就相当于加上0x9E3779B9(补码),这个值就很敏感,是Tea加密的特征值,我们只需要逆着写出解密脚本就行。比较数据我们通过动态调试读取内存得出。

Secret Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import struct

def TeaDecrypto(a):
global DeltaArr
global DeltaVariable
global SecretKey
v1=a[0]
v2=a[1]
DeltaEnd=DeltaVariable
for i in range(32):
v2-=((((v1<<4)&0xffffffff)+DeltaArr[2])&0xffffffff)^((v1+DeltaEnd)&0xffffffff)^((((v1>>5)&0xffffffff)+DeltaArr[3])&0xffffffff)
v2&=0xffffffff
v1-=((((v2<<4)&0xffffffff)+DeltaArr[0])&0xffffffff)^((v2+DeltaEnd)&0xffffffff)^((((v2>>5)&0xffffffff)+DeltaArr[1])&0xffffffff)
v1&=0xffffffff
DeltaEnd-=Delta
# print(hex(v1),hex(v2))
tmp=struct.pack('<I',v1)
SecretKey+=str(tmp,encoding="utf-8")
tmp=struct.pack('<I',v2)
SecretKey+=str(tmp,encoding="utf-8")

Delta=0x9E3779B9
DeltaArr=[0x9E, 0x37, 0x79, 0xB9]
VerifyData1=[0xE66F0E9E, 0x42A17CD6]
VerifyData2=[0x5BDDE878, 0xD236F4AD]
SecretKey=''
DeltaVariable=Delta*32
TeaDecrypto(VerifyData1)
TeaDecrypto(VerifyData2)
print(SecretKey)

得到key=please_drink_tea

接下来看对secret message的加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
unsigned int __fastcall sub_B92170(unsigned int *a1, int a2)
{
unsigned int result; // eax
unsigned int v3; // esi
unsigned int v4; // edi
unsigned int v5; // ecx
int v7; // [esp+14h] [ebp-4h]

v7 = 32;
result = 0;
v3 = *a1;
v4 = a1[1];
do
{
v5 = result + *(_DWORD *)(a2 + 4 * (result & 3));
result -= 0x61C88647;
v3 += v5 ^ (v4 + ((16 * v4) ^ (v4 >> 5)));
v4 += (result + *(_DWORD *)(a2 + 4 * ((result >> 11) & 3))) ^ (v3 + ((16 * v3) ^ (v3 >> 5)));
--v7;
}
while ( v7 );
a1[1] = v4;
*a1 = v3;
return result;
}

传入的参数是我们的输入和Key,动态调试分析程序,传入8字节,每4字节一组,传入的key分为四组,四字节一组。再往下分析加密,发现是典型的xtea加密。同样的逆着写出解密脚本,带入动态调试得到的比较数据。

Secret Message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import struct


def TeaDecrypto(a):
global Delta
global SecretKey
global DeltaEveryTurn
global Flag
v1=a[0]
v2=a[1]
DeltaEveryRound=DeltaEveryTurn
for i in range(32):
v2 -= ((DeltaEveryRound + (SecretKey[(DeltaEveryRound>>11)&3])&0xffffffff)&0xffffffff) ^ ((v1 + (((v1<<4)&0xffffffff) ^ ((v1 >> 5)&0xffffffff)))&0xffffffff)
v2&=0xffffffff
DeltaEveryRound-=Delta
v1 -= ((DeltaEveryRound+SecretKey[DeltaEveryRound & 3])&0xffffffff) ^ ((v2 + (((v2<<4)&0xffffffff) ^ ((v2 >> 5)&0xffffffff)))&0xffffffff)
v1&=0xffffffff
# print(hex(v1),hex(v2))
tmp=struct.pack('<I',v1)
Flag+=str(tmp,encoding='utf-8')
tmp=struct.pack('<I',v2)
Flag+=str(tmp,encoding='utf-8')

Flag=''
SecretKey=[0x61656c70, 0x645f6573, 0x6b6e6972, 0x6165745f]
Delta=0x9e3779b9
DeltaEveryTurn=32*Delta
Compare_data1=[0x7D758E0A, 0xEDAFF6AD]
Compare_data2=[0x9DDE7E86, 0xE3D36CD5]
Compare_data3=[0xABD3C82E, 0x6B7B2CFE]
Compare_data4=[0x2C6608C2, 0x0686A1DA]
TeaDecrypto(Compare_data1)
TeaDecrypto(Compare_data2)
TeaDecrypto(Compare_data3)
TeaDecrypto(Compare_data4)
print(Flag)

得到Message=c616454f52a6334273b5f455a10ef818

做题的时候遇到了一个一直想不通的问题,在我动态调试获取xtea加密的比较数据,然后解密发现原始数据太大,根本不是可以被直接输入的字符。百思不得其解去查看了wp发现我的比较数据和wp里面的有细微差别,当时没在意,就把wp里的比较数据带进去,最后的结果跟wp里的一样。我当时很高兴觉得题目已经解决了。但是我把结果带进去调试(当时断点设在了v18 = __rdtsc()处,这个下面会提到),发现最后结果反馈了Wrong!这我就很不能理解了,我再反复查看比较数据,还是和先前一样有着细微差距。我注意到判断语句前有这样一段代码:

1
2
3
4
5
6
7
v18 = __rdtsc();
dword_3BBDB8 = v18;
v19 = 28;
v20 = (unsigned int)(dword_3BBDBC + v18 - dword_3BC5C4 - dword_3BC5C0) >> 5;
if ( v20 > 0x100 )
v20 = 256;
v21 = (_DWORD *)dword_3BBDC0[v20];

我们查询*__rdtsc()*函数,msdn上是这样解释的:Generates the _rdtsc instruction, which returns the processor time stamp. The processor time stamp records the number of clock cycles since the last reset.tsc时间戳寄存器记录时钟周期的。而此函数的作用是返回处理器时间戳记录的自上次重置以来的时钟周期数。当我们执行某程序时,处理器重置来处理过程,__rdtsc读取到该函数为止的周期数,也可理解为执行到该函数所费的时间。接下来v20 = (unsigned int)(dword_3BBDBC + v18 - dword_3BC5C4 - dword_3BC5C0) >> 5然后对v20的值是否大于0x100进行判断。v20的赋值式子里面的那些dword_***,我们往前寻找他们的赋值式,发现也同理。dword_3BBDBC和v18分别是执行到tea加密和xtea加密所费的时间,dword_3BC5C4和dword_3BC5C0

所记录的是在执行到两段加密前所费的时间。所以他们相加相减得到加密的总时长。对总时长进行判断。v21记录的是比较数据的地址,由dword_3BBDC0[v20]给v21赋值。所以加密的总时长影响最后的比较数据。我们断点下在v18 = __rdtsc()处,所以程序执行到此被我们停住了,但是tsc寄存器对时钟周期的记录未停止,所以导致函数返回的时间过长,使得我们读到的比较数据的值不正确,影响最终结果。我们将断点下在v18 = __rdtsc()之后就可以读到正确的比较数据,进而解密得到正确答案。

puzzle

flag{23c3cb3aedbbfdd009d1bf52e530676a}

拿到题目为exe文件,exeinfope查壳显示为[ Tampared file ] UPX v.3.91w - 64 bit EXE signature:被篡改的UPX。知道是UPX壳于是尝试直接使用FUPX脱壳发现FUPX不能将其识别成UPX壳,所以猜测是修改了壳程序的一些特征信息。用010Editor查看,发现几个节区名都为’vmp’,我们手动将其改为’UPX’。再使用FUPX进行脱壳,就可以识别到UPX壳并完成脱壳。尝试运行却失败。搜索资料发现,这是UPX在脱壳时原程序的重定位表丢失导致的。PE文件默认使用动态基址,所以没有重定位表会导致DLL文件等加载到内存的位置出现问题,导致程序无法正常运行。所以我们使用StudyPe将文件使用动态基址改为使用固定基址即可。(也可以不借助pe工具,直接010Editor打开文件,将Dllcharacteristics结构体中的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE的值从1修改成0)尝然后双击程序,发现可以正常运行。

将文件放入IDA中分析,通过字符串查找锁定主函数。分析程序,代码有点长,而且是拿Go语言写的,直接静态分析不是很好分析,我们动态调试着分析。首先是对一堆值得初始化,接着input输入,然后有一段判断代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if ( !v17 )
{
v3 = v35;
v4 = 0LL;
v5 = 0LL;
while ( v4 < 9 )
{
for ( i = 0LL; i < 9; ++i )
{
v1 = (__int64 *)v40[3 * v4 - 1];
if ( v40[3 * v4] <= (unsigned __int64)i )
sub_466740((__int64)v11, (__int64)v12);
v0 = *((unsigned __int8 *)v1 + i);
if ( !(_BYTE)v0 )
{
v7 = v3[1];
if ( v7 <= v5 )
goto LABEL_13;
if ( v7 <= (unsigned __int64)v5 )
sub_466740((__int64)v11, (__int64)v12);
v8 = *(unsigned __int8 *)(v5 + *v3);
if ( (unsigned __int8)v8 < 0x30u || (unsigned __int8)v8 > 0x39u )
goto LABEL_13;
v0 = (unsigned int)(v8 - 48);
*((_BYTE *)v1 + i) = v0;
++v5;
}
}
++v4;
}

通过读内存,知道v3存储的就是我们输入到的那片内存的地址。v40是之前初始化过的变量,我们查看内存。v40为_int64类型数据,它的值为一段内存地址+两个9的格式,举个例子如:0x000000C000121E1F,0x0000000000000009,0x0000000000000009这样。v40[3 * v4 - 1]就是取得每第三个值,也就是那个内存地址里的值。接着,v0在一个字节一个字节地读那个地址里地数据,每个地址读9个字节。如果为零,则先对输入的值进行判断,将输入的范围控制在0-9。满足条件则写入替换那个零值。这里即为数独的特征。紧接着是一个判断语句,如果满足判断条件即可进入到输出flag的步骤里。

1
if ( v3[1] == v5 && (v15 = sub_4B8720((__int64)&v39, 9LL, 9, v13[8]), v13[8]) )

v3[1]通过读内存可以知道是存储的输入字符串的长度。v5为填入数据的个数,也就是那些内存地址里零的个数。把零理解为数独的填空即是输入要把空填满的判断。然后是函数sub_4B8720传入的参数是数独的9*9的表,和一个值为9的int。在这个函数里面,对数组游戏规则进行判定,若满足则返回一。如上我们可以梳理出整个程序的结构:首先对变量初始化,然后让用户输入,对输入的值进行判断,符合规则的填入数独矩阵的0值。然后对空是否填满和数独是否获胜进行判断,若满足数独获胜条件,则进行下面的flag输出。所以,我们将内存中数独的数据读取下来:

1
2
3
4
5
6
7
8
9
v40=[4,0,0,0,0,9,0,0,8,
0,0,1,0,2,0,0,0,6,
0,2,5,0,0,0,0,3,0,
0,0,0,0,1,0,0,8,0,
0,5,0,7,0,6,1,0,0,
2,0,0,0,9,0,0,0,0,
0,0,0,0,8,0,2,0,7,
0,0,4,6,0,0,0,0,0,
7,6,0,0,0,0,8,0,0]

我们使用在线解数独工具完成此数独得到正确输入:76135283549798674164925733849217386455934161872359295314

将其输入则得到flag。

analgo

flag{82fb373061ad9c8bfcee217d25e0bbc8}

拿到题目为exe文件,exeinfope查壳为无壳64位可执行文件。分析程序,if ( v21[1] == 42 )是对输入长度的判断,看到最具特征的就是>>>>><<>>>><[[[.]+]<[-.+.].+.]].][.][[.+.]>[.<-.]<.][.][[.-,.<>>>>>>><<<<.].][[.[.]<.][.].-]].[.]字符串,这很明显是brainfuck语言。而后的**main_anal()*函数对brainfuck*语言进行解释,然后对我们的输入进行加密。加密完成以后对加密字符串进行长度比较和与目标字符串比较:if ( v7 == 42 ? runtime_memequal() : 0 )。通过动态调试,发现比较数据其实就是在函数*main_anal()***后赋值的一堆变量。对于这种brainfuck解释器的题目,直接分析解释代码相对而言比较困难,所以我们先尝试看看数据能否进行爆破。

首先我们尝试‘flag’加上‘a’去填充42个字节,然后观察加密后的结果并提取比较数据两个相比较:

加密数据:38DDB37273B6F53877BAF93C7BBEFD407FC2014483C6054887CA094C8BCE0D508FD2115493D6155897AF

比较数据:38DDB3E82117069F09A2568BC8940D12F37EE677382D0C8CDE13F14CAF7B5002CA8B23A0F5F9661B2797

可以看到我们正确输入前四个字节,然后加密以后前三字节的数据与比较数据相同。我们继续缩小正确的数据的大小(’fla’)

加密数据:38DDF13473B6F53877BAF93C7BBEFD407FC2014483C6054887CA094C8BCE0D508FD2115493D6155897AF

比较数据:38DDB3E82117069F09A2568BC8940D12F37EE677382D0C8CDE13F14CAF7B5002CA8B23A0F5F9661B2797

继续缩小正确数据大小也是这样,密文正确的字节数永远比明文正确的字节数少一,且加密顺序从左到右。其实到此我们就可以看出加密算法没有涉及扩散和混淆,所以我们可以在此题尝试爆破,但是爆破至少从第二个字节开始。

我认为爆破的切入点在于代码if ( v7 == 42 ? runtime_memequal() : 0 )处。*runtime_memequal()*是字符串比较函数,我们先看这个函数开始的代码:

1
2
3
4
5
runtime_memequal proc near
rax, rbx
short loc_9826AD
mov rax, 1
retn
1
2
3
4
5
6
loc_9826AD:
rsi, rax
rdi, rbx
rbx, rcx
memeqbody
runtime_memequal endp

通过读内存或者分析代码我们可以知道,rax存储我们的密文,rbx存储着比较字符串。但这个函数并不是单纯的整体比较字符串,这里他有一个判断的跳转。为零说明两字符串相等,那么直接给rax寄存器赋一返回了,在后面会用test检测寄存器值是否为零,为零则直接输出’Right!’了。但如果比较结果非零值得话则会进行另外的操作。GO语言的新版调用约定为在参数不超过9个时采用其规定的寄存器传递参数。这里即是三步参数传递操作,前两步是传递目的操作数,源操作数,二第三步传递的则是比较长度,这个我们可以往前寻找rcx:cmp rcx, 2Ah,也就是之前对长度的比较,但其实程序要想走到这一步就得先通过在输入后很快就进行的长度判断,所以其实这里rcx的值其实就是42,且这一步比较其实没有什么意义,所以这一行代码也就成了我们patch的切入点。,这个我们下文再提。这个memeqbody,还是挺有意思的,经过分析后得知他的大致逻辑是先对比较长度与8进行判断,因为rsi,rdi这些r开头的寄存器是64位寄存器,他们同时最多对八个字节的数据进行操作,小于八是寄存器总体比较完成以后将结果左移8减去比较长度的位数,若大于八则八字节八字节地比较,若是最后不足八字节,则将前一次比较地后8减去最后剩下地字节数位与最后剩下地字节凑成8位进行比较。同样地,不论哪一种比较

将目的操作数,源操作数,比较长度传递进函数,其实就给了我们不用输入全部正确的明文就可以获得正确反馈的可能。所以我们需要patch代码来给rcx赋上我们需要的比较长度。

原patch处代码:

1
2
3
4
5
6
7
8
9
10
			cmp     rcx, 2Ah ; '*'
jz short loc_773A4C
xor eax, eax
jmp short loc_773A59
loc_A33A5D:
mov rax, [rsp+168h+var_48]
call runtime_memequal
loc_773A59:
test al, al
jnz short loc_773AB7

patch后:

1
2
3
4
5
6
7
8
mov     rcx, 3
nop
nop
nop
mov rax, [rsp+168h+var_48]
call runtime_memequal
test al, al
jnz short loc_773AB7

然后我们写脚本爆破:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import subprocess
import string

Blast_data=list('flag11111111111111111111111111111111111111\n')
for i in range(4,42):
exe_bin=bytearray(open("D:\\ctf\\analgo.exe",'rb').read())
# print(exe_bin)
exe_bin[0xb3045]=i
open("D:\\ctf\\analgo.exe",'wb').write(exe_bin)
for j in string.printable:
# print(Blast_data)
Blast_data[i]=j
pipe=subprocess.Popen("D:\\ctf\\analgo",bufsize=5,stdout=subprocess.PIPE,stdin=subprocess.PIPE)
pipe.stdin.write(bytes("".join(Blast_data).encode("ascii")))
if b'right!\n'==pipe.stdout.read():
break
print("".join(Blast_data).encode("utf-8"))

注:

mov指令源操作数的地址由010Editor读pe文件的.text节区的节表信息然后查找得到。

检测我们每字节爆破成功条件的字符串为’right!\n’,因为GO语言的fmt.Println函数是输出并且换行

爆破完成也就得到了我们的flag ^ ^

Crakeme 3

拿到apk文件,用jadx打开,找到MainActivity进行分析,可以看到关键函数:

1
2
3
4
5
6
public /* synthetic */ void m0lambda$onCreate$0$comctfcrackme3MainActivity(EditText editText, View view) {
String obj = editText.getText().toString();
if (!obj.startsWith("flag{") || !obj.endsWith("}") || obj.length() <= 6 || obj.length() >= 39) {
return;
}
Toast.makeText(this, check(obj.substring(5, obj.length() - 1)), 0).show();

先对输入的格式进行检测判断(长度、格式),然后传入check函数中。直接对该函数进行搜索却没有结果,说明是动态注册的,在执行System.loadLibrary,会首先执行C组件里的*JNI_OnLoad()*函数,来实现告知VM当前so库所使用的JNI版本;对数据初始化;对Java类中的native函数进行注册等。

所以我们使用IDA反编译so文件,搜索*JNI_OnLoad()*:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( !v1 )
{
v3 = (*(*v5[0] + 48LL))(v5[0], "com/ctf/crackme3/MainActivity");
if ( v3 )
{
v4 = (*(*v5[0] + 1720LL))(v5[0], v3, off_724E0, 1LL);
result = 65542LL;
if ( v4 >= 0 )
return result;
puts("register native method failed!");
}
else
{
printf("cannot get class:%s\n", "com/ctf/crackme3/MainActivity");
}

查看off_724E0地址处内存,可知道这里动态注册了名为check的函数,反编译该函数进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ( sub_3D70(v3) == 32 )
{
v10 = 16;
v4 = malloc(0x80uLL);
if ( sub_4330(v3, v4, &v10) )
{
v5 = *a1;
v6 = "Bad format";
}
else
{
v9 = *v4 ^ 0x39DEB332;
v11 = v4[1] ^ v9;
v12 = v4[2] ^ v11;
v8 = v4[3] ^ v12;
if ( sub_43F0(&v9) && sub_44E0(&v11) && sub_4630(&v8) )
{
v5 = *a1;
v6 = "right flag!!!";
}

if ( sub_3D70(v3) == 32 ),32字节输入。if ( sub_4330(v3, v4, &v10) )将输入分为4组每组8字节。有后面的四组异或可以猜测此处将输入每两字节的16进制当作一个新的字节化为了四组四字节数据。然后进行异或后传入三个加密函数进行加密。

首先我们尝试使用Findcrypto直接确定其加密方式。发现sub_43F0被识别为SHA1加密,代码中直接给出了明文第三个字节的数据,已经减小了爆破的运算量,我们提取他的比较数据进行数据爆破拿到第一组明文:

1
2
3
4
5
6
7
8
9
10
11
import hashlib

compare_data='2e60865e94119223c4657eac98d744ee909e66b8'
arr=[]
# print(len(compare_data))
for index1 in range(0xff):
for index2 in range(0xff):
for index3 in range(0xff):
blast_arr=[index1,index2,0xe9,index3]
if hashlib.sha1(bytes(blast_arr)).hexdigest().lower() == compare_data:
print("%02x"%index1,"%02x"%index2,'e9',"%02x"%index3)

一直有疑惑为什么四组数据却只用了三组加密,然后分析第二个加密函数便明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
key = 0;
v5 = 0x807060504030201LL;
v6 = 0xC0B0A09;
v7 = 0xE0D;
v8 = 0xF;
(generate_key)(generated_key, &key);
len = 16LL;
generated_data = 0LL;
text_init(generated_key, a1, 8LL, &generated_data, &len);
SM4_Cipher(generated_key, &generated_data, &len);
(nullsub_2)(&generated_data, 16LL);
return generated_data == 0xD4
&& *(&generated_data + 1) == 0x76D9
&& *(&generated_data + 3) == 0x9EFD8CED
&& *(&generated_data + 7) == 0xE63A562039CF7536LL
&& HIBYTE(generated_data) == 0xBF;

sub_44E0(&v11)传入八字节数据,在密钥初始化的函数中找到了SM4加密中,轮密钥生成步骤中的系统参数和字节替换步骤中的Sbox,判断其为SM4加密,并且可以在text_init函数中发现,给*(a1+160)设置成了0-f并且现行与输入异或。可以得知此为CBC模式的SM4加密,初始向量iv为0-f。明确于此,我们使用在线工具求解此SM4加密:(网站需要将数据转成base64)

1
2
3
4
5
6
7
8
9
10
11
12
import base64
key = []
iv = []
for i in range(16):
key.append(i)
iv.append(i)
cmp = [0xD4, 0xD9, 0x76, 0xED, 0x8C, 0xFD, 0x9E, 0x36, 0x75, 0xCF, 0x39, 0x20, 0x56, 0x3A, 0xE6, 0xBF]
print(base64.b64encode(bytes(key)))
print(base64.b64encode(bytes(iv)))
print(base64.b64encode(bytes(cmp)))
# http://lzltool.com/SM4在线解密得到, 0xD1E575822D0B54FF
# D1E57582 2D0B54FF

接下来我们看最后一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool __fastcall sub_4630(unsigned __int8 *a1)
{
int v2; // [rsp+Ch] [rbp-FCh] BYREF
char v3[112]; // [rsp+10h] [rbp-F8h] BYREF
int v4[24]; // [rsp+80h] [rbp-88h] BYREF
unsigned __int64 v5; // [rsp+E0h] [rbp-28h]

v5 = __readfsqword(0x28u);
v2 = 100;
sub_8960(v4);
sub_8980(v4, a1, 4, v3, &v2);
sub_8DB0(v4, v3, &v2);
return v3[0] == 'c'
&& v3[1] == 'p'
&& v3[2] == 'V'
&& v3[3] == 'V'
&& v3[4] == 'n'
&& v3[5] == 'W'
&& v3[6] == '='
&& v3[7] == '=';
}

从比较数据我们就基本可以大致猜测出是一个base64有关的加密,我们进入函数捕捉特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while ( 1 )
{
v18 = *(v17 - 1) << 16;
if ( v14 <= 2 )
break;
v19 = *v17 << 8;
v20 = v17[1];
a4[v13] = aAbcdefghijklmn[*(v17 - 1) >> 2];
a4[v13 + 1] = aAbcdefghijklmn[((v19 | v18) >> 12) & 0x3F];
a4[v13 + 2] = aAbcdefghijklmn[((v20 | v19) >> 6) & 0x3F];
a4[v13 + 3] = aAbcdefghijklmn[v20 & 0x3F];
v13 += 4LL;
v17 += 3;
v21 = (v14 - 3 < 0) ^ __OFADD__(-3, v14) | (v14 == 3);
v14 -= 3;
if ( v21 )
{
v15 = &a4[v13];
v10 = v16;
goto LABEL_16;
}

abcdefghijklmnpoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/并且提取出table。到此为止,我们明确此为一个很经典的base64变表加密。我们提取出数据,直接写出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
import base64
import string

str1 = "cpVVnW=="

string1 = "abcdefghijklmnpoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
arr=[]
str=base64.b64decode(str1.translate(str.maketrans(string1,string2)))
for i in str:
arr.append(hex(i))
print(arr)

最后得到的三组数据,根据题目里的异或条件异或回去,并且转换成小段字节序即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
one = 0x09e948b0
two_0 = 0x8275e5d1
two_1 = 0xff540b2d
three = 0x37efeb08
flag0 = hex(one ^ 0x39DEB332)[2:]
print(flag0)

flag1 = hex(two_0 ^ one)[2:]
print(flag1)

flag2 = hex(two_1 ^ two_0)[2:]
print(flag2)

flag3 = hex(two_1 ^ three)[2:]
print(flag3)

# 82fb373061ad9c8bfcee217d25e0bbc8

flag{82fb373061ad9c8bfcee217d25e0bbc8}

Crakeme 4

同Crakeme3为一道apk逆向题。MainActivity中的结构与上上题类似。对输入格式和长度的检测,并将除格式字符串外的字符串传入check函数。直接搜索java开头的native函数,并无结果。由上题积攒的经验,猜测此函数在JNI_Onload中动态加载。我们IDA打开so文件,反编译JNI_Onload函数,我们可以看到动态加载check函数的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ( v6 == 188431884 )
{
v15 = main_main;
v14 = *(_OWORD *)&off_38D40;
if ( (*(int (__fastcall **)(__int64, __int64, __int128 *, __int64))(*(_QWORD *)v13 + 1720LL))(
v13,
v12,
&v14,
1LL) >= 0 )
v6 = 1424746346;
else
v6 = 206631466;
goto LABEL_5;
}
v5 = -1;
v6 = -944928091;
}

我们可以发现这个v13+1720应该是一个函数,前面还涉及到*__fastcall这种调用约定。我们尝试修复该函数,将数据类型改为JNIEnv

if ( (*v13)->RegisterNatives(v13, (jclass)v12, (const JNINativeMethod *)&v14, 1LL) >= 0 )

发现可以看到用于动态注册的RegisterNatives的JNI函数,即代表我们修复成功。接下来我们分析主函数,其中也有一些未修复的JNI函数,我们同样的对他们进行修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
__int64 __fastcall main_main(JNIEnv *a1, __int64 a2, __int64 a3)
{
int v6; // w24
unsigned __int64 v7; // x0
int v8; // w3
int v9; // w4
int v10; // w5
int v11; // w6
void *v12; // x7
unsigned __int8 *v13; // x22
int v14; // w8
int v15; // w15
int v16; // w16
__int64 v17; // x0
__int64 v18; // x19
int v20; // w8
jclass v21; // x0
jmethodID v22; // x0
jclass v23; // [xsp+8h] [xbp-1108h]
__int64 blowfish_key; // [xsp+10h] [xbp-1100h] BYREF
__int64 v25[2]; // [xsp+18h] [xbp-10F8h] BYREF
void *ptr; // [xsp+28h] [xbp-10E8h]
__int128 v27; // [xsp+30h] [xbp-10E0h] BYREF
__int64 (__fastcall *v28)(__int64, __int64, __int64); // [xsp+40h] [xbp-10D0h]
int generat_key[1042]; // [xsp+48h] [xbp-10C8h] BYREF
char generat_text[8]; // [xsp+1090h] [xbp-80h] BYREF
int to_be_encrypted_text[4]; // [xsp+1098h] [xbp-78h] BYREF
__int64 v32; // [xsp+10A8h] [xbp-68h]

v6 = 1945572552;
v32 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v25[1] = 0LL;
ptr = 0LL;
v25[0] = 0LL;
v7 = strlen(a5);
sub_14FE0((char *)v25, a5, v7, v8, v9, v10, v11, v12);
v13 = (unsigned __int8 *)(*a1)->GetStringUTFChars(a1, a3, 0LL);
sub_13FC8(v13, to_be_encrypted_text);
blowfish_key = 0xFFDEBC9A78563412LL;
init_and_cipher((__int64)&blowfish_key, (__int64)generat_key, 8uLL);
BlowFish_Cipher((unsigned int *)to_be_encrypted_text, generat_text, generat_key);
if ( (unsigned __int8)generat_text[0] != 0x92
|| (unsigned __int8)generat_text[1] != 0xB1
|| (unsigned __int8)generat_text[2] != 0xC0
|| (unsigned __int8)generat_text[3] != 0x8D
|| generat_text[4] != 0x7D
|| generat_text[5] != 0x4D
|| generat_text[6] != 2
|| (unsigned __int8)generat_text[7] != 0xB6 )
{
(*a1)->ReleaseStringUTFChars(a1, (jstring)a3, (const char *)v13);
if ( (v25[0] & 1) != 0 )
v15 = 0x1EE16766;
else
v15 = 0xD5748B91;
v16 = 0x9D4E379;
while ( 1 )
{
while ( v16 <= 0x9D4E378 )
{
LABEL_15:
if ( v16 == 0xD2D01EF3 )
{
v17 = ((__int64 (__fastcall *)(JNIEnv *))(*a1)->NewStringUTF)(a1);
goto LABEL_24;
}
if ( v16 == 0xD5748B91 )
goto LABEL_17;
}
while ( v16 == 164946809 )
{
v16 = v15;
if ( v15 <= 0x9D4E378 )
goto LABEL_15;
}
if ( v16 == 0x1EE16766 )
{
LABEL_17:
v16 = 0xD2D01EF3;
goto LABEL_15;
}
}
}
(*a1)->ReleaseStringUTFChars(a1, (jstring)a3, (const char *)v13);
v27 = *(_OWORD *)&off_38D28;
v28 = DES;
v23 = (*a1)->FindClass(a1, byte_3C020);
v14 = -1439435635;
while ( v14 == -1439435635 )
{
if ( v23 )
v14 = -781376839;
else
v14 = -359426064;
}
if ( v14 != -781376839 )
goto LABEL_32;
if ( (*a1)->RegisterNatives(a1, v23, (const JNINativeMethod *)&v27, 1LL) >= 0 )
v20 = 670644798;
else
v20 = 82564779;
while ( v20 == 82564779 || v20 == 670644798 )
LABEL_32:
v20 = 715000375;
v21 = (*a1)->GetObjectClass(a1, a2);
v22 = (*a1)->GetMethodID(a1, v21, aNehnf, &unk_3C0C0);
v17 = sub_14BBC(a1, a2, (__int64)v22, a3);
LABEL_24:
v18 = v17;
while ( v6 != 847613141 )
{
if ( (v25[0] & 1) != 0 )
v6 = 847613141;
else
v6 = -519487832;
if ( v6 == -519487832 )
return v18;
}
operator delete(ptr);
return v18;
}

总体分析下来,可以发现程序其实走了两个加密函数,而第一个加密函数我们使用Findcrypt插件可以很轻易的识别出blowfish加密的常量,确定为blowfish加密。

接下来程序还将进入一个关键函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v5 = 1945572552;
plaintext[1] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v21[1] = 0LL;
ptr = 0LL;
v21[0] = 0LL;
v6 = strlen(a5);
sub_14FE0((char *)v21, a5, v6, v7, v8, v9, v10, v11);
v12 = (unsigned __int8 *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
sub_13FC8(v12, v25);
key = 0xE7CDAB8967452301LL;
key_generation((__int64)&key, (__int64)generated_key, 0, v13, v14, v15, v16);
DES_cipher((__int64)plaintext, ciphertext, (__int64)generated_key);
if ( (unsigned __int8)ciphertext[0] == 0xE4
&& (unsigned __int8)ciphertext[1] == 0xEA
&& (unsigned __int8)ciphertext[2] == 0xC4
&& (unsigned __int8)ciphertext[3] == 0x81
&& (unsigned __int8)ciphertext[4] == 0xB9
&& (unsigned __int8)ciphertext[5] == 0xFE
&& ciphertext[6] == 0x26
&& (unsigned __int8)ciphertext[7] == 0xBC )

其中在key_generation函数中捕捉到了DES加密的常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
v23 = 0LL;
v24 = result;
v9 = v42;
v44 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v43[2] = Circular_displacement_table3;
v43[3] = Circular_displacement_table4;
v43[0] = Circular_displacement_table1;
v43[1] = Circular_displacement_table2;
v10 = PC_1_8;
v42[0] = PC_1_1;
v42[1] = PC_1_2;
v42[6] = PC_1_7;
v42[4] = PC_1_5;
v42[5] = PC_1_6;
v42[2] = PC_1_3;
v42[3] = PC_1_4;
v41[0] = PC_1_8[0];
v41[1] = PC_1_8[1];
v41[6] = PC_1_14;
v41[4] = PC_1_12;
v41[5] = PC_1_13;
v11 = PC_2;
v41[2] = PC_1_8[2];
v41[3] = PC_1_8[3];
v40[2] = PC_2[2];
v40[3] = PC_2[3];
v40[0] = PC_2[0];
v40[1] = PC_2[1];
v40[6] = PC_2[6];
v40[7] = PC_2[7];
v40[4] = PC_2[4];
v40[5] = PC_2[5];
v40[10] = PC_2[10];
v40[11] = PC_2[11];
v40[8] = PC_2[8];
v40[9] = PC_2[9]

如循环位移表,PC置换盒,同样的,我们进入DES_cipher,可以分析确认出DES加密的轮函数,轮加密所使用的S盒等等。确认此为一个标准的DES加密。且上述两个加密都未发现初始向量,等其他加密模式的特征,为最原始的ECB加密模式,据此,我们写出解题代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Cipher import Blowfish
from Crypto.Cipher import DES

Blowfish_key=bytes([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF])
Blowfish_ciphertext=bytes([0x92, 0xB1, 0xC0, 0x8D, 0x7D, 0x4D, 0x02, 0xB6])
Blowfish_cipher=Blowfish.new(Blowfish_key,Blowfish.MODE_ECB)
flag1=Blowfish_cipher.decrypt(Blowfish_ciphertext)
print('flag{',end='')
for i in flag1:
print("{:02x}".format(i),end='')

DES_key=bytes([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xe7])
DES_ciphertext=bytes([0xE4, 0XEA, 0XC4, 0X81, 0xB9, 0xFE, 0x26, 0xBC])
DES_cipher=DES.new(DES_key,DES.MODE_ECB)
flag2=DES_cipher.decrypt(DES_ciphertext)
for i in flag2:
print("{:02x}".format(i),end='')
print('}',end='')

flag{3d1cda6eccd30edae3696515def8d5b9}

小小总结一下这两道apk逆向,自己基础不够扎实,其实还是跟着wp去复现,才知道apk逆向的基本步骤,学到最重要的一点就是,调用c组件里的函数除了通过导出函数直接调用native函数(直接搜索java开头的native函数即可查找),还可以在运行的过程中动态注册。遇到这种情况,就分析JNI_Onload函数,里面大概率会调用RegisterNatives这种JNI函数去动态注册所需函数。IDA对这些JNI函数的识别并不是很理想,很多时候需要我们手动修改数据类型为JNIEnv*来帮助IDA识别函数以辅助我们进一步分析程序。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2022-2023 Syclover.Kama
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信