静态反调试

静态反调试技术只需要在程序运行之前进行一次破解即可解除全部限制,且不需要二次操作。而且静态反调试技术对于操作系统的依赖性非常大,相同的反调试技术在不同的操作系统表现可能不同。

参考:

https://xz.aliyun.com/t/5685

https://xz.aliyun.com/t/5732/

https://xz.aliyun.com/t/6133


执行顺序

__attribute__((constructor)) 属性修饰函数可使该函数先于 main 函数前执⾏。

.init_array 段的函数执行时机早于 main 函数。

ptrace

ptrace即process tracer(进程跟踪),ptrace系统调用是 Linux 提供的一个调试进程的工具,其提供了一种可以观察和控制另一个进程(tracee)的方法,并检查和更改tracee的存储器和跟踪器,主要用于实施断点调试和系统调用跟踪,linux下常见的调试工具GDB原理就是基于ptrace。

ptrace检测是否处于调试状态。

加壳

UPX

手动脱壳

  1. x32dbg打开,点击运行(F9到输入点),跳过ntdll的加载,等到程序输出特征字符串后,定位字符串
  2. 追踪过去后往上翻,找到运行入口
  3. 然后用插件Scylla来dump出这段,dump到一个程序,然后ida就可以正常分析了

花指令

去花

IDA - 用 U 取消对应代码段的定义,然后在跳转位置出用热键 C 重新定义即可,中间的垃圾代码nop掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import idautils
def nop(addr,endaddr):
while(addr<endaddr):
PatchByte(addr,0x90)
addr+=1
def undefine(addr,endaddr):
while addr<endaddr:
MakeUnkn(addr,0)
addr+=1
def dejunkcode(addr,endaddr):
while addr<endaddr:
MakeCode(addr)
# 匹配模版
if Byte(addr)==0xe8 and Byte(addr+6)==0x12 and Byte(addr+7)==0x83:
next=addr+10
nop(addr,next)
addr=next
continue
addr+=ItemSize(addr)
dejunkcode(0x00401000,0x00411E40)

jz / jnz

1
2
3
4
5
6
7
start = 0x4019FC
end = 0x401AD8
while (start < end):
if ((get_wide_byte(start) == 0x74) and (get_wide_byte(start + 2) == 0x75)):
ida_bytes.patch_byte(start + 4, 0x90)
start += 1
print('OK!')

SMC

SMC(自修改代码,Self-Modifying Code),就是在真正执行某一段代码时,程序会对自身的该段代码进行自修改,只有在修改后的代码才是可汇编,可执行的。在程序未对该段代码进行修改之前,在静态分析状态下,均是不可读的字节码,IDA之类的反汇编器无法识别程序的正常逻辑。是一种反调试代码技术。

还原方法

  1. 首先确定需要加密的代码部分;
  2. 通过vs查看反汇编(或者OD、IDA等)可以确定对应部分的反汇编代码;
  3. 将这段代码手工用16进制编辑器覆盖为加密后的数据;
  4. 程序执行时则运行事先写好的代码先进行解密再运行。

参考

https://blog.csdn.net/qq_41923479/article/details/80377708
https://blog.csdn.net/palmer9/article/details/105034093

脚本

IDA - File - Script file

1
2
3
4
5
6
7
8
9
10
11
#include <idc.idc>

static main()
{
auto addr = 0x00401216;
auto i = 0;
for(i=0;i<=0x43E;i++)
{
PatchByte(addr+i,Byte(addr+i)^Byte(0x00409080+i));
}
}

MOV混淆

剑桥大学的Stephen Dolan证明了x86的mov指令可以完成几乎所有功能了(可能还需要jmp),其他指令都是“多余的”。这种混淆的特征就是:汇编代码的汇编指令几乎全部就是MOV。

1、 字符串的搜索是给我们最好的提示。

2、 MOV混淆是不会混淆函数的逻辑的。因此函数的逻辑还是不变的。

3、 大多数汇编代码的意思是可以猜测的。可以大概推测出具体操作了什么。

在这里提供一个开源的工具,是针对这的MOV混淆的工具:https://github.com/kirschju/demovfuscator,这个工具可以很好的帮助我们分析MOV混淆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<idc.idc>
static isAscii(p)
{
if(Byte(p)<='9' && Byte(p)>='0')
return 1;
if(Byte(p)<='z' && Byte(p)>='a')
return 1;
if(Byte(p)<='Z' && Byte(p)>='A')
return 1;
if(Byte(p)=='}' || Byte(p)=='{' || Byte(p)=='_' || Byte(p)=='@' || Byte(p)=='!' ||
Byte(p)=='#' || Byte(p)=='&' || Byte(p)=='*')
return 1;
return 0;
}
static main()
{
auto start=0x0804824C,end=0x08053FA9; //代码段起止地址
auto point=start;
auto str="";
while(point<=end)
{
if(isAscii(point) && Byte(point+1)==0 && Byte(point+2)==0 && Byte(point+3)==0)
{
Message("%X %c\n",point,Byte(point));
str=str+Byte(point);
}
point=point+1;
}
Message("%s\n",str);
}

虚拟机(VM)

虚拟机:自己定义一套指令,在程序中能有一套函数和结构解释自己定义的指令并执行功能。

vm(虚拟机保护)是一种基于虚拟机的代码保护技术。他将基于x86汇编系统中的可执行代码转换为字节码指令系统的代码。来达到不被轻易篡改和逆向的目的。

简单来说就是出题人通过实现一个小型的虚拟机,把程序的代码转换为程序设计者自定义的操作码(opcode)然后在程序执行时通过解释操作码,执行对应的函数,从而实现程序原有的功能。

image-20240314210238634

VMRUN:虚拟机入口函数

dispatcher:调度器,用于解释opcode,并选择对应的Handler函数执行,当Handler执行完后会跳回这里,形成一个循环。

opcode:程序可执行代码转换成的操作码

Handler:各种功能对象模块

一般步骤

分析VM结构->分析opcode->编写parser->re算法

  • VM结构常见类型:基于栈、基于队列、基于信号量

  • opcode:

    与VM数据结构对应的指令 :push pop

    运算指令:add、sub、mul

OLLVM混淆(LLVM)

  1. D810插件

    https://github.com/joydo/d810

  2. 基于SnowGirlsdeflat,利用angr框架实现去除控制流平坦化。

    python3 deflat.py -f check_passwd_x8664_flat --addr 0x400530

  3. LLVM多种格式

    xx.c (源代码)

    xx.ll (LLVM字节码的文本表示)

    xx.s (机器汇编码表示的汇编文件)

    xx.bc (LLVM字节码的二进制形式)

    xx.out (可执行的二进制文件)

    20210428135001768

常用转换:

1
2
3
4
5
6
7
clang test.bc -o test

llvm-dis test.bc -o test.ll
llvm-as test.ll -o test.bc
llc test.bc -o test.s
gcc hello.s -o hello
gcc -c -fPIE hello.s -o hello

TLS

ThreadLocalStorage(TLS),是Windows为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。

而当Coder选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。

TLS回调函数的执行,与数据的初始化,都在程序入口点之前执行,TLS是整个程序最早运行的地方,所以可以用这个特性,执行一些特殊操作。然后在最后程序退出时还会调用一次。

TLS反调试:

往定义的TLS函数里写代码。 OD调试程序的时候,要先找到程序的入口点,跟拿着钥匙开门一样,找到孔,才能插钥匙。TLS回调函数,由于可以在程序中最早运行的时候执行,可以把在调试器检测到入口点之前,它自己检测是否存在调试。例如,检测加载环境是不是“OllyDbg”等。

解法:

可以通过patch掉TLS回调来反反调试,但既然它用到了isDebuggerPresent,我们也可以通过nop填充来让调试能够继续运行。

Tls_callback() 函数:

第二个参数Reason可以有四个值,分别是 DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH,这四个值分别对应的时期是进程启动、线程启动、线程退出、进程退出,具体的值分别对应1,2,3,0。几次调用TLS回调函数的顺序也是1-2-3-0即DLL_PROCESS_ATTACH->DLL_THREAD_ATTACH->DLL_THREAD_DETACH->DLL_PROCESS_DETACH。

异常

异常触发后的顺序

优先顺序:调试器 > VEH > SEH

SEH

如果程序引发了异常,程序将利用Windows的一种异常处理机制 SEH(Structured Exception Handling)机制处理异常,在处理过程中对程序进行一定的修改。常见形式:__try{...} __except(filter){...}

由于SEH中的很多代码不会被IDA反编译出来,所以它常常被用来反静态分析:程序员在try块中的某些情况下故意触发一些异常,来执行except块中他们想要隐藏起来的代码。

简而言之,就像它们的名字一样,程序先执行try块中的逻辑,如果try中产生了一些异常的情况(可以简单理解为程序发生了一些意外),就会执行except中的代码来处理异常;反之,如果try块中没有产生异常,except块就不会被执行。

VEH

VEH,向量化异常处理(Vectored Exception Handling),它优先于SEH执行,当VEH处理了异常就不会提交给SEH。

VEH是全局链表,每个VEH按顺序被调用,VEH可返回两个值:EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTION。EXCEPTION_CONTINUE_SEARCH把异常交给下一个VEH处理,即不处理;EXCEPTION_CONTINUE_EXECUTION 则表示处理该异常。

可以通过AddVectoredExceptionHandler函数和RemoveVectoredExceptionHandler函数进行插入VEH和移除VEH。

SEH是基于线程的,而VEH是基于进程的。

因为很清楚的可以看到SEH的数据结构是保存在栈空间的,直接在函数体内用push来处理,而VEH是以双链表的形式保存在堆里面。