本帖最后由 基丶 于 2021-4-27 18:10 编辑
题目链接:https://ctf.pediy.com/itembank.htm
分析:
先查下壳,无壳。
直接拖OD搜字符串,搜不到,看来是压缩过了。
换一种思路,给GetWindowText下断,试试能不能断下。
发现能断下
回溯一层回到调用GetWindowText的那段代码下断。
程序在按确定时会断下两次,第一次获取注册码,第二次获取用户名。
在第二次时,给buffer下内存访问断点,试试能不能追到加密函数。
下了内存访问断点之后会断下,但是发现断下的模块是系统模块(KernelBase、Ntdll、User32),这些模块直接跳过。
接着就断到程序自己的模块里
分析函数用OD不怎么舒服,拖到IDA分析一波。
这个程序没有设置动态基址,并且使用的也是默认基址,所以打开IDA后直接跳转到00401BF0 分析就可以。
在IDA里,F5以下,看下这个程序的伪代码
[C++] 纯文本查看 复制代码 int __thiscall sub_401BF0(HWND *this, int a2)
{
int v2; // edx
int v3; // ebx
int v4; // esi
int v5; // edi
char v6; // al
char v7; // al
const CHAR *v8; // esi
HWND v9; // eax
v2 = 0;
v3 = 1;
v4 = *(_DWORD *)(a2 + 128);
v5 = *(_DWORD *)(v4 - 8);
if ( v5 > 0 )
{
while ( 1 )
{
v6 = *(_BYTE *)(v4 + v2);
if ( (v6 < 48 || v6 > 57) && (v6 < 65 || v6 > 90) && (v6 < 97 || v6 > 122) )
break;
if ( ++v2 >= v5 )
goto LABEL_11;
}
v3 = 0;
LABEL_11:
while ( v2 > 1 )
{
v7 = *(_BYTE *)(v4 + v2-- - 1);
if ( v7 == *(_BYTE *)(v4 + v2 - 1) )
v3 = 0;
}
if ( !v3 )
{
v8 = *(const CHAR **)(a2 + 100);
v9 = GetParent(this[7]);
sub_418A00(v9);
sub_41B0D1(1013, v8);
}
}
return v3;
}
发现这个代码就是检查用户名字符串长度>=5并且仅支持大小写字母+数字的。
需要调用这个函数就证明接下来就是实现加密部分了,我们给这个函数的栈帧下断点。
断下后回溯一层,就能找到我们的加密函数。
故技重施再次拖到IDA,F5看下伪代码好分析。
[C++] 纯文本查看 复制代码
int __thiscall sub_401AE0(int this)
{
HWND v2; // eax
int v3; // eax
HWND v4; // eax
int v5; // esi
int result; // eax
_BYTE *v7; // edi
int v8; // ebp
int v9; // eax
int v10; // esi
int v11; // eax
int v12; // ecx
int v13; // edx
char *v14; // eax
int i; // ecx
int v16; // esi
int v17; // [esp+10h] [ebp-Ch]
int v18; // [esp+14h] [ebp-8h]
int v19; // [esp+18h] [ebp-4h]
v2 = GetParent(*(HWND *)(this + 28));
v3 = sub_418A00(v2);
v4 = GetParent(*(HWND *)(v3 + 28));
v5 = sub_418A00(v4);
v17 = v5;
result = sub_417669(0x15u);
v7 = (_BYTE *)result;
v8 = *(_DWORD *)(*(_DWORD *)(v5 + 128) - 8);
v19 = result;
if ( v8 >= 5 ) // V8 = 字符串长度
{
result = sub_401BF0((HWND *)this, v5); // 对字符串效验
if ( result )
{
v9 = 0; // index
while ( 1 )
{
v10 = *(_DWORD *)(v5 + 128); // 取出用户名
v11 = v9 + 1; // index
v18 = v11; // 保存了下Index
v12 = *(char *)(v10 + v11 - 1); // 用户名 + index - 1 取出子串
if ( v11 < v8 )
{
v13 = v11;
v14 = (char *)(v10 + 1);
do
{
v12 += *v14++;
++v13;
}
while ( v13 < v8 );
}
for ( ; v12 < 10000; v12 *= 3 )
;
for ( i = v12 / 3; i; *v7 = 0 )
{
v16 = i % 10;
++v7;
i = (i - i % 10) / 10;
*(v7 - 1) = *(_BYTE *)(v16 + *(_DWORD *)(this + 92));
}
v9 = v18;
if ( v18 >= 5 )
break;
v5 = v17;
}
result = sub_401C80(v19, v17);
}
}
return result;
}
拦截一波加密后的字符串试试,输入进去看看行不行。
岂可修,不行的话那么最值得怀疑的就是 sub_401C80 函数了。因为加密后还调用了这个函数,可能还会实现二次加密。
IDA跟一波这个函数
[C++] 纯文本查看 复制代码 int __thiscall sub_401C80(HWND *this, const char *a2, int a3)
{
HWND *v3; // ebx
unsigned int v20; // kr04_4
signed int KeyLen; // ebp
int V25; // eax
const CHAR *v8; // esi
HWND v9; // edx
signed int v10; // ecx
int v11; // ebx
int v12; // ecx
int v13; // ecx
const CHAR *v14; // edi
HWND v15; // eax
HWND v16; // eax
int v17; // [esp+10h] [ebp-Ch]
signed int i; // [esp+24h] [ebp+8h]
v3 = this;
sub_417669(0x16u); // 分配缓冲区
v20 = strlen(a2) + 1; // A2是加密后的文本缓冲区指针
v17 = 1;
KeyLen = v20 - 1; // V6为加密后用户名的长度
V25 = v20 - 1 + 4; // 密码的长度
if ( V25 != *(_DWORD *)(*(_DWORD *)(a3 + 124) - 8) )// 这里判断密码长度与加密后用户名长度如果不相等
{
v8 = *(const CHAR **)(a3 + 100);
v9 = v3[7];
goto LABEL_15;
}
v10 = 0;
v11 = 0;
for ( i = 0; v10 < KeyLen; i = v10 )
{
if ( v11 % 5 == 4 ) // 判断子串是第否是第5、10、15个子串,等等五的倍数的子串
{
V25 = *(_DWORD *)(a3 + 124); // 现在这个是我们的密码缓冲区
if ( *(_BYTE *)(v11 + V25) != 45 ) // 取出密码的子串,子串一定要=45,否则失败,字符ASCALL码45为 ‘-’
// 那就是说明,注册码的第五的倍数个必为 ‘-’,否则注册码出错,弹出失败
{
v8 = *(const CHAR **)(a3 + 100); // 应该是开始效验
v9 = this[7];
LABEL_15: // 失败
v16 = GetParent(v9);
sub_418A00(v16);
return sub_41B0D1(1013, v8);
}
v12 = v10 - 1;
}
else
{
v13 = a2[v10]; // a2是加密后的KEY,这里取出KEY子串
V25 = v13 / 5;
KeyLen = v20 - 1; // KEY长度
if ( v13 - v13 % 5 - 20 == *(char *)(v11 + *(_DWORD *)(a3 + 124)) + v13 % 2 + 12 )// a3+124是我们的密码缓冲区,这就是二次加密的算法
{
if ( v17 == v20 - 1 )
{
v14 = *(const CHAR **)(a3 + 96); // /成功
v15 = GetParent(this[7]);
sub_418A00(v15);
sub_41B0D1(1013, v14);
sub_41B0D1(1016, *(LPCSTR *)(a3 + 128));
sub_41B079(1003);
V25 = sub_41B268(5);
}
++v17;
}
v12 = i;
}
v10 = v12 + 1;
++v11;
}
return V25;
}
经过分析之后就能写个注册机还原下算法。
能照搬IDA的就直接照搬IDA,照搬不了的就直接看汇编还原下。
[C++] 纯文本查看 复制代码 int main(){
char UserName[] = "CTFHUB";
char VerifyBuf[] = "zouzhiyong";
int StrLen = strlen(UserName);
char Key[24] = { 0 };
char *KeyPointer = Key;
if (StrLen >= 5)//判断字符串长度是否>=5
{
int Temp = 0;
//接着是对字符串效验的函数 必须是 大小写英文/数字
while (1)
{
int Index = Temp + 1; // V11
int TempIndex = Index; // V18
LONG Character = *(char*)(UserName + Index - 1);
if (Index < StrLen)
{
int TempIndex2 = Index;
char * CharacterAddress = (char*)(UserName + 1);
do
{
char axx = (*CharacterAddress);
Character += axx;
CharacterAddress++;
++TempIndex2;
} while (TempIndex2 < StrLen);
}
for (; Character < 10000; Character *= 3);
//
for (int i = Character / 3; i; *KeyPointer=0)
{
int Num = i % 10;
++KeyPointer;
i = (i - i % 10) / 10;
*(KeyPointer - 1) = *(char*)(Num + VerifyBuf);
}
Temp = TempIndex;
if (Temp >= 5) break;
}
}
//获取KEY完成,开始计算注册码
int V17 = 0;
unsigned int KeyLen = 0;
KeyLen = strlen(Key);
char RegisterCode[30] = { 0 };
int Index = 0;//V12
int Temp1 = 0;//V10
int Temp2 = 0;//V11
for (int i = 0; i < KeyLen; i = Temp1)
{
//判断子串是第否是第5、10、15个子串,等等五的倍数的子串
if (Temp2 % 5 == 4)
{
RegisterCode[Temp2] = 45;
Index = Temp1 - 1;
}//TEMP1忽略了五的倍数,当执行到5的倍数时不会+1
else
{
int KeyChar = Key[Temp1];//V13
//
//if (KeyChar - KeyChar % 5 - 20 == *(char*)(Temp2 + (*(DWORD*)RegisterCode)) + KeyChar % 2 + 12)
//{
//}
//*(char*)(Temp2 + RegisterCode) = KeyChar - KeyChar % 5 - 20 - 12 - KeyChar % 2;
*(char*)(Temp2 + RegisterCode) = KeyChar - KeyChar % 5 - 20 - KeyChar % 2 - 12;
Index = i;
}
Temp1 = Index + 1;
Temp2++;
}
printf("%s", RegisterCode);
system("pause");
return 1;
} |