info 本篇是为了复现了当时做题的步骤总结经验,所以会写的比较详细。如果您水平较高,只想看知识点可以直接跳到后面防止被气到脑溢血。同时本篇内容有错误的地方也请在评论区指出,感谢

不重要的前言

这是我第一次参加强网杯,但是一道题都没做出来,看来还是要继续学习啊!两天啊两天,我一开始以为我可以解出来,但是失败了。后来经过询问和思考终于知道错在哪里了,所以写一篇记录一下解题过程和知识点。

那么开始吧!

查看文件信息

image-20231218091226849

这一步是使用工具对文件头部进行分析,找出该文件的基本文件类型,为后面的反编译分析提供便利。

根据DIE的信息:

  1. 普通64位PE文件
  2. C或C++
  3. 没有加壳
  4. 注意在节区一行,有TLS字样是亮的,说明有TLS节(当时没注意)

打开文件获取明显信息

1
2
3
恭喜你进入了主函数里
Please input your 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
31
32
33
34
35
36
37
38
39
40
41
42
void __fastcall main_0(int argc, const char **argv, const char **envp)
{
char *v3; // rdi
__int64 i; // rcx
char v5; // [rsp+20h] [rbp+0h] BYREF
_DWORD Q[15]; // [rsp+28h] [rbp+8h] BYREF
int j; // [rsp+64h] [rbp+44h]
unsigned int k; // [rsp+84h] [rbp+64h]

v3 = &v5;
for ( i = 34i64; i; --i )
{
*(_DWORD *)v3 = 0xCCCCCCCC;
v3 += 4;
}
j___CheckForDebuggerJustMyCode((__int64)&byte_7FF71EE840F4, (__int64)argv);
sub_7FF71EE711A9(byte_7FF71EE7AD78);
sub_7FF71EE7123F((__int64)aPleaseInputYou);
std::istream::getline(std::cin, flag, 33i64);
if ( j_strlen(flag) == 32 )
{
memset(Q, 0, 0x20ui64);
bToD(Q, flag);
for ( j = 0; j < 4; ++j )
TEA(&Q[2 * j], &Q[2 * j + 1]);
dToB(Q, last);
for ( k = 0; (int)k < 32; ++k )
{
if ( check[k] != last[k] )
{
sub_7FF71EE7123F((__int64)aNoNoNo);
sub_7FF71EE711A9("%d", k);
return;
}
}
sub_7FF71EE7123F((__int64)aYes);
}
else
{
sub_7FF71EE711A9("Wrong Length!");
}
}

大致分为三个阶段:

  1. 读入flag
  2. 校验flag
  3. 输出
1
2
3
sub_7FF71EE711A9(byte_7FF71EE7AD78);
sub_7FF71EE7123F((__int64)aPleaseInputYou);
std::istream::getline(std::cin, flag, 33i64);
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( j_strlen(flag) == 32 )
{
memset(Q, 0, 0x20ui64);
bToD(Q, flag);
for ( j = 0; j < 4; ++j )
TEA(&Q[2 * j], &Q[2 * j + 1]);
dToB(Q, last);
for ( k = 0; k < 32; ++k )
{
if ( check[k] != last[k] )
{
}
}
1
2
3
4
5
6
7
8
9
10
		sub_7FF71EE7123F((__int64)aNoNoNo);
sub_7FF71EE711A9("%d", k);
return;
sub_7FF71EE7123F((__int64)aYes);
}
else
{
sub_7FF71EE711A9("Wrong Length!");
}
}
1
2
3
4
5
6
7
v3 = &v5;
for ( i = 34i64; i; --i )
{
*(_DWORD *)v3 = 0xCCCCCCCC;
v3 += 4;
}//看起来没有意义,在一开始初始化内存
j___CheckForDebuggerJustMyCode((__int64)&byte_7FF71EE840F4, (__int64)argv);//看名字是反调试

读入和输出部分没什么好分析的,主要是检验flag部分,但是先试试可不可以调试,尝试之后果然不可以。那么开始分析检验flag部分:

根据if ( j_strlen(flag) == 32 )

flag为32位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall bToD(_DWORD *Q, char *flag)
{
int i; // [rsp+44h] [rbp+24h]
int a; // [rsp+64h] [rbp+44h]
int j; // [rsp+84h] [rbp+64h]

j___CheckForDebuggerJustMyCode(byte_7FF71EE840F4);
for ( i = 0; i < 8; ++i )
{
a = 0;
for ( j = 0; j < 4; ++j )
a |= flag[4 * i + j] << (8 * j);
Q[i] = a;
}
}

看到flag[4 * i + j],立即想到把flag看作二维数组,一共8行一行4个元素,由于Q是DWORD型,flag是char型,让a异或等于一行flag(第i个要flag左移8i位)相当于让a等于一行flag反向排列组合的十六进制字符,那么Q也会变成8个元素刚好每个32位

1
2
for ( j = 0; j < 4; ++j )
TEA(&Q[2 * j], &Q[2 * j + 1]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall TEA(_DWORD *Q2j_, _DWORD *Q2j1)
{
unsigned int det; // [rsp+44h] [rbp+24h]
int i; // [rsp+64h] [rbp+44h]
int j; // [rsp+84h] [rbp+64h]

j___CheckForDebuggerJustMyCode(byte_7FF71EE840F4);
det = 0x90508D47;
for ( i = 0; i < 4; ++i )
{
for ( j = 0; j < 33; ++j )
{
*Q2j_ += (((32 * *Q2j1) ^ (*Q2j1 >> 4)) + *Q2j1) ^ (det + tt[det & 3]) ^ det;
*Q2j1 += (((32 * *Q2j_) ^ (*Q2j_ >> 4)) + *Q2j_) ^ (det + tt[(det >> 11) & 3]);
det -= 0x77BF7F99;
}
}
}

tt数组的值是:

1
tt              db 31h, 0B7h, 0B6h, 31h, 4 dup(0)

这个函数是典型的TEA加密,它是分组加密,且使用异或,每一步都可逆,写出逆向代码就是:

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
void dec(unsigned int *a) {
unsigned int b2;
unsigned int b1;
b1 = *a;
b2 = a[1];
unsigned int e = 0x90508D47;
int tt[8] = { 0x62, 0x6F, 0x6D, 0x62, 0x00, 0x00, 0x00, 0x00 };
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 33; ++j)
e -= 0x77BF7F99;
}
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 33; ++j)
{
e += 0x77BF7F99;
b2 -= (((b1 * 32) ^ (b1 >> 4)) + b1) ^ (e + tt[(e >> 11) & 3]);
b1 -= (((32 * b2) ^ (b2 >> 4)) + b2) ^ (e + tt[e & 3]) ^ e;
}

}
*a = b1;
a[1] = b2;
}
for (int k = 0; k < 4; k++)
{
dec(&Q[k*2]);
}

TEA基本解决步骤就是恢复det状态,相等循环长度,内容反向排列,加变减,减变加,异或不变

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
void __fastcall dtob(_DWORD *Q, char *last)
{
char *p_spec; // rdi
__int64 i; // rcx
char spec; // [rsp+20h] [rbp+0h] BYREF
int max; // [rsp+24h] [rbp+4h]
int b[11]; // [rsp+48h] [rbp+28h]
int j; // [rsp+74h] [rbp+54h]
unsigned int a; // [rsp+94h] [rbp+74h]
int k; // [rsp+B4h] [rbp+94h]

p_spec = &spec;
for ( i = 46i64; i; --i )
{
*(_DWORD *)p_spec = 0xCCCCCCCC;
p_spec += 4;
}
j___CheckForDebuggerJustMyCode((__int64)&byte_7FF71EE840F4, (__int64)last);
b[0] = 0;
b[1] = 8;
b[2] = 16;
b[3] = 24;
for ( j = 0; j < 8; ++j )
{
a = Q[j];
for ( k = 0; k < 4; ++k )
last[4 * j + k] = a >> b[k];
}
}

这个函数把Qj右移的结果赋给last,把last看成二维char数组,,所以根据bk的值右移相当于每行逆向排序last

1
2
3
4
5
6
7
8
9
10
for ( k = 0; (int)k < 32; ++k )
{
if ( check[k] != last[k] )
{
sub_7FF71EE7123F((__int64)aNoNoNo);
sub_7FF71EE711A9("%d", k);
return;
}
}
sub_7FF71EE7123F((__int64)aYes);
1
2
3
4
5
.data:00007FF71EE7E040 check           db 0E0h, 0F3h, 21h, 96h, 97h, 0C7h, 0DEh, 89h, 9Bh, 0CAh
.data:00007FF71EE7E040 ; DATA XREF: main_0+119↑o
.data:00007FF71EE7E04A db 62h, 8Dh, 0B0h, 5Dh, 0FCh, 0D2h, 89h, 55h, 1Ch, 42h
.data:00007FF71EE7E054 db 50h, 0A8h, 76h, 9Bh, 0EAh, 0B2h, 0C6h, 2Fh, 7Ch, 0CFh
.data:00007FF71EE7E05E db 11h, 0DEh

所以只需要令last等于check然后逆向即可得到答案

写逆向代码然后得出答案

1
2
3
4
5
6
7
8
9
10
11
12
13
def reverse_hex_groups(hex_numbers, group_size):
hex_numbers = hex_numbers.replace('[', '').replace(']', '').split(',')
hex_numbers = [num.strip()[2:] for num in hex_numbers]
grouped_numbers = [hex_numbers[i:i + group_size] for i in range(0, len(hex_numbers), group_size)]
reversed_groups = [group[::-1] for group in grouped_numbers]
reversed_hex = '0x' + ','.join([''.join(group) for group in reversed_groups])
return reversed_hex

input_hex_numbers = input("Enter hex numbers separated by commas (with optional surrounding brackets): ")
input_group_size = int(input("Enter the group size: "))

output = reverse_hex_groups(input_hex_numbers, input_group_size)
print(output)
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
void dec(unsigned int *a) {
unsigned int b2;
unsigned int b1;
b1 = *a;
b2 = a[1];
unsigned int e = 0x90508D47;
int tt[8] = { 0x62, 0x6F, 0x6D, 0x62, 0x00, 0x00, 0x00, 0x00 };
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 33; ++j)
e -= 0x77BF7F99;
}
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 33; ++j)
{
e += 0x77BF7F99;
b2 -= (((b1 * 32) ^ (b1 >> 4)) + b1) ^ (e + tt[(e >> 11) & 3]);
b1 -= (((32 * b2) ^ (b2 >> 4)) + b2) ^ (e + tt[e & 3]) ^ e;
}

}
*a = b1;
a[1] = b2;
}
for (int k = 0; k < 4; k++)
{
dec(&Q[k*2]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
def reverse_hex(hex_list):
reversed_hex = ""
for hex_num in hex_list:
# 移除 "0x",然后每两位反转
reversed_num = "".join(reversed([hex_num[i:i+2] for i in range(2, len(hex_num), 2)]))
reversed_hex += reversed_num
return reversed_hex

user_input = input("请输入一组以0x开头,逗号间隔的十六进制数:")
hex_list = user_input.split(',')

# 输出结果
print(reverse_hex(hex_list))

那么得出答案,答案是……???

第二步做完就发现问题了:

错误的答案
fake_answer

这是为什么呢??


重回细节

还有什么没有检查呢,只有反调试了吧……

那么打开j___CheckForDebuggerJustMyCode:

1
2
3
4
5
6
7
8
void __fastcall _CheckForDebuggerJustMyCode(_BYTE *a1)
{
if ( *a1 )
{
if ( dword_7FF71EE7E9C4 )
GetCurrentThreadId();
}
}

这是一个没有返回值的函数,并且唯一可能执行的副作用是GetCurrentThreadId(),然而这个函数是返回当前线程句柄,所以这个函数只是名字叫检查debugger的函数,实际上没有作用!!

但是我们调试的时候确实会闪退,那么我们再试一试

无论是下断点还是直接调试都会直接闪退,于是我尝试用remote windows debugger试一试点击调试:

获得提示
image-20231218114708422

那么要是可以用这个提示把反调试破掉就可以调试了

在字符串列表查看这个文字,通过交叉引用来查看哪个函数使用了这个

找到了这个函数:

sub_7FF7AB081AE0
sub_7FF7AB081AE0

反编译之后是:

1
2
3
4
5
6
void __fastcall __noreturn sub_7FF7AB081AE0()
{
j___CheckForDebuggerJustMyCode(byte_7FF7AB0940F4);
sub_7FF7AB0811A9("SomeThing Go Wrong\n");
exit(0);
}

那么继续找交叉引用,重复多次后定位到两个函数

2个TlsCallBack

image-20231218121518231

找到这两个函数

TlsCallBack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall TlsCallback_0_0(__int64 a1, unsigned __int16 a2)
{
struct _PEB *v2; // rax
__int64 result; // rax
int i; // [rsp+44h] [rbp+24h]

j___CheckForDebuggerJustMyCode(byte_7FF7AB0940F4);
v2 = NtCurrentPeb();
LOBYTE(v2) = v2->BeingDebugged;
if ( v2->BeingDebugged == 4 )
sub_7FF7AB081AE0();
result = a2 & 1;
if ( (a2 & 1) != 0 )
{
for ( i = 0; i < 4; ++i )
{
j_oooo(off_7FF7AB08E008 + i + 1, a2);
result = (i + 1);
}
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall TlsCallback_1_0(__int64 a1, char a2)
{
__int64 result; // rax
int i; // [rsp+44h] [rbp+24h]

j___CheckForDebuggerJustMyCode(byte_7FF7AB0940F4);
if ( NtCurrentPeb()->BeingDebugged == 1 )
sub_7FF7AB081AE0();
result = a2 & 1;
if ( (a2 & 1) != 0 )
{
for ( i = 0; i < 32; ++i )
{
*(off_7FF7AB08E060 + i + 1) ^= i;
result = (i + 1);
}
}
return result;
}

这两个函数中的sub_7FF7AB081AE0()就是刚刚显示somethingwrong的函数,所以通过patch的方法(keypatch)把两个==改成!=这样就不会报错了,保存后打开patch过的文件,调试,果然没问题了,但是并没有解决刚刚的问题,所以继续执行看看哪里不对,于是发现:

tt数组和check数组的值被修改了

1
tt[8] = { 0x62, 0x6F, 0x6D, 0x62, 0x00, 0x00, 0x00, 0x00 };
1
unsigned int Q[8] = { 0x9523F2E0,0x8ED8C293,0x8668C393,0xDDF250BC,0x510E4499,0x8C60BD44,0x34DCABF2,0xC10FD260 };

使用这个值去计算即可获得正确的答案

flag{W31com3_2_Th3_QwbS7_4nd_H4v3_Fun}


复盘

现在的问题是

为什么main函数没有调用那两个函数,但是那两个函数会先运行呢,这里要讲到之前提到的Tls节区了,TLS是线程局部储存的缩写,有一种函数叫TLS回调函数,这种函数在线程创建时和销毁时会执行一次

TLS回调函数的设计目的是为了提供一种机制,使得每个线程都有自己的全局变量副本,从而避免了多线程同步问题。同时,它也提供了一种在每个线程的生命周期的开始和结束时执行特定代码的机制

所以刚刚的反调试函数就是Tls函数存在特殊的节区Tls节区,详细内容可以看<逆核>p550.

然后再看看它是如何修改数据的

修改处代码
其一

off_14001E060在内存中的位置

这里的值是一个偏移,偏移的位置是check上面一位

所以i+1就是check数组

二号在这里这个函数是上面的图里面的函数里面的函数这里的方法一样但是看到偏移很抽象

这里是到1DFFF+1就是1E000就是tt的位置(我只能说很阴险)

实际上,这道题只要动态调试进入main就可以解决了

ok,完结撒花


另一种做法

ida有一种调试方式叫附加调试,即先打开文件,然后让ida附加到进程上,对于这道题来说,由于反调试代码只在Tls回调函数中,所以用附加调试可以直接跳过反调试,同时得到最后的值。