Give_a_try
近端时间在学习逆向方面的知识,经过了差不多半个月的入门学习,终于着手分析了第一道CTF
逆向题目。
这道题目涵盖了包括**花指令*,*TLS
反调试**,*随机数算法以及算法逆向*。
知识铺垫
花指令
花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法,在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行,使得程序无法很好地进行反编译,难以理解程序内容,达到混淆视听的效果。
这个题目中使用到的是**不可执行的花指令,不可执行花指令指的是这部分花指令代码在程序的正常执行过程中不会被执行。
一开始学习花指令的时候,只知道花指令的核心思想是构造恒成立跳转,中间插无效代码
,但是通过这次的逆向分析,明白不一定只有JCC
指令可以编写花指令,只要是修改EIP
的指令都有能力有机会制造花指令。
TLS
TLS(Thread Local Storage)是线程局部存储,主要是为了解决多线程中的变量同步的问题。
TLS在安全领域中,常被用来处理如**反调试*、抢占执行等操作,当一个程序被调用的时候,先执行TLS函数,再跳转到程序的*IMAGEBASE
执行PE文件结构。这时候就可以在TLS函数中添加代码来防止程序被调试。
```C
include "pch.h"
include
include
include
include "ntdll/ntdll.h"
_IMAGE_TLS_DIRECTORY32
// 声明需要使用到TLS
pragma comment(linker,"/INCLUDE:__tls_used")
DWORD isDebug = 0;
void NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
// 不接受内核调试
NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
// 检查是否被调试,为0表示没有被调试,不为零表示被调试。
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, (PVOID)&isDebug, sizeof(DWORD), NULL);
}
}
void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (Reason == DLL_PROCESS_ATTACH)
{
// 不接受内核调试
NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);
// 检查是否被调试,为0表示没有被调试,不为零表示被调试。
NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, (PVOID)&isDebug, sizeof(DWORD), NULL);
}
}
int main(void)
{
MessageBoxA(NULL, "Main函数执行", "提示", MB_OK);
system("pause");
}
// 新建一段数据放到TLS目录表中
pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK, TLS_CALLBACK2, NULL };
pragma data_seg()
```
使用PE工具进行查看,就会发现多出的TLS目录
32位中TLS结构体如下:
C
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData; /* tls节区的起始地址 */
DWORD EndAddressOfRawData; /* tls节区的最终地址 */
DWORD AddressOfIndex; /* tls节区的索引 */
DWORD AddressOfCallBacks; /* 指向回调函数的指针 */
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
此处只需要关注AddressOfCallBacks
,程序在该指针放入了将要执行的函数地址。
伪随机数
本题用到了srand()
和rand*()
两个函数,这两个函数其实是伪随机数,因为一旦设置了种子,那么生成的随机数序列就是固定的。
```C
define _A 214013LL
define _B 2531011LL
void mysrand(int x){
a = x;
}
int myrand(){
return ((a = a *_A + _B) >> 16)&0x7fff;
}
mysrand(0);
srand(0);
while(1){
printf("%d %d\n", rand(), myrand());
getchar();
}
```
此时会发现两者的输出结果是一样的。
PE工具查看程序
放入到PE工具后,发现程序有TLS目录,点击寻找AddressOfCallBacks
地址。根据了解此处写的就是回调函数的地址。
验证是否为回调函数地址,让程序中断在系统断点位置,而不是中断在OEP
位置。OD设置:选择->调试设置(ALT+Q)->事件->设置第一次暂停于系统断点
。
运行程序后,在OD的内存窗口,Ctrl+G
跳转到00404032
位置,发现存储的是00402000
。
借助OD的插件StrongOD
,选择Break On Tls
。
重新加载程序,发现程序停在了00402000
位置,正好是上面00404032
地址存储的数值。(重新运行的时候记得吧程序断点改回主函数
)
去除花指令
承接上文,找到回调函数入口后,使用F8
单步步过,跟踪程序。
asm
00402006 E8 00000000 call Give_a_t.0040200B
0040200B 810424 17000000 add dword ptr ss:[esp],0x17
00402012 C3 retn
这三句是作者编写花指令的核心思想,首先call Give_a_t.0040200B
就是跳转到0040200B
,并将0040200B
入栈;然后执行add dword ptr ss:[esp],0x17
,就是把0x0040200B + 0x17
,也就是0x402022
。实际效果就是jmp 402022
,也就是说0040200B-402021
全都是花指令,使用nop
进行填充。
继续向下执行
asm
0040202C E8 01000000 call Give_a_t.00402032
00402031 c2 8304 retn 0x483
此处使用call
指令跳转到00402032
,而第二行出现的是00402031
,那么就可以将00402031
修改为nop
。
nop
完后,作者梅开二度
asm
0040202C E8 01000000 call Give_a_t.00402032
00402031 90 nop
00402032 830424 06 add dword ptr ss:[esp],0x6
00402036 C3 retn
先call
到00402032
,将00402031
入栈,然后再做加法0x402031+0x6
,结果是0x402037
,那么40202C - 402036
直接就是花指令,直接nop
填充。
继续往下,发现作者一个模板使用了许多许多次,大致模板为E8 01 00 00 00 ?? ?? ?? ?? ?? C3
。
既然发现了模板就可以选择写脚本进行批量去除,也可以使用Ctrl+B
的模糊搜索进行查找再去除。
处理了三个类似花指令后,发现程序读取了PEB
结构体。但是我还只是个小白,不知道他在干嘛。继续往下。
还是同样模板的花指令,接着程序读取了当前线程句柄,然后就调用了NtSetInformationThread
函数。
asm
0040208A 6A 00 push 0x0
0040208C 6A 00 push 0x0
0040208E 6A 11 push 0x11
00402090 50 push eax
00402091 E8 A0F2FFFF call <jmp.&ntdll.NtSetInformationThread>
写成C语言:
C
NtSetInformationThread(eax, 0x11, 0, 0);
// 根据汇编代码可以得知eax就是当前线程句柄
// 0x11 转换成十进制就是 17,也就是ThreadHideFromDebugger
继续往下,再去除一个花指令,程序再次读取当前线程句柄,并将线程句柄的值转存到ebx
寄存器中,接着调用NtQueryInformationProcess
函数。
asm
004020B3 6A 00 push 0x0
004020B5 6A 04 push 0x4
004020B7 68 6C404000 push Give_a_t.0040406C
004020BC 6A 07 push 0x7
004020BE 53 push ebx
004020BF E8 6CF2FFFF call <jmp.&ntdll.NtQueryInformationProcess>
写成C语言:
C
NtQueryInformationProcess(ebx, 0x7, 0040406C, 0x4, 0);
// ebx 是从 eax 传递过去的,存储的是当前线程句柄
// 0x7 转换成十进制 11, 也就是ProcessDebugPort
// 0040406C 等同于全局变量 isDebug
// 0x4 是四个字节
asm
004020C4 83F8 00 cmp eax,0x0
004020C7 0F42FE cmovb edi,esi
然后进行查询,如果发现是调试中,则返回0xffffffff
,但是因为使用的OD又反调试插件,所有此处返回的是0x0
,也就是没有识别到程序正在被调试。
继续往下,再去一个花指令,然后动态Patch TLS回调函数
,作者的思路是真的牛逼!!!
asm
004020D5 833D 6C404000 0>cmp dword ptr ds:[0x40406C],0x0
004020DC 0F45FE cmovne edi,esi
004020EA 893D 36404000 mov dword ptr ds:[0x404036],edi ; ntdll.7718BAD0
执行完后找到第二个回调函数地址004020f7
。
Ctrl+G
跳转到004020f7
,F2
下断点,将程序运行到第二个回调函数位置。
继续向下,又发现作者的祖传操作,直接将004020FD - 00402127
之间的花指令nop
掉。
asm
004020FD E8 00000000 call Give_a_t.00402102
00402102 810424 25000000 add dword ptr ss:[esp],0x25
00402109 C3 retn
后面作者有进行了异或算法,说明如果再反调试阶段出现问题,那么即使输入正确的验证码也无法返回正确结果。
脚本运行方式
手动去除花指令是真的辛苦,这边附上个脚本...但是手动去除可以顺便把程序大体读懂,只不过遇到大程序就完蛋。
写一个名为DeJunk.txt
的文件(名字其实无所谓,需要知道是txt
后缀即可),将如下命令输入。
```
// 从Eip 的位置查找第一个特征码
find eip,#E80000000081042417000000C3576174636820757220737465702100#
// 判断是否找到,找到返回花指令的开始地址
cmp $RESULT,0
// 如果没有找到就跳出
je exit
// 如果找到就填充为nop
mov [$RESULT],#90909090909090909090909090909090909090909090909090909090#
// 从Eip位置查找下一个特征码
find eip,#E80000000081042425000000C354686520666C616720626567696E7320776974682022666C61677B2200#
// 判断是否找到
cmp $RESULT,0
// 如果没有找到就跳出
je exit
// 如果找到就填充为nop
mov [$RESULT],#909090909090909090909090909090909090909090909090909090909090909090909090909090909090#
// 定义一个循环的标签,因为下面这个特征码不止一次,所以需要循环多次进行查找
loop:
// 从Eip的位置查找
find eip,#E801000000??????????C3#
// 判断是否找到
cmp $RESULT,0
// 如果没有找到就跳出
je exit
// 如果找到就填充为nop
mov [$RESULT],#9090909090909090909090#
// 继续循环
jmp loop
exit:
MSG "over"
ret
```
进入OD,选项->ODbgScript
-> 运行脚本。如果需要调试脚本,也可以选择脚本运行窗口
,按Tab
即可单步调试。功能很强大
IDA逆向
虽然前面通过汇编已经大部分分析完成,但是还是需要IDA逆向出伪代码。
*回调函数伪代码*
第一个TLS的功能是反调试,和检测调试状态,最后,动态修复第二个TLS回调函数的地址。
```C
void __stdcall TlsCallback_0(int a1, int a2, int a3)
{
int (__stdcall v3)(int, int, int); // edi
unsigned int v4; // et0
void v5; // eax
void *v6; // eax
unsigned int v7; // [esp+0h] [ebp-10h]
if ( a2 == 1 )
{
v3 = sub_4020F7;
v4 = __readeflags();
v7 = v4;
if ( v4 & 0x100 )
v3 = 0;
__writeeflags(v7);
if ( (_DWORD )&NtCurrentPeb()->InheritedAddressSpace & 0x10000 )
v3 = 0;
v5 = (void )GetCurrentThread();
NtSetInformationThread(v5, ThreadHideFromDebugger, 0, 0);
v6 = (void )GetCurrentProcess();
NtQueryInformationProcess(v6, ProcessDebugPort, &ReturnLength, 4u, 0);
if ( ReturnLength )
v3 = 0;
dword_404036 = (int)v3;
}
}
```
第二个TLS回调函数的功能就是修改了isdebug
这个全局变量的值,然后给TLS数组的第三个位置填0,所以就没有第三个TLS回调了。
```C
void __stdcall sub_4020F7(int a1, int a2, int a3)
{
void *v3; // eax
if ( a2 == 1 )
{
v3 = (void )GetCurrentProcess();
NtQueryInformationProcess(v3, ProcessDebugPort, &ReturnLength, 4u, &ReturnLength);
ReturnLength ^= 0x31333337u;
dword_40403A = 0;
ReturnLength ^= (unsigned __int8 *)start;
}
}
```
算法逆向
将程序拖入IDA后 shift+F12
打开字符串窗口。
可以看到作者很良心啊,说了句废话,The flag begins with “flag{”
。但是事实证明,这不是句废话,并且如果没有这句话,就根本做不出来。
花指令已经去掉,那么找到核心算法就很简单了,这边直接给出核心算法的伪代码。
C
if ( strlen(Str) != 42 ){
// 判断flag是否为42位,此处也有小坑,有的操作系统只能输入41位。
return MessageBoxA(0, aThinkAgain, 0, 0);
}
v2 = 0;
v3 = *Str;
v4 = Str + 1;
while ( v3 )
{
// 将各个字符串相加,结果保存在v2
v2 += v3;
v3 = *v4++;
}
// 将v2与全局变量异或的值作为随机数种子
srand(ReturnLength ^ v2);
for ( i = 0; i != 42; ++i )
{
v6 = (unsigned __int8)Str[i] * rand();
v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
v22 = v21 * (unsigned __int64)v21 % 0xFAC96621; // 注意,IDA的F5插件漏了这行,大坑
if ( v6 % 0xFAC96621 * (unsigned __int64)v21 % 0xFAC96621 != dword_4030B4[i] ){
// 将以上运算结果与数组中存的值比较,不相同则退出循环
break;
}
}
if ( i >= 0x2A ){
// 判断循环是否中途推出,全部执行完则弹出Congrats
result = MessageBoxA(0, aCorrect, aCongrats, 0);
}
else{
// 如果没有执行完则弹出Incorrect
result = MessageBoxA(0, aIncorrect, 0, 0);
}
return result;
}
如果知道 key 的累加和,就可以得到**随机数种子*,要想计算得到 key 的累加和,只能通过 *v6 = (unsigned __int8)key[i] * rand();
反推,反推的方法是暴力枚举srand
种子。*
*已知key以"flag"开头,则key[0] - key[3]
都是确定的,那么只要找到一个种子,使前4个字符计算后和dword_4030B4[i]
相等即可。
爆破key累加和
C
void CalcKeySum()
{
char key[5] = "flag";
// 爆破keySum
for (unsigned int keySum = 0; keySum < 255*42; keySum++)
{
srand(keySum ^ isdebug);
int i;
for (i = 0; i < 4; i++)
{
unsigned int v6 = (unsigned __int8)key[i] * rand();
unsigned int v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
unsigned int v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
unsigned int v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
unsigned int v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
unsigned int v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
unsigned int v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
unsigned int v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
unsigned int v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
unsigned int v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
unsigned int v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
unsigned int v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
unsigned int v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
unsigned int v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
unsigned int v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
unsigned int v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
unsigned int v22 = v21 * (unsigned __int64)v21 % 0xFAC96621;
if ((unsigned __int64)v6 % 0xFAC96621 * (unsigned int)v22 % 0xFAC96621 != dword_4030B4[i])
{
break;
}
}
if (i == 4)
{
printf("keySum = %X\n", keySum);
seed = keySum ^ isdebug;
break;
}
}
}
爆破种子+密码
C
void SetRand()
{
srand(seed); // 爆破得到的种子
for (int i = 0; i < 42; i++)
{
Rand[i] = rand();
for (unsigned char ch = 0; ch < 0xFF; ch++)
{
unsigned int v6 = (unsigned __int8)ch * Rand[i];
unsigned int v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
unsigned int v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
unsigned int v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
unsigned int v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
unsigned int v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
unsigned int v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
unsigned int v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
unsigned int v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
unsigned int v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
unsigned int v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
unsigned int v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
unsigned int v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
unsigned int v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
unsigned int v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
unsigned int v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
unsigned int v22 = v21 * (unsigned __int64)v21 % 0xFAC96621;
if (v6 % 0xFAC96621 * (unsigned __int64)v22 % 0xFAC96621 == dword_4030B4[i])
putchar((char)ch);
}
}
}
End
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论