攻防世界 津门杯 GoodRE
1.点开,64位,无壳,无混淆
2.查看基本函数逻辑
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 *i64_v3_input; // r12
int *v4; // r14
__int64 v5; // rbx
int v6_Input; // eax
int *v7; // rbp
char *v8; // r13
int v10[18]; // [rsp+10h] [rbp-3F8h] BYREF
char v11[72]; // [rsp+58h] [rbp-3B0h] BYREF
char v12[72]; // [rsp+A0h] [rbp-368h] BYREF
char v13[72]; // [rsp+E8h] [rbp-320h] BYREF
int v14[72]; // [rsp+130h] [rbp-2D8h] BYREF
char v15[288]; // [rsp+250h] [rbp-1B8h] BYREF
__int64 v16_input[8]; // [rsp+370h] [rbp-98h] BYREF
__int64 v17[11]; // [rsp+3B0h] [rbp-58h] BYREF
v17[3] = __readfsqword(0x28u);
memset(v16_input, 0, sizeof(v16_input));
v17[0] = 0LL;
v17[1] = 0LL;
std::operator<<<std::char_traits<char>>(&std::cout, ">> ", a3);
std::operator>><char,std::char_traits<char>>(&std::cin, v16_input);
if ( (unsigned int)strlen((const char *)v16_input) == 64 )
{
i64_v3_input = v16_input;
v4 = (int *)&unk_561CC045A020;
v5 = 0LL;
do
{
v6_Input = CheckInput(i64_v3_input, 8);
v7 = v10;
AnalyzeBlock((__int64)&v10[v5], v6_Input);
AnalyzeBlock((__int64)&v14[v5], 17);
v8 = v15;
AnalyzeBlock((__int64)&v15[v5 * 4], *v4);
++i64_v3_input;
++v4;
v5 += 9LL;
}
while ( i64_v3_input != v17 );
sub_561CC0456B30(v10, v14);
sub_561CC0456B30((int *)v11, v14);
sub_561CC0456B30((int *)v12, v14);
sub_561CC0456B30((int *)v13, v14);
while ( !(unsigned int)sub_561CC0456ADC(v7, v8) )
{
v7 += 9;
v8 += 36;
if ( v7 == v14 )
{
__printf_chk(1LL, "flag{%s}\n", (const char *)v16_input);
return 0LL;
}
}
puts("error");
}
else
{
puts("error");
}
return 0LL;
}
首先会检查输入的长度
if ( (unsigned int)strlen((const char *)v16_input) == 64 )
接着我们继续跟踪,会发现一个函数对我们的输入进行处理,我们点进去分析一下
v6_Input = CheckInput(i64_v3_input, 8);
//*************************************
//**********下面是跟踪代码**************
//*************************************
__int64 __fastcall sub_558EE6283A3B(_BYTE *a1_Input, int a2)
{
unsigned __int64 v2_LenOfStrplus1; // kr08_8
int v3_LenOfStr; // r10d
_BYTE *v4_Input; // r9
__int64 result; // rax
__int64 i; // rdx
v2_LenOfStrplus1 = strlen(a0123456789abcd) + 1;
if ( a2 <= 0 )
return 0LL;
v3_LenOfStr = v2_LenOfStrplus1 - 1;
v4_Input = a1_Input;
LODWORD(result) = 0;
while ( 1 )
{
if ( v3_LenOfStr <= 0 )
return 0LL;
for ( i = 0LL; a0123456789abcd[i] != *v4_Input; ++i )
{
if ( i == (_DWORD)v2_LenOfStrplus1 - 2 )
return 0LL;
}
if ( v3_LenOfStr <= (int)i )
break;
result = (unsigned int)(i + (v2_LenOfStrplus1 - 1) * result);
if ( ++v4_Input == &a1_Input[a2 - 1 + 1] )
return result;
}
return 0LL;
}
化简一下函数的逻辑,大概是这样的
__int64 convert_custom_base_string(_BYTE *input, int length) //length == 8
{
const char *alphabet = "0123456789ABCDEF"; // 自定义进制字符集
int base = strlen(alphabet); // 基数
__int64 result = 0;
if (length <= 0)
return 0;
for (int i = 0; i < length; ++i) {
char ch = input[i];
int value = -1;
// 找出字符在 alphabet 中的位置
for (int j = 0; j < base; ++j) {
if (alphabet[j] == ch) {
value = j; //value为ch在表中的索引
break;
}
}
// 非法字符
if (value == -1)
return 0;
// 防止当前字符的值大于等于进制
if (value >= base)
return 0;
result = result * base + value;
}
return result;
}
动态调试一下就可以发现函数会检查我们的输入是否在字符串中,并且最后会返回原来的输入,所以我们重命名后就不用管他了。
接下来我们又看到一个函数,继续跟踪
v7 = v10;
AnalyzeBlock((__int64)&v10[v5], v6_Input);
AnalyzeBlock((__int64)&v14[v5], 17);
v8 = v15;
AnalyzeBlock((__int64)&v15[v5 * 4], *v4);
//*************************************
//**********下面是跟踪代码**************
//*************************************
__int64 __fastcall sub_558EE6283408(__int64 a1, int a2)
{
__int64 v2; // rax
unsigned int v3; // edx
*(_BYTE *)(a1 + 8) = 0;
*(_DWORD *)(a1 + 4) = a2;
v2 = 4LL;
while ( 1 )
{
v3 = v2 - 1;
if ( *(_BYTE *)(a1 + v2 + 3) )
break;
if ( !--v2 )
goto LABEL_4;
}
v3 = v2;
LABEL_4:
*(_DWORD *)a1 = v3;
return v3;
}
一下是化简的代码
这里假设了一个结构体,以便于理解代码。
这个是结构体
struct Block {
int value0; // +0
int inputValue; // +4
char bytes[4]; // +8 ... 11(从偏移 +7 到 +4 向下检查)
};
__int64 analyze_block(__int64 ptr, int input) //ptr指向结构体
{
// Set byte at offset +8 to 0
*(_BYTE *)(ptr + 8) = 0;
// Save input value to offset +4
*(_DWORD *)(ptr + 4) = input;
// Start checking from the highest offset
int i = 4;
while (i > 0) {
if (*(_BYTE *)(ptr + i + 3)) // Check bytes at offsets +7, +6, +5, +4
break;
--i;
}
// Save result to offset +0
*(_DWORD *)ptr = i;
return i;
}
可以看到a2是我们的输入,这个函数把我们的输入存入结构体的inputValue字段.(然后从最高位开始检查,如果发现有字节不为0,则停止,并返回结果。暂时不知道有啥用)
那么继续往下跟就可以猜测是加密代码了,因为出现的都是同一个函数,而且内部逻辑很复杂。
这里我重命名了一些函数
unsigned __int64 __fastcall sub_561CC0456B30(int *a1, int *a2)
{
int v2; // r14d
int v0[12]; // [rsp+20h] [rbp-338h] BYREF
int v1[12]; // [rsp+50h] [rbp-308h] BYREF
int v6[12]; // [rsp+80h] [rbp-2D8h] BYREF
int v7[12]; // [rsp+B0h] [rbp-2A8h] BYREF
int v8[12]; // [rsp+E0h] [rbp-278h] BYREF
_DWORD v9[12]; // [rsp+110h] [rbp-248h] BYREF
int v10[12]; // [rsp+140h] [rbp-218h] BYREF
int v11[12]; // [rsp+170h] [rbp-1E8h] BYREF
int v12[12]; // [rsp+1A0h] [rbp-1B8h] BYREF
int v13[12]; // [rsp+1D0h] [rbp-188h] BYREF
int v14[12]; // [rsp+200h] [rbp-158h] BYREF
int v15[12]; // [rsp+230h] [rbp-128h] BYREF
int v16[12]; // [rsp+260h] [rbp-F8h] BYREF
int v17[12]; // [rsp+290h] [rbp-C8h] BYREF
int v18[12]; // [rsp+2C0h] [rbp-98h] BYREF
int v19[10]; // [rsp+2F0h] [rbp-68h] BYREF
unsigned __int64 v20; // [rsp+318h] [rbp-40h]
v20 = __readfsqword(0x28u);
memcpy_my(v0, a1);
memcpy_my(v1, a1 + 9);
AnalyzeBlock((__int64)v6, 0);
AnalyzeBlock((__int64)v7, 0x830A5376);
AnalyzeBlock((__int64)v8, 0x1D3D2ACF);
Xor(v9, v8, v7);
memcpy_my(v10, a2); // sub_557EA15F0A13: 类似于 memcpy,带长度限制,复制数据块。
memcpy_my(v11, a2 + 9);
memcpy_my(v12, a2 + 18);
memcpy_my(v13, a2 + 27);
v2 = 32;
do
{
sub_561CC04562E9(v6, v6, v9);
sub_561CC0456767((__int64)v14, v1, 4);
sub_561CC04562E9(v14, v14, v10);
sub_561CC04562E9(v15, v1, v6);
ShiftLeft((__int64)v16, v1, 5);
sub_561CC04562E9(v16, v16, v11);
Xor(v14, v14, v15);
Xor(v14, v14, v16);
sub_561CC04562E9(v0, v0, v14);
sub_561CC0456767((__int64)v17, v0, 4);
sub_561CC04562E9(v17, v17, v12);
sub_561CC04562E9(v18, v0, v6);
ShiftLeft((__int64)v19, v0, 5);
sub_561CC04562E9(v19, v19, v13);
Xor(v17, v17, v18);
Xor(v17, v17, v19);
sub_561CC04562E9(v1, v1, v17);
--v2;
}
while ( v2 );
memcpy_my(a1, v0);
memcpy_my(a1 + 9, v1);
return __readfsqword(0x28u) ^ v20;
}
这时候我们可以借助ai帮助分析一下每个函数的作用,然后综合一下。因为出现了好多重复的函数,所以猜测是把一些步骤用函数封装了,好让我们看不出来。
stateL = a1[0..8];
stateR = a1[9..17];
key0 = a2[0..8];
key1 = a2[9..17];
key2 = a2[18..26];
key3 = a2[27..35];
for (int i = 0; i < 32; i++) {
mix(v6, v6, const_block); // v6 更新
temp1 = RightCompress(stateR, 4);
temp1 = temp1 + key0;
temp2 = Add(stateR, v6);
temp3 = LeftCompress(stateR, 5);
temp3 = temp3 + key1;
mixed1 = XOR(temp1, temp2);
mixed1 = XOR(mixed1, temp3);
stateL = Add(stateL, mixed1);
temp4 = RightCompress(stateL, 4);
temp4 = temp4 + key2;
temp5 = Add(stateL, v6);
temp6 = LeftCompress(stateL, 5);
temp6 = temp6 + key3;
mixed2 = XOR(temp4, temp5);
mixed2 = XOR(mixed2, temp6);
stateR = Add(stateR, mixed2);
}
// 结果回写
a1[0..8] = stateL;
a1[9..17] = stateR;
假如你是密码领域大神,可能你能直接看出来这个是tea加密算法,然后你就可以直接用这个算法解密了。(但是我没这么厉害,所以就寄了,看了WP才明白hh)
但是说实话还是挺明显的。
比如32次循环,两个int,4个key。这些都是关键。
3.解密
回主函数看一下这个
__printf_chk(1LL, "flag{%s}\n", (const char *)v16_input);
一路跟踪回去发现__v4 = (int *)&unk_561CC045A020;__
点进去看看
.data:0000561CC045A020 unk_561CC045A020 db 3Bh ; ; ; DATA XREF: main+EF↑o
.data:0000561CC045A021 db 1Ah
.data:0000561CC045A022 db 0AEh
.data:0000561CC045A023 db 79h ; y
.data:0000561CC045A024 db 0D3h
.data:0000561CC045A025 db 80h ; €
.data:0000561CC045A026 db 60h ; `
.data:0000561CC045A027 db 59h ; Y
.data:0000561CC045A028 db 80h ; €
.data:0000561CC045A029 db 3Eh ; >
.data:0000561CC045A02A db 0E0h
.data:0000561CC045A02B db 80h ; €
.data:0000561CC045A02C db 73h ; s
.data:0000561CC045A02D db 8Dh
.data:0000561CC045A02E db 6Ch ; l
.data:0000561CC045A02F db 84h
.data:0000561CC045A030 db 0F7h
.data:0000561CC045A031 db 1Ch
.data:0000561CC045A032 db 0A0h
.data:0000561CC045A033 db 21h ; !
.data:0000561CC045A034 db 32h ; 2
.data:0000561CC045A035 db 0CAh
.data:0000561CC045A036 db 0CAh
.data:0000561CC045A037 db 0C7h
.data:0000561CC045A038 db 14h
.data:0000561CC045A039 db 0ACh
.data:0000561CC045A03A db 0F9h
.data:0000561CC045A03B db 45h ; E
.data:0000561CC045A03C db 2Fh ; /
.data:0000561CC045A03D db 0F2h
.data:0000561CC045A03E db 0F5h
.data:0000561CC045A03F db 0C5h
一眼顶真的加密后的数据
现在还缺少密钥和delta,所以还不能解密。
回到加密代码我们动调分析
AnalyzeBlock((__int64)v7, 0x830A5376);
AnalyzeBlock((__int64)v8, 0x1D3D2ACF);
Xor(v9, v8, v7);
推测这里代码为
v7 = 0x830A5376
v8 = 0x1D3D2ACF
v9 = v8 ^ v7 //v9 == 0x9E3779B9
显然这是经典的delta的值
在继续动调得到4个key都是0x11,就能写解密代码了,反正看了wp,就白嫖了别的大佬的代码。
https://blog.csdn.net/yjh_fnu_ltn/article/details/139221612
#include <debugapi.h>
#include <stdio.h>
int main()
{
unsigned int key[4] = {0x11, 0x11, 0x11, 0x11}; // 密钥
unsigned int value[8] = {0x79AE1A3B, 0x596080D3, 0x80E03E80, 0x846C8D73, 0x21A01CF7, 0x0C7CACA32, 0x45F9AC14, 0x0C5F5F22F}; // 密文
int dalte = 0x9e3779b9;
int i = 0, j = 0, h = 0;
int wheel = 32; // 轮数
unsigned int sum = 0;
// 逆算法
for (i = 0; i < 8; i++, i++)
{
sum = (dalte * (wheel));
// 每轮加密
// for (j = 0; j < 4; ++j)
{
for (h = 0; h < wheel; ++h)
{
value[i + 1] -= (key[3] + (value[i] >> 5)) ^ (sum + value[i]) ^ (key[2] + 16 * value[i]);
value[i] -= (key[1] + (value[i + 1] >> 5)) ^ (sum + value[i + 1]) ^ (key[0] + 16 * value[i + 1]);
sum -= dalte;
}
}
}
for (i = 0; i < 8; i++)
{
printf("%X", value[i]);
}
return 0;
}
//7DEA3F6D3B3D6C0C620864ADD2FA2AE1A61F2736F0060DA0B97E8356D017CE59
Comments NOTHING