本帖最后由 舒默哦 于 2021-4-17 01:47 编辑
这道题在网上下的,也不知道谁发的,文件我发上来了,想要分析的朋友可以下载下来搞搞,再来看我的帖子,对照下看是否有出入。
正文
这是32位的一个控制台程序,启动后界面如下:
主函数如下:
当程序走到wsprintfA函数会引起异常,v10[0]里面存的是sub_401360函数地址。
异常来自这个函数,在OD里跟进这个函数查看:
执行到401372产生了异常:
再次点击运行时,程序产生第二次异常卡住了。正常打开程序后,输入随机16位字符,也会走这个地方并且产生异常,程序并不会卡死,会提醒输入的flag错误。
那么。暂时可以推断该程序是注册了一个VEH或者注册了一个最顶层异常处理函数,因为在主函数中没有看见结构化异常处理的相关代码。分析反调试:
用户层异常流程:
先说明下异常在用户层的分发流程:
KiUserExceptionDispatcher->RtlDispatchException->VEH -> 没有VEH处理则 调用SEH。没有SEH,则查看是否有最顶层异常处理函数,没有程序就会exitprocess。
注意:用户态的异常如果在非调试状态下的话仅仅只有一轮的分发,而只有在调试状态下才会进行第二轮,再次判断调试器是否要接手异常。如果注册了最顶层异常处理函数,有调试器存在的情况下,顶级异常处理函数就会被跳过,否则就会被顶级处理函数接管。
由上面产生异常时,分析得出的结论:该程序可能注册了一个VEH或者注册了一个最顶层异常处理函数。那么在哪儿注册的呢?
首先查看是否有TLS表的存在:
TLS不存在,那么可以暂时在KiUserExceptionDispatcher下断,程序产生异常后,一步一步往下跟,看看有什么情况。
运气好,这儿看到RtlDispatchException函数的函数头被HOOK了:
接下来,可以用IDA打开分析:
再次跟进Handler:
跟进sub_401550函数,发现还注册了顶层异常处理函数。
过反调试:
既然知道注册了顶层异常处理函数,那么在NtQueryInformationProcess下断,时机是产生异常时下断。NtQueryInformationProcess会断下两次。
第一次的协议号是0x22,过滤掉,第二次协议号是0x7,与调试有关的,修改返回值,整个流程就通了。
第二次调用NtQueryInformationProcess返回时候,把sf置1,跳过inc esi这条指令。
随意输入16个字符,程序不会被卡住,正常退出了:
分析加密算法:
这个程序把整个加密过程放到了异常处理的流程中了,具体放到了sub_401600函数和handler函数里面了。1、先分析输入数据的流向:
产生异常前,输入的字符串数据分别被保存ebx、edx、ecx、esi中,此外eax=0x797963。
产生异常后,KiUserExceptionDispatcher->RtlDispatchException->jmp sub_401600
其中a1是_CONTEXT结构体,a2是EXCEPTION_RECORD。
a1+0xA0在_CONTEXT结构体中,恰好对应的是esi寄存器。
数据经过处理后被存放到了EXCEPTION_RECORD的ExceptionInformation中。
[C] 纯文本查看 复制代码
_EXCEPTION_RECORD 结构体如下:
#define EXCEPTION_MAXIMUM_PARAMETERS 15 // maximum number of exception parameters
type struct _EXCEPTION_RECORD
{
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags; //异常状态
struct _EXCEPTION_RECORD* ExceptionRecord; //下一个异常
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters; //附加参数个数
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS]; //附加参数指针
}
经过一些流程后,最后调用顶层异常处理函数,把加密后的flag放到eax、ebx、edx、ecx、esi、edi寄存器里。
[C] 纯文本查看 复制代码
LONG __stdcall TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
ExceptionInfo->ContextRecord->Eax = ExceptionInfo->ExceptionRecord->ExceptionInformation[0];
ExceptionInfo->ContextRecord->Ebx = ExceptionInfo->ExceptionRecord->ExceptionInformation[1];
ExceptionInfo->ContextRecord->Ecx = ExceptionInfo->ExceptionRecord->ExceptionInformation[2];
ExceptionInfo->ContextRecord->Edx = ExceptionInfo->ExceptionRecord->ExceptionInformation[3];
ExceptionInfo->ContextRecord->Edi = ExceptionInfo->ExceptionRecord->ExceptionInformation[4];
ExceptionInfo->ContextRecord->Esi = ExceptionInfo->ExceptionRecord->ExceptionInformation[5];
ExceptionInfo->ContextRecord->Eip = (DWORD)sub_401390;
SetUnhandledExceptionFilter(lpTopLevelExceptionFilter);
RemoveVectoredExceptionHandler(Handler);
return -1;
}
EIP修正为sub_401390:
下面sub_401390函数的主要功能:
1、最终把输入的flag作比较,都匹配成功了则返回1,失败返回0。
2、修改返回地址,把EIP改为了main函数的地址。
最后返回main函数0x401B6E处继续执行。
2、分析加密流程
输入的flag的会经过三次加密,最终和一个字符串对比,这个字符串是 "LMlWu2Y/Tk8c33Y+T8Lv0a==";匹配成功则flag输入正确。
第一次加密:
第二次加密:
第三次加密:Handler->sub_401110
[C] 纯文本查看 复制代码
_BYTE *__cdecl sub_401110(unsigned int a1, int *__shifted(EXCEPTION_RECORD,0x14) a2)
{
_BYTE *v3; // [esp+0h] [ebp-18h]
unsigned int v4; // [esp+8h] [ebp-10h]
unsigned int v5; // [esp+Ch] [ebp-Ch]
unsigned int i; // [esp+10h] [ebp-8h]
_BYTE *v7; // [esp+14h] [ebp-4h]
_BYTE *v8; // [esp+14h] [ebp-4h]
_BYTE *v9; // [esp+14h] [ebp-4h]
_BYTE *v10; // [esp+14h] [ebp-4h]
_BYTE *v11; // [esp+14h] [ebp-4h]
v7 = calloc(4 * (a1 / 3) + 5, 1u);
v3 = v7;
for ( i = 0; i < 3 * (a1 / 3); i += 3 )
{
LOBYTE(v4) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 2);
BYTE1(v4) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 1);
HIWORD(v4) = *((unsigned __int8 *)ADJ(a2)->ExceptionInformation + i);
*v7 = byte_41E8B0[(v4 >> 18) & 0x3F];
v8 = v7 + 1;
*v8++ = byte_41E8B0[(v4 >> 12) & 0x3F];
*v8++ = byte_41E8B0[(v4 >> 6) & 0x3F];
*v8 = byte_41E8B0[v4 & 0x3F];
v7 = v8 + 1;
}
if ( a1 != i )
{
LOWORD(v5) = 0;
HIBYTE(v5) = 0;
if ( a1 - i == 2 )
{
BYTE1(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i + 1);
BYTE2(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i);
*v7 = byte_41E8B0[(v5 >> 18) & 0x3F];
v9 = v7 + 1;
*v9 = byte_41E8B0[(v5 >> 12) & 0x3F];
v10 = v9 + 1;
*v10 = byte_41E8B0[(v5 >> 6) & 0x3F];
}
else
{
BYTE2(v5) = *((_BYTE *)ADJ(a2)->ExceptionInformation + i);
*v7 = byte_41E8B0[(v5 >> 18) & 0x3F];
v11 = v7 + 1;
*v11 = byte_41E8B0[(v5 >> 12) & 0x3F];
v10 = v11 + 1;
*v10 = 61;
}
v10[1] = 61;
}
return v3;
}
其中byte_41E8B0是一个字符串:
逆算法
逆算法,我直接贴代码了:
[C] 纯文本查看 复制代码
#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <string>
typedef unsigned char uint8;
#define _BYTE unsigned char
#define _WORD unsigned word
#define LOBYTE_(w) ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE_(w) ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
#define BYTEn(x, n) (*((_BYTE*)&(x)+n))
#define WORDn(x, n) (*((_WORD*)&(x)+n))
#define BYTE0(x) BYTEn(x, 0) // byte 0 (counting from 0) 添加此宏定义
#define BYTE1(x) BYTEn(x, 1) // byte 1 (counting from 0)
#define BYTE2(x) BYTEn(x, 2)
#define BYTE3(x) BYTEn(x, 3)
#define BYTE4(x) BYTEn(x, 4)
const char* byte_41E8B0 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/";
void reversecalc(char* incs)
{
/*----------------------------------------------------------------------------------*/
/* 逆算法第一步 ,也就是逆加密的第三步 */
/*----------------------------------------------------------------------------------*/
std::string s = byte_41E8B0;
//const char* str = "WlML/Y2uc8kT+Y33vL8T ==a0";
const char* str = "LMlWu2Y/Tk8c33Y+T8Lv0a==";
char ttt = str[0x14];
//int incs[8] = { 0 };
//char* tempstr = (char*)str;
char tv[0x32] = { 0 };
int xtemp = 0;
int xtemp1 = 0;
int xtemp2 = 0;
int xtemp3 = 0;
int i = 0;
int j = 0;
for (; i < strlen(str)-4; i+=4)
{
xtemp = s.find(str[i], 0);
xtemp <<= 18;
xtemp1 = s.find(str[i+1], 0);
xtemp1 <<= 12;
xtemp2 = s.find(str[i+2], 0);
xtemp2 <<= 6;
xtemp3 = s.find(str[i+3], 0);
xtemp = (xtemp | xtemp1 | xtemp2 | xtemp3);
BYTE byte_1, byte_2, byte_3, byte_4;
unsigned int ret;
//int 类型 占4个字节,32位
byte_1 = (xtemp & 0xff000000) >> 24;
byte_2 = (xtemp & 0x00ff0000) >> 16;
byte_3 = (xtemp & 0x0000ff00) >> 8;
byte_4 = xtemp & 0x000000ff;
incs[j++] = byte_2;
incs[j++] = byte_3;
incs[j++] = byte_4;
}
xtemp = s.find(str[i], 0);
xtemp <<= 18;
xtemp >>= 16;
xtemp1 = s.find(str[i+1], 0);
xtemp1 <<= 12;
incs[j] = xtemp;
/*----------------------------------------------------------------------------------*/
/* 逆算法第二步 ,也就是逆加密的第二步 */
/*----------------------------------------------------------------------------------*/
EXCEPTION_RECORD a2 = { 0};
for (int i = 0; i < 4; i++)
{
a2.ExceptionInformation[i] = *((int*)incs+i);
}
a2.ExceptionInformation[3] ^= a2.ExceptionInformation[1];
a2.ExceptionInformation[1] ^= a2.ExceptionInformation[3];
a2.ExceptionInformation[3] ^= a2.ExceptionInformation[1];
a2.ExceptionInformation[2] ^= a2.ExceptionInformation[0];
a2.ExceptionInformation[0] ^= a2.ExceptionInformation[2];
a2.ExceptionInformation[2] ^= a2.ExceptionInformation[0];
a2.ExceptionInformation[1] ^= a2.ExceptionInformation[0];
a2.ExceptionInformation[0] ^= a2.ExceptionInformation[1];
a2.ExceptionInformation[1] ^= a2.ExceptionInformation[0];
/*----------------------------------------------------------------------------------*/
/* 逆算法第三步 ,也就是逆加密的第一步 */
/*----------------------------------------------------------------------------------*/
const char* v2 = "1234567890123456cyy";
int tv2[8] = { 0,0xbe517013,0x34d7f69e,0x70133315 ,0xc6dfff05 };
int temp = 0;
for (int i = 4; i > 0; --i)
{
temp = a2.ExceptionInformation[i-1];
if (i==4)
{
temp ^= *(DWORD*)((char*)v2 + i * 4);//(DWORD)v2[i * 4];
}
else
{
temp ^=tv2[i+1];
}
__asm {
mov eax, temp
ror eax, 5
mov temp, eax
}
tv2[i] = temp;
}
printf("%s\n", tv2 + 1);
return;
}
int main()
{
char a2[30] = { 0 };
reversecalc(a2);
system("pause");
return 0;
}
运行之后最终得到:flag{Ex<ept10n~}
把flag{Ex<ept10n~}贴到窗口,成功了!
复盘
先找sub_401600函数的来源,是谁调用了。
sub_401600在sub_401680里引用了其地址。
再次查看sub_401680的来源,来源于sub_401000():
再追sub_401000的来源:
来到了.rdata数据区。看到const _PVFV First ,就知道是_initterm函数调用sub_401000,_initterm函数先于在main执行,这个函数的功能是初始化全局变量。
那么,程序大概是这样子:
[C] 纯文本查看 复制代码
//点号代表省略
......
int sub_401680()
{
.......
}
.......
#pragma init_seg(".CRT$XCC")
int g_hook = sub_401680();
int main()
{
.......
}
复盘程序的整个流程:
1、_initterm先于main执行,调用sub_401680(),完成RtlDispatchException函数的HOOK。
2、执行main函数,输入flag之后,产生异常:KiUserExceptionDispatcher->RtlDispatchException->jmp sub_401600。
3、在sub_401600函数里面,注册了VEH,在VEH处理函数里面又注册了顶层异常处理函数,然后返回RtlDispatchException里继续执行。
此时,还没有注册了顶层异常处理函数,因为还没有调用VEH。
4、RtlDispatchException调用VEH查看异常是否被处理了,此时,在VEH函数里注册了顶层异常处理函数,返回RtlDispatchException,
调用UnhandledExceptionFilter查看程序是否在被调试,被调试则交给调试器,没有调试器就调用SetUnhandledExceptionFilter()注册的函数来处理,最终修改EIP返回main()。
|