一道反调试小题分析
本帖最后由 舒默哦 于 2021-4-17 01:47 编辑这道题在网上下的,也不知道谁发的,文件我发上来了,想要分析的朋友可以下载下来搞搞,再来看我的帖子,对照下看是否有出入。
https://static.52pojie.cn/static/image/hrline/1.gif
## 正文
这是32位的一个控制台程序,启动后界面如下:
主函数如下:
当程序走到wsprintfA函数会引起异常,v10里面存的是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中。
_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
; //附加参数指针
}
经过一些流程后,最后调用顶层异常处理函数,把加密后的flag放到eax、ebx、edx、ecx、esi、edi寄存器里。
LONG __stdcall TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
ExceptionInfo->ContextRecord->Eax = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Ebx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Ecx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Edx = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Edi = ExceptionInfo->ExceptionRecord->ExceptionInformation;
ExceptionInfo->ContextRecord->Esi = ExceptionInfo->ExceptionRecord->ExceptionInformation;
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
_BYTE *__cdecl sub_401110(unsigned int a1, int *__shifted(EXCEPTION_RECORD,0x14) a2)
{
_BYTE *v3; //
unsigned int v4; //
unsigned int v5; //
unsigned int i; //
_BYTE *v7; //
_BYTE *v8; //
_BYTE *v9; //
_BYTE *v10; //
_BYTE *v11; //
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;
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 = 61;
}
return v3;
}
其中byte_41E8B0是一个字符串:
### 逆算法
逆算法,我直接贴代码了:
#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 _BYTEunsigned char
#define _WORDunsigned 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;
//int incs = { 0 };
//char* tempstr = (char*)str;
char tv = { 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, 0);
xtemp <<= 18;
xtemp1 = s.find(str, 0);
xtemp1 <<= 12;
xtemp2 = s.find(str, 0);
xtemp2 <<= 6;
xtemp3 = s.find(str, 0);
xtemp = (xtemp | xtemp1 | xtemp2 | xtemp3);
BYTE byte_1, byte_2, byte_3, byte_4;
unsigned intret;
//int 类型 占4个字节,32位
byte_1 = (xtemp & 0xff000000) >> 24;
byte_2 = (xtemp & 0x00ff0000) >> 16;
byte_3 = (xtemp & 0x0000ff00) >> 8;
byte_4 = xtemp & 0x000000ff;
incs = byte_2;
incs = byte_3;
incs = byte_4;
}
xtemp = s.find(str, 0);
xtemp <<= 18;
xtemp >>= 16;
xtemp1 = s.find(str, 0);
xtemp1 <<= 12;
incs = xtemp;
/*----------------------------------------------------------------------------------*/
/* 逆算法第二步 ,也就是逆加密的第二步 */
/*----------------------------------------------------------------------------------*/
EXCEPTION_RECORD a2 = { 0};
for (int i = 0; i < 4; i++)
{
a2.ExceptionInformation = *((int*)incs+i);
}
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
a2.ExceptionInformation ^= a2.ExceptionInformation;
/*----------------------------------------------------------------------------------*/
/* 逆算法第三步 ,也就是逆加密的第一步 */
/*----------------------------------------------------------------------------------*/
const char* v2 = "1234567890123456cyy";
int tv2 = { 0,0xbe517013,0x34d7f69e,0x70133315 ,0xc6dfff05 };
int temp = 0;
for (int i = 4; i > 0; --i)
{
temp = a2.ExceptionInformation;
if (i==4)
{
temp ^= *(DWORD*)((char*)v2 + i * 4);//(DWORD)v2;
}
else
{
temp ^=tv2;
}
__asm {
mov eax, temp
ror eax, 5
mov temp, eax
}
tv2 = temp;
}
printf("%s\n", tv2 + 1);
return;
}
int main()
{
char a2 = { 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执行,这个函数的功能是初始化全局变量。
那么,程序大概是这样子:
//点号代表省略
......
int sub_401680()
{
.......
}
.......
#pragma init_seg(".CRT$XCC")
int g_hook = sub_401680();
int main()
{
.......
}
https://static.52pojie.cn/static/image/hrline/4.gif
复盘程序的整个流程:
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()。https://static.52pojie.cn/static/image/hrline/4.gif
优秀, 反手一个收藏, 等哪年想起来了再学习哈哈哈 每到一个新的机制是,调试器都有一次处理的机会 {:1_893:}学习学习,每天进步一小点 感谢分享,学习中 慢慢研究吧!
附件在这儿。
没基础的熟悉三环和内核的异常流程就可以了,就是学起来有点枯燥 谢谢楼主分析,试着学习,再次谢谢 分析得头头是到 想要看懂理解需要哪些基础 alongzhenggang 发表于 2021-4-17 07:47
想要看懂理解需要哪些基础
熟悉三环和内核的异常流程就可以了{:301_992:} 学习中,对我这样的新手来说有点难度 舒默哦 发表于 2021-4-17 10:14
熟悉三环和内核的异常流程就可以了
纯小白:@没基础的,不过谢谢,去试着学习 火哥牛逼! 哥比彩砖还炫 发表于 2021-4-18 14:27
火哥牛逼!
{:301_983:}火锅是谁 学习中,对我这样的新手来说有点难度