0%

CTF入门题

CTF入门题

昨天下午在宿舍没有太多事做,就在耍手机,有个朋友说他参加了个CTF比赛,但是逆向的题目不太会,想找我帮忙看看,我正想着反正也没啥事,而且他说参加的是MRCTF,一个青少年的CTF比赛,这类的题目应该是十分钟一个的,并且他给我发的说还是入门题目,我就想着多多少少帮忙看看吧,故事就开始了。

exe逆向

搞exe逆向我自认为已经是快到头了(自认为的),就想着快点搞完,他就给我发过来了,用查壳的一查是个UPX的,手头没有脱壳机,我就手拖一下吧。
exe
可以看到是一个大跳转,然后一直跟下去,发现貌似是加花混淆了的,而且处在ntdll里面,那么就没啥好说的了,直接无脑到返回,这个时候OD一直显示在nt,直到出现打印了一个提示信息:

1
2
Welcome to MRCTF 2021, wish you can catch the flag
Please input your flag:

exe
然后整个程序就看到了00401751这个应该就是入口,直接就可以拖出来了,没啥太大的难度,然后想了一下是算法题,就直接用IDA打开就好,用OD反倒是没啥意思。
然后进去看main函数,大致上是这样子的:

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
int start()
{
char v1; // [esp+14h] [ebp-53Ch]
char v2; // [esp+15h] [ebp-53Bh]
char v3; // [esp+16h] [ebp-53Ah]
char v4; // [esp+17h] [ebp-539h]
char v5; // [esp+18h] [ebp-538h]
char v6; // [esp+19h] [ebp-537h]
char v7; // [esp+1Ah] [ebp-536h]
char v8; // [esp+1Bh] [ebp-535h]
char v9; // [esp+1Ch] [ebp-534h]
char v10; // [esp+1Dh] [ebp-533h]
char v11; // [esp+1Eh] [ebp-532h]
char v12; // [esp+1Fh] [ebp-531h]
char v13; // [esp+20h] [ebp-530h]
char v14; // [esp+21h] [ebp-52Fh]
char v15; // [esp+22h] [ebp-52Eh]
char v16; // [esp+23h] [ebp-52Dh]
char v17; // [esp+24h] [ebp-52Ch]
char v18; // [esp+25h] [ebp-52Bh]
char v19; // [esp+26h] [ebp-52Ah]
char v20; // [esp+27h] [ebp-529h]
char v21; // [esp+28h] [ebp-528h]
char v22; // [esp+29h] [ebp-527h]
char v23; // [esp+2Ah] [ebp-526h]
char v24; // [esp+2Bh] [ebp-525h]
char v25; // [esp+2Ch] [ebp-524h]
char v26; // [esp+2Dh] [ebp-523h]
char v27; // [esp+2Eh] [ebp-522h]
char v28; // [esp+2Fh] [ebp-521h]
char v29; // [esp+30h] [ebp-520h]
char v30; // [esp+31h] [ebp-51Fh]
char v31; // [esp+32h] [ebp-51Eh]
char v32; // [esp+33h] [ebp-51Dh]
char v33; // [esp+34h] [ebp-51Ch]
char v34; // [esp+35h] [ebp-51Bh]
char v35; // [esp+36h] [ebp-51Ah]
char v36; // [esp+37h] [ebp-519h]
char v37; // [esp+38h] [ebp-518h]
char v38; // [esp+39h] [ebp-517h]
char v39; // [esp+3Ah] [ebp-516h]
char v40; // [esp+3Bh] [ebp-515h]
char v41; // [esp+3Ch] [ebp-514h]
char v42; // [esp+3Dh] [ebp-513h]
char v43; // [esp+3Eh] [ebp-512h]
char v44; // [esp+3Fh] [ebp-511h]
char Str; // [esp+40h] [ebp-510h]
char v46[1024]; // [esp+140h] [ebp-410h]
unsigned int v47; // [esp+540h] [ebp-10h]
size_t v48; // [esp+544h] [ebp-Ch]
char *v49; // [esp+548h] [ebp-8h]
unsigned int i; // [esp+54Ch] [ebp-4h]

sub_402290();
puts("Welcome to MRCTF 2021, wish you can catch the flag");
printf("Please input your flag:");
scanf("%s", &Str);
v49 = &Str;
v48 = strlen(&Str);
v47 = sub_401500(v49, v48, v46);
v46[v47] = 0;
v1 = 116;
v2 = 118;
v3 = 106;
v4 = 100;
v5 = 118;
v6 = 101;
v7 = 122;
v8 = 55;
v9 = 68;
v10 = 48;
v11 = 118;
v12 = 83;
v13 = 121;
v14 = 90;
v15 = 98;
v16 = 110;
v17 = 122;
v18 = 118;
v19 = 57;
v20 = 48;
v21 = 109;
v22 = 102;
v23 = 57;
v24 = 110;
v25 = 117;
v26 = 75;
v27 = 110;
v28 = 117;
v29 = 114;
v30 = 76;
v31 = 56;
v32 = 89;
v33 = 66;
v34 = 90;
v35 = 105;
v36 = 88;
v37 = 105;
v38 = 115;
v39 = 101;
v40 = 72;
v41 = 70;
v42 = 113;
v43 = 61;
v44 = 61;
for ( i = 0; i < v47; ++i )
{
if ( v46[i] != *(&v1 + i) )
{
printf("Sorry, plz try again");
break;
}
}
printf("Jesus, You are so handsome!!");
return 0;
}

很明显的给出了一个加密之后的字符,但是出题人为了防止搜索字符串时候直接看到加密的内容,用这种比较麻烦的方法进行了小的保护,将那一长串的赋值进行编码之后是这样子的一个东西:

1
tvjdvez7D0vSyZbozv90mf9nuKnurL8YBZiXiseHFq==

这个让谁都会想到是base64,然后去网站解码一下发现出错了,这个时候我的傻子行为就开始了,简单阅读这段代码,发现sub_401500这个函数是我们的加密函数,就需要跟进去,然后代码就能看到了:

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
int __cdecl sub_401500(int a1, unsigned int a2, int a3)
{
int v3; // eax
int v4; // ST08_4
int v5; // eax
int v6; // eax
int v7; // eax
int v8; // ST08_4
int v9; // eax
int v10; // eax
int v11; // eax
int v12; // ST08_4
int v13; // eax
int v15; // [esp+8h] [ebp-Ch]
int v16; // [esp+8h] [ebp-Ch]
unsigned int v17; // [esp+Ch] [ebp-8h]

v17 = 0;
v15 = 0;
while ( v17 + 3 <= a2 )
{
v3 = v15;
v4 = v15 + 1;
*(_BYTE *)(v3 + a3) = byte_403040[(signed int)*(unsigned __int8 *)(a1 + v17) >> 2];
v5 = v4++;
*(_BYTE *)(v5 + a3) = byte_403040[16 * *(_BYTE *)(a1 + v17) & 0x30 | ((signed int)*(unsigned __int8 *)(v17 + 1 + a1) >> 4)];
*(_BYTE *)(v4 + a3) = byte_403040[4 * *(_BYTE *)(v17 + 1 + a1) & 0x3C | ((signed int)*(unsigned __int8 *)(v17 + 2 + a1) >> 6)];
v6 = v4 + 1;
v15 = v4 + 2;
*(_BYTE *)(v6 + a3) = byte_403040[*(_BYTE *)(v17 + 2 + a1) & 0x3F];
v17 += 3;
}
if ( v17 < a2 )
{
if ( a2 - v17 == 1 )
{
v7 = v15;
v8 = v15 + 1;
*(_BYTE *)(v7 + a3) = byte_403040[(signed int)*(unsigned __int8 *)(a1 + v17) >> 2];
*(_BYTE *)(v8 + a3) = byte_403040[16 * *(_BYTE *)(a1 + v17) & 0x30];
v9 = v8 + 1;
v16 = v8 + 2;
*(_BYTE *)(a3 + v9) = 61;
}
else
{
v11 = v15;
v12 = v15 + 1;
*(_BYTE *)(v11 + a3) = byte_403040[(signed int)*(unsigned __int8 *)(a1 + v17) >> 2];
*(_BYTE *)(v12 + a3) = byte_403040[16 * *(_BYTE *)(a1 + v17) & 0x30 | ((signed int)*(unsigned __int8 *)(v17 + 1 + a1) >> 4)];
v13 = v12 + 1;
v16 = v12 + 2;
*(_BYTE *)(v13 + a3) = byte_403040[4 * *(_BYTE *)(v17 + 1 + a1) & 0x3C];
}
v10 = v16;
v15 = v16 + 1;
*(_BYTE *)(a3 + v10) = 61;
}
return v15;
}

然后我就开始分析了:

1
2
3
4
5
6
7
*(_BYTE *)(v3 + a3) = byte_403040[(signed int)*(unsigned __int8 *)(a1 + v17) >> 2];
v5 = v4++;
*(_BYTE *)(v5 + a3) = byte_403040[16 * *(_BYTE *)(a1 + v17) & 0x30 | ((signed int)*(unsigned __int8 *)(v17 + 1 + a1) >> 4)];
*(_BYTE *)(v4 + a3) = byte_403040[4 * *(_BYTE *)(v17 + 1 + a1) & 0x3C | ((signed int)*(unsigned __int8 *)(v17 + 2 + a1) >> 6)];
v6 = v4 + 1;
v15 = v4 + 2;
*(_BYTE *)(v6 + a3) = byte_403040[*(_BYTE *)(v17 + 2 + a1) & 0x3F];

这段是将三个字符转化为4个字符,然后进行了一些位值操作,我就用C复现了一下:

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
#include <stdio.h>
int one(int x){
return x<<2;
}
int two (int x,int y){
for(int i=1;i<64;i++){
int r = (((y * 16) & 0x30) | (i >> 4));
//printf("r:%d\n",r);
if(r == x){
return i;
}
}
return -1;
}
int main(){
char a[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
printf("%d\n",one(52));
printf("%d\n",two(3,one(52)));
printf("%d\n",0x31 >> 2);
printf("%d\n",((0x34 * 16) & 0x30) | (0x35 >> 4));
printf("%d\n",((0x32 * 4) & 0x3c) | (0x33 >> 6));
printf("%d\n",0x33 & 0x3F);
printf("%c\n",a[0x31 >> 2]);
printf("%c\n",a[((0x31 * 16) & 0x30) | (0x32 >> 4)]);
printf("%c\n",a[((0x32 * 4) & 0x3c) | (0x33 >> 6)]);
printf("%c",a[0x33 & 0x3F]);
return 0;
}

读这段加密函数的时候里面的byte_403040就是我上面的a字符串,到这个时候我还没有想到要换码表,只是单纯的想把整个算法全部解出来。
我发现每三个一组,第二个值依靠于第三个值,第一个值只是一个移位,而且第二个值比较麻烦,我只能用到枚举方法进行解密,所以就有了我上面的two函数,然后手动枚举的时候发现出错了,然后测试了一个小时还是不对,到这个地方我就有点烦躁了,想一个签到题怎么会分析这么一个加密函数,会不会是一个成品的加密算法我不知道,到这个时候我还没想到是换表的base64,但是真的搞烦了,我就没继续做,晚上睡觉的时候觉得签到题没做出来真的太丢脸了,就又想了一下,后面有两个等号会是啥,突然想到应该是换表了,然后第二天就把他解出来了,确实好久没做了,感觉脑子有点跟不上了。
带一个网上搜索的换表代码吧:

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
# coding:utf-8

s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"
def My_base64_encode(inputs):
# 将字符串转化为2进制
bin_str = []
for i in inputs:
x = str(bin(ord(i))).replace('0b', '')
bin_str.append('{:0>8}'.format(x))
#print(bin_str)
# 输出的字符串
outputs = ""
# 不够三倍数,需补齐的次数
nums = 0
while bin_str:
#每次取三个字符的二进制
temp_list = bin_str[:3]
if(len(temp_list) != 3):
nums = 3 - len(temp_list)
while len(temp_list) < 3:
temp_list += ['0' * 8]
temp_str = "".join(temp_list)
#print(temp_str)
# 将三个8字节的二进制转换为4个十进制
temp_str_list = []
for i in range(0,4):
temp_str_list.append(int(temp_str[i*6:(i+1)*6],2))
#print(temp_str_list)
if nums:
temp_str_list = temp_str_list[0:4 - nums]

for i in temp_str_list:
outputs += s[i]
bin_str = bin_str[3:]
outputs += nums * '='
print("Encrypted String:\n%s "%outputs)

def My_base64_decode(inputs):
# 将字符串转化为2进制
bin_str = []
for i in inputs:
if i != '=':
x = str(bin(s.index(i))).replace('0b', '')
bin_str.append('{:0>6}'.format(x))
#print(bin_str)
# 输出的字符串
outputs = ""
nums = inputs.count('=')
while bin_str:
temp_list = bin_str[:4]
temp_str = "".join(temp_list)
#print(temp_str)
# 补足8位字节
if(len(temp_str) % 8 != 0):
temp_str = temp_str[0:-1 * nums * 2]
# 将四个6字节的二进制转换为三个字符
for i in range(0,int(len(temp_str) / 8)):
outputs += chr(int(temp_str[i*8:(i+1)*8],2))
bin_str = bin_str[4:]
print("Decrypted String:\n%s "%outputs)

print()
print(" *************************************")
print(" * (1)encode (2)decode *")
print(" *************************************")
print()


num = input("Please select the operation you want to perform:\n")
if(num == "1"):
input_str = input("Please enter a string that needs to be encrypted: \n")
My_base64_encode(input_str)
else:
input_str = input("Please enter a string that needs to be decrypted: \n")
My_base64_decode(input_str)

安卓入门题

这个题目其实是在我没想到换表base64的时候他就发给我了,主要是我觉得入签到题没做出来有点丢脸,我就再要一个找回点面子,他就给我发来了安卓的一个签到题,用jadx打开看了一下,非常好,没有加壳,没有混淆,反调试我没看,但是应该也是没有的,那么这个题目就应该手到擒来了。

我用我的模拟器安装都提示失败了,其实实际上是我的安卓版本不够,他需要的sdk版本应该比较高,但是我又懒得再下载一个新的模拟器了,所以就直接静态分析吧,进来之后看到包很多,找到MainActivity,发现没有什么验证的东西,只是有这么一句代码:editText2.addTextChangedListener(new MainActivity$onCreate$1(this, editText2, button, editText));想了一下应该是输入框输入完之后才会允许点击,那就跟进去这个匿名内部类看一下:

1
2
3
4
5
6
7
8
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
if (this.$password.length() == 39) {
this.$button.setEnabled(true);
this.$button.setOnClickListener(new MainActivity$onCreate$1$onTextChanged$1(this));
return;
}
this.$button.setEnabled(false);
}

确实是password等于39的时候才会允许点击按钮,所以说密码是39位,然后找到按钮点击的匿名内部类,看一下里面是啥:

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
public final void onClick(View view) {
EditText editText = this.this$0.$username;
Intrinsics.checkNotNullExpressionValue(editText, "username");
Object obj = editText.getText().toString();
EditText editText2 = this.this$0.$password;
Intrinsics.checkNotNullExpressionValue(editText2, "password");
String obj2 = editText2.getText().toString();
String str = "Wrong pass!";
if (Intrinsics.areEqual(obj, (Object) "MRCTF")) {
String str2 = "null cannot be cast to non-null type java.lang.String";
Objects.requireNonNull(obj2, str2);
String substring = obj2.substring(6, 13);
String str3 = "(this as java.lang.Strin…ing(startIndex, endIndex)";
Intrinsics.checkNotNullExpressionValue(substring, str3);
if (Intrinsics.areEqual(MainActivityKt.encode(substring), (Object) "f5e69f42b7a9a0f1c9edede743a1d27f")) {
if (Intrinsics.areEqual(obj2.subSequence(0, 6), (Object) "MRCTF{") && Intrinsics.areEqual(obj2.subSequence(obj2.length() - 1, obj2.length()), (Object) "}")) {
MainActivity mainActivity = this.this$0.this$0;
int length = obj2.length() - 1;
Objects.requireNonNull(obj2, str2);
str2 = obj2.substring(13, length);
Intrinsics.checkNotNullExpressionValue(str2, str3);
if (mainActivity.check2(str2) == 1) {
Toast.makeText(this.this$0.this$0.getApplicationContext(), "Right pass!", 0).show();
return;
} else {
Toast.makeText(this.this$0.this$0.getApplicationContext(), str, 0).show();
return;
}
}
Toast.makeText(this.this$0.this$0.getApplicationContext(), str, 0).show();
return;
}
}
Toast.makeText(this.this$0.this$0.getApplicationContext(), str, 0).show();
}

然后可以看到,他先是获取了输入框的内容,然后比较了一下username是不是等于MRCTF,说明username是MRCTF,然后继续分析可以看到他截取了6,13的密码的值,是一段很像md5的一个值,是通过encode进行加密的,然后跟进这个encode去看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static final String encode(String str) {
Intrinsics.checkNotNullParameter(str, "plain");
MessageDigest instance = MessageDigest.getInstance("MD5");
Intrinsics.checkNotNullExpressionValue(instance, "MessageDigest.getInstance(\"MD5\")");
Object bytes = str.getBytes(Charsets.UTF_8);
Intrinsics.checkNotNullExpressionValue(bytes, "(this as java.lang.String).getBytes(charset)");
bytes = instance.digest(bytes);
Intrinsics.checkNotNullExpressionValue(bytes, "instance.digest(plain.toByteArray())");
StringBuffer stringBuffer = new StringBuffer();
for (byte b : bytes) {
String toHexString = Integer.toHexString(b & 255);
Intrinsics.checkNotNullExpressionValue(toHexString, "Integer.toHexString(hex)");
if (toHexString.length() < 2) {
toHexString = '0' + toHexString;
}
stringBuffer.append(toHexString);
}
str = stringBuffer.toString();
Intrinsics.checkNotNullExpressionValue(str, "buff.toString()");
return str;
}

这就很明显了,就是一个典型的MD5加密算法,需要去解密一下,这个值,cmd5付费,发现一个somd5不要钱就可以得到结果,那么这个值就解决了。
继续回到onclick里面,发现他又判断两边的内容是不是”MRCTF{“和”}”,两边的值也确定了,然后看到下面还有一个check2的判断方法,截取的是中间部分剩下的,那说明这是最后一个加密了。
跟进去这个check2,发现是这样子的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final int check2(String str) {
Intrinsics.checkNotNullParameter(str, "part2");
int[] iArr = new int[]{110, 82, 89, 87, 86, 95, 6, 94, 105, 71, 80, 92, 83, 4, 93, 85, 111, 65, 93, 111, 124, 98, 115, 100, 118};
rand rand = new rand(srand1, srand2);
int length = str.length();
int i = 0;
int i2 = i;
while (i < length) {
if ((str.charAt(i) ^ StringsKt___StringsKt.first(rand.next(rand.getMemory()))) != iArr[i2]) {
return 0;
}
i2++;
i++;
}
return 1;
}

打眼一看他自己写的类只有两个,一个rand,一个StringsKt___StringsKt,先看rand:

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
public static final class rand {
private long[] memory;

public rand(long j, long j2) {
this.memory = new long[]{j, j2};
}

public final long[] getMemory() {
return this.memory;
}

public final void setMemory(long[] jArr) {
Intrinsics.checkNotNullParameter(jArr, "<set-?>");
this.memory = jArr;
}

public final String next(long[] jArr) {
Intrinsics.checkNotNullParameter(jArr, "memory");
long j = jArr[0];
long j2 = jArr[1];
jArr[0] = j2;
j ^= j << 23;
jArr[1] = ((j >>> 17) ^ (j ^ j2)) ^ (j2 >>> 26);
String valueOf = String.valueOf(jArr[1] + j2);
String str = "null cannot be cast to non-null type java.lang.String";
Objects.requireNonNull(valueOf, str);
Object substring = valueOf.substring(0, 1);
String str2 = "(this as java.lang.Strin…ing(startIndex, endIndex)";
Intrinsics.checkNotNullExpressionValue(substring, str2);
if (Intrinsics.areEqual(substring, (Object) "-")) {
return "0";
}
String valueOf2 = String.valueOf(jArr[1] + j2);
Objects.requireNonNull(valueOf2, str);
valueOf2 = valueOf2.substring(0, 1);
Intrinsics.checkNotNullExpressionValue(valueOf2, str2);
return valueOf2;
}
}

代码很简单,就是将传进来的两个long值进行一顿操作再就没了。这个也是可以直接复制的。
那么继续看StringsKt___StringsKt.first这个方法是啥:

1
2
3
4
5
6
7
public static final char first(CharSequence charSequence) {
Intrinsics.checkNotNullParameter(charSequence, "$this$first");
if ((charSequence.length() == 0 ? 1 : 0) == 0) {
return charSequence.charAt(0);
}
throw new NoSuchElementException("Char sequence is empty.");
}

也是很简单,到此为止我们就解决了所有的未知方法,但是这个check2方法还需要两个long的值,是在MainActivity中进行定义的:

1
2
private static long srand1 = 202120212021L;
private static long srand2 = 114514114514L;

好了,这些东西都解决了,那么就是反推结果了,可以看到check2方法只有满足:

1
2
3
if ((str.charAt(i) ^ StringsKt___StringsKt.first(rand.next(rand.getMemory()))) != iArr[i2]) {
return 0;
}

这个的时候返回0,也就是错误,那么我们就让他不满足,看到是一个异或,那么直接反过来得到str的每个字符,最终我们就可以得到最终的结果,自己写个小脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static long srand1 = 202120212021L;
private static long srand2 = 114514114514L;
public static final char first(CharSequence charSequence) {
if ((charSequence.length() == 0 ? 1 : 0) == 0) {
return charSequence.charAt(0);
}
return 0;
}

public final static int check2() {
int[] iArr = new int[] { 110, 82, 89, 87, 86, 95, 6, 94, 105, 71, 80, 92, 83, 4, 93, 85, 111, 65, 93, 111, 124,
98, 115, 100, 118 };
rand rand = new rand(srand1, srand2);
int i = 0;
int i2 = i;
String out = "";
while (i < iArr.length) {
out += (char)(first(rand.next(rand.getMemory()))^iArr[i2]);
System.out.println(out);
i2++;
i++;
}
return 1;
}

打印出最终的中间部分,正当我觉得大功告成的时候,他和我说flag不对,我觉得判断是没有任何错误的,到此为止已经到十一点了,第二天还要体测,不能睡的太晚,我就没有继续去搞他。
到了第二天,我就回过头来再看了一下这段代码,发现了一个蹊跷,实际上在oncreate方法执行的时候他还执行了一个jni.test(),我没去注意,觉得签到题不应该牵扯到so吧,而且我还忽略了一个问题,就是在MainActivity里面还有一个内部类:

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
public static final class Companion {
private Companion() {
}

public final long getSrand1() {
return MainActivity.srand1;
}

public final void setSrand1(long j) {
MainActivity.srand1 = j;
}

public final long getSrand2() {
return MainActivity.srand2;
}

public final void setSrand2(long j) {
MainActivity.srand2 = j;
}

@JvmStatic
public final int SetSrand(long j, long j2) {
MainActivity.Companion.setSrand1(j);
MainActivity.Companion.setSrand2(j2);
return 0;
}
}

他操作的时候Srand这两个值,这是唯一的在java层操作这个值的一个类,就考虑到可能是这个类修改了这两个值,但是没有任何一个地方调用了这个方法,很有可能是在so中进行的操作,然后我就拖出了so文件,简单分析了一下,里面有一个加密算法,但是看的有点长:

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
signed int __cdecl com_example_mrcheckin_jni_test(_JNIEnv *a1, unsigned __int8 *a2)
{
signed __int32 v2; // eax
__int64 v3; // rax
signed int v4; // ecx
__int64 v5; // kr10_8
unsigned int v6; // esi
int v7; // kr20_4
signed int result; // eax
signed int i; // [esp+1Ch] [ebp-70h]
int v10; // [esp+20h] [ebp-6Ch]
int v11; // [esp+20h] [ebp-6Ch]
unsigned int v12; // [esp+24h] [ebp-68h]
unsigned int v13; // [esp+24h] [ebp-68h]
__int64 v14; // [esp+28h] [ebp-64h]
unsigned __int64 v15; // [esp+38h] [ebp-54h]
unsigned __int64 v16; // [esp+40h] [ebp-4Ch]
__int64 v17; // [esp+48h] [ebp-44h]
int v18; // [esp+50h] [ebp-3Ch]
char v19; // [esp+54h] [ebp-38h]
__int64 v20; // [esp+58h] [ebp-34h]
__int64 v21; // [esp+60h] [ebp-2Ch]
__int64 v22; // [esp+68h] [ebp-24h]
__int64 v23; // [esp+70h] [ebp-1Ch]
int v24; // [esp+78h] [ebp-14h]

v19 = 0;
v18 = 825372722;
if ( !(strlen((const char *)a2) & 7) )
{
v17 = strlen((const char *)a2) >> 3;
v12 = 0;
v10 = 0;
while ( v12 < 4 )
{
v2 = _byteswap_ulong(*(int *)((char *)&v18 + v10));
*((_DWORD *)&v20 + 2 * v12) = v2;
*((_DWORD *)&v20 + 2 * v12++ + 1) = v2 >> 31;
v10 += 4;
}
v13 = 0;
v11 = 0;
while ( v13 < (signed __int64)(unsigned int)v17 )
{
v14 = 0LL;
v16 = (signed int)_byteswap_ulong(*(_DWORD *)&a2[v11]);
v15 = (signed int)_byteswap_ulong(*(_DWORD *)&a2[v11 + (_DWORD)(&dword_A6E4 - 10680)]);
for ( i = 0; i < 34; ++i )
{
v14 += 2654435769LL;
HIDWORD(v3) = ((v21 + __PAIR__(SHIDWORD(v15) >> 5, (unsigned int)(v15 >> 5))) >> 32) ^ ((v14 + v15) >> 32) ^ ((v20 + 16 * v15) >> 32);
LODWORD(v3) = v16;
v4 = (__PAIR__(HIDWORD(v16), (v21 + (v15 >> 5)) ^ (v14 + v15) ^ (v20 + 16 * v15)) + v3) >> 32;
LODWORD(v3) = ((v21 + (v15 >> 5)) ^ (v14 + v15) ^ (v20 + 16 * v15)) + v16;
v16 = __PAIR__(v4, (unsigned int)v3);
HIDWORD(v3) = v4;
v5 = v22 + 16 * v3;
v6 = (v14 + v3) ^ v5;
v7 = v23 + (__PAIR__((unsigned int)v4, (unsigned int)v3) >> 5);
HIDWORD(v3) = ((v23
+ __PAIR__(
(unsigned int)(v4 >> 5),
(unsigned int)(__PAIR__((unsigned int)v4, (unsigned int)v3) >> 5))) >> 32) ^ ((v14 + __PAIR__((unsigned int)v4, (unsigned int)v3)) >> 32) ^ HIDWORD(v5);
LODWORD(v3) = v15;
v15 = __PAIR__(HIDWORD(v15), v7 ^ v6) + v3;
}
a2[v11] = BYTE3(v16);
a2[v11 + 1] = BYTE2(v16);
a2[v11 + 2] = BYTE1(v16);
a2[v11 + 3] = v16;
a2[v11 + 4] = BYTE3(v15);
a2[v11 + 5] = BYTE2(v15);
a2[v11 + 6] = BYTE1(v15);
a2[v11 + 7] = v15;
++v13;
v11 += 8;
}
}
result = 42720;
if ( _stack_chk_guard != v24 )
JUMPOUT(*(_DWORD *)JNI_OnLoad);
return result;
}

,就想着直接用他的这个so文件导入到我的安卓项目中看看他是否能直接将我的srand进行修改,然后我就得到修改之后的srand,然后我在操作的时候发现执行jni.test()的时候会报错,然后我又跟进so查看,发现它在jni_onload的时候执行了一个函数:

1
2
3
4
_BOOL4 __cdecl sub_6340(_JNIEnv *a1)
{
return sub_7380(a1, "com/example/mrcheckin/jni", (int)&off_B920, 1) != 0;
}

应该是一个根据程序操作的一个方法,然后我就按照程序的名字修改了我的项目进行测试,但是还是会报错,具体什么错误也没仔细去看了,反正解决了一大顿也没解决好,到这里思路终止了,我不能动态调试,属实是太着急人了,所以我就不得不全装一个新版本的安卓模拟器去执行这个程序然后找到被修改之后的srand,我先下载的夜神,安装上一个hook框架之后直接就崩了,我又下载了一个新版本的雷电模拟器,我之前的hook框架在雷电可以运行,所以我就用的雷电模拟器。
一切正常,那个安卓程序也可以安装成功了。
这个时候本想动态调试的,但是想了想还是直接hook吧,这样子会简单一些,现在我们hook的方法有许多:

  1. hook rand类的构造函数,获取传递进来的srand值
  2. hook check2方法,在执行的时候获取到srand值

我选择了后者,其实主要是想偷懒。
那么我就写了一个hook代码:

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
package com.example.msg;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;

import java.lang.reflect.Field;

import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class TestMain implements IXposedHookLoadPackage {
public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
XposedBridge.log(lpparam.packageName);
XposedBridge.log(lpparam.packageName);
if (!lpparam.packageName.equals("com.example.mrcheckin"))
return;
Log.d("wker", "into");
findAndHookMethod("com.example.mrcheckin.MainActivity", lpparam.classLoader, "check2", String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
}

@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Class clazz = param.thisObject.getClass();
XposedBridge.log("LoadClass : "+clazz.getName());
Field[] fields = clazz.getDeclaredFields();
for(int f=0;f < fields.length;f++){
fields[f].setAccessible(true);
XposedBridge.log( fields[f].getName());
if(fields[f].getName().equals("srand1") || fields[f].getName().equals("srand2")){
Long password = (Long) fields[f].get(param.thisObject);
XposedBridge.log(String.valueOf(password));
}
}
}
});
}
}

这里需要注意的是,这个srand是private的,所以我们需要setAccessible进行处理一下。
最终可以在日志中看到:
srand
将这两个值重新放入到我们的脚本中最终可以得到:

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
_
_c
_ch
_che
_chec
_check
_check1
_check1n
_check1n_
_check1n_w
_check1n_we
_check1n_wel
_check1n_welc
_check1n_welc0
_check1n_welc0m
_check1n_welc0me
_check1n_welc0me_
_check1n_welc0me_t
_check1n_welc0me_to
_check1n_welc0me_to_
_check1n_welc0me_to_M
_check1n_welc0me_to_MR
_check1n_welc0me_to_MRC
_check1n_welc0me_to_MRCT
_check1n_welc0me_to_MRCTF

然后最终拼接到我们之前的flag中,最终的flag就出来了。

反思

真的是社会在进步了,思想有点落后了,还把自己高中时候的水平带到现在高中时候的水平,盲目狂妄自大只会让我一步步的掉入坑中,脚踏实地才是我们应该做的。