学破解第206天,《IDA与OD同步调试及常见反调试手段》学习
前言:坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:(https://www.52pojie.cn/thread-1791705-1-1.html)
**立帖为证!--------记录学习的点点滴滴**
## 0x1 联调准备
1.在分析ctf题目时,经常需要动态和静态分析结合判断函数的作用,参数的传递,堆栈变化等等,这个时候如果有一款插件能够让OD和IDA同时调试,会大大减轻我们来回调试的工作量,也更方便我们进行对比。
2.ret-sync 是一组插件,可帮助将调试会话 (WinDbg/GDB/LLDB/OllyDbg2/x64dbg) 与 IDA/Ghidra/Binary Ninja 反汇编程序同步。
3.下载地址:(https://github.com/bootleg/ret-sync),我们只需要IDA和od支持,可以打开文件夹复制链接到:(https://minhaskamal.github.io/DownGit/#/home),然后就可以很方便的下载了我们想要的文件,不需要整个代码库下载下来。
4.安装方式:
```
把ext_ida文件夹复制到IDA目录
把ext_olly1里编译的dll复制ollydbg的plugin里
打开IDA拖入调试程序, Alt+F7选择SyncPlugin.py (好像要退出一次, 必须有idb)
打开OD, 拖入程序, 点击OD窗口标题, Alt+s开启同步(Alt+U关闭同步)
```
5.折腾了一晚上几个小时还不行,明天晚上直接装论坛7.7的IDA,自带全插件。
## 0x2 实战分析
1.以BUUCTF上的Crackme为例,进来后F5看看代码:
```
int wmain()
{
FILE *v0; // eax
FILE *v1; // eax
char v3; //
char v4; // BYREF
char Format; // BYREF
char v6; // BYREF
char v7; // BYREF
printf("Come one! Crack Me~~~\n");
memset(v7, 0, sizeof(v7));
memset(v6, 0, sizeof(v6));
while ( 1 )
{
do
{
do
{
printf("user(6-16 letters or numbers):");
scanf("%s", v7);
v0 = (FILE *)sub_4024BE();
fflush(v0);
}
while ( !(unsigned __int8)sub_401000(v7) );
printf("password(6-16 letters or numbers):");
scanf("%s", v6);
v1 = (FILE *)sub_4024BE();
fflush(v1);
}
while ( !(unsigned __int8)sub_401000(v6) );
sub_401090(v7);
memset(Format, 0, sizeof(Format));
memset(v4, 0, sizeof(v4));
v3 = ((int (__cdecl *)(char *, char *))loc_4011A0)(Format, v4);
if ( (unsigned __int8)sub_401830(v7, v6) )
{
if ( v3 )
break;
}
printf(v4);
}
printf(Format);
return 0;
}
```
2.通过题目描述和运行程序,可以知道要求输入用户名welcomebeijing,密码也是6-16位字符,如果不正确会提示Please try again,因此可知v7是welcomebeijing,v6是输入的密码。
![https://s1.ax1x.com/2023/06/02/pCS30II.jpg](https://s1.ax1x.com/2023/06/02/pCS30II.jpg)
```
Come one! Crack Me~~~
user(6-16 letters or numbers):welcomebeijing
password(6-16 letters or numbers):welcomebeijing
Please try again
user(6-16 letters or numbers):
```
3.接下来开始同步调试,od插件目录放编译后的dll,发现是动态基址,所以需要用study PE固定基址,打开ida sync,od启动插件,发现ida变黄了,说明同步成功了,接下来直接运行到输入用户名和密码后的while循环处,密码这里输入6个a,然后看看v6和v7做了什么处理。
```
while ( !(unsigned __int8)sub_401000(v6) );
sub_401090(v7);
00401D57 .50 push eax ;eax就是我输入的6个a
00401D58 .E8 A3F2FFFF call crack2.00401000
00401D5D .83C4 04 add esp,0x4 ;如果al不为0,那么这里就失败,需要重新输入密码
00401D60 .0FB6C8 movzx ecx,al
00401D63 .85C9 test ecx,ecx ;crack2.00402331
00401D65 .75 05 jnz short crack2.00401D6C
00401D67 .^ E9 4EFFFFFF jmp crack2.00401CBA
```
4.通过上面的代码和IDA中同步显示的流程图可以知道如果call 401000后,没有清空eax寄存器,那么程序流程就跳到重新输入用户名和密码的地方去了,ida进去看看,发现就是判断我们输入的密码是不是字母或数字,如果没有问题就执行下一句代码处理v7。
```
v2 = strlen(a1);
for ( i = 0; i < v2; ++i )
{
if ( !isalnum(a1) )
return 0;
}
```
5.进来看一看V7的call crack2.00401090处理,光频肉眼看不出来啥,OD F7进去单步慢慢看,可知第一个for循环416050这个地址存了0-255,然后do while循环没看懂具体干了啥,接下来看while循环,result=v3,所以里面的if必须成立,然后发现不做任何处理,最后返回值也是0,不会影响逻辑。
```
_BYTE *__cdecl sub_401090(_BYTE *a1)
{
_BYTE *result; // eax
int v2; //
int v3; //
_BYTE *v4; //
int i; //
char v7; //
char v8; //
unsigned __int8 v9; //
for ( i = 0; i < 256; ++i )
byte_416050 = i;
v2 = 0;
v9 = 0;
v3 = 0;
result = a1;
v4 = a1;
do
LOBYTE(result) = *v4;
while ( *v4++ );
while ( v2 < 256 )
{
v8 = byte_416050;
v9 += v8 + a1;
v7 = byte_416050;
++v3;
byte_416050 = v8;
byte_416050 = v7;
result = (_BYTE *)v3;
if ( v3 >= v4 - (a1 + 1) )
v3 = 0;
++v2;
}
return result;
}
```
6.跳出call,继续往后看,两个数据拷贝操作,call 004011A0执行完,出现了提示语成功和失败,然而并没有看到相关运算,所以推测只是把字符取出来,一会运算后进行提示,那么再往下看就是一个非常关键的if判断了,因为执行完这个if后才能跳出循环,而且call 00401830同时将用户名和密码传参进去,肯定是关键处理,进去看看
```
bool __cdecl sub_401830(int a1, const char *a2)
{
int v3; //
int v4; //
int v5; //
unsigned int v6; //
char v7; //
char v8; //
char v9; //
unsigned __int8 v10; //
unsigned __int8 v11; //
char v12; //
int v13; // BYREF
char v14; // BYREF
char v15; // BYREF
char v16; // BYREF
v4 = 0;
v5 = 0;
v11 = 0;
v10 = 0;
memset(v16, 0, sizeof(v16));
v14 = 0;
memset(v15, 0, sizeof(v15));
v9 = 0;
v6 = 0;
v3 = 0;
while ( v6 < strlen(a2) )
{
if ( isdigit(a2) )
{
v8 = a2 - 48;
}
else if ( isxdigit(a2) )
{
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
a2 = 34;
v8 = (a2 | 0x20) - 87;
}
else
{
v8 = ((a2 | 0x20) - 97) % 6 + 10;
}
__rdtsc();
__rdtsc();
v9 = v8 + 16 * v9;
if ( !((int)(v6 + 1) % 2) )
{
v15 = v9;
v9 = 0;
}
++v6;
}
while ( v5 < 8 )
{
v10 += byte_416050[++v11];
v12 = byte_416050;
v7 = byte_416050;
byte_416050 = v12;
byte_416050 = v7;
if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
v12 = v10 + v11;
v16 = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15;
if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
{
v10 = -83;
v11 = 43;
}
sub_401710(v16, a1, v5++);
v4 = v5;
if ( v5 >= (unsigned int)(&v15 - v15) )
v4 = 0;
}
v13 = 0;
sub_401470(v16, &v13);
return v13 == 0xAB94;
}
```
7.似乎发现了不得了的东西:反调试,先暂停分析,补一补反调试知识。
## 0x3 反调试
1.IsDebuggerPresent函数
```
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,
函数返回0;如果调试附加了进程,函数返回一个非零值。
```
2.CheckRemoteDebuggerPresent函数
```
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被
调试,通过传递自身进程句柄还可以探测自身是否被调试。
```
3.NtQueryInformationProcess函数
```
这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。它的第一个参数是进程句柄,
第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就
会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有
ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。
例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
```
4.GetLastError函数
```
使用SetLastError函数,将当前的错误码设置为一个任意值。
如果进程没有被调试器附加,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。
但如果进程被调试器附加,多数调试器默认的设置是捕获异常后不将异常传递给应用程序,这时GetLastError获取的错误码应该没改变。
对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。
如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。
0x57就是指ERROR_INVALID_PARAMETER,可以通过GetLastError() != 0x57判断
```
5.ZwSetInformationThread函数
```
ZwSetInformationThread拥有两个参数,第一个参数用来接收当前线程的句柄,第二个参数表示线程信息类型,
若其值设置为ThreadHideFromDebugger(0x11),使用语句ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
调用该函数后,调试进程就会被分离出来。该函数不会对正常运行的程序产生任何影响,但若运行的是调试器程
序,因为该函数隐藏了当前线程,调试器无法再收到该线程的调试事件,最终停止调试。
还有一个函数DebugActiveProcessStop用来分离调试器和被调试进程,从而停止调试。两个API容易混淆,需要牢记它们的区别。
```
6.检测BeingDebugged属性
```
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。
这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。
```
7.检测ProcessHeap属性
```
Reserved数组中一个未公开的位置叫作ProcessHeap,它被设置为加载器为进程分配的第一个堆的位置。
ProcessHeap位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。
这些属性叫作ForceFlags和Flags。在Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处;
在Windows 7系统中,对于32位的应用程序来说ForceFlags属性位于堆头部偏移量0x44处
```
8.还有一些例如易语言时钟检测,也能进行反调试。
## 0x4 继续分析
1.通过学习反调试知识,可知此处有三个反调试,可以直接手动nop掉对应代码或者不管它,论坛的OD能过掉很多反调试。
```
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
```
2.这个函数肯定也是要返回true的,那么最后return语句的前一句就很关键,可以看到经过诸多判断后,v13是0,经过运算后要等与0125624,那么v16的来源就很关键,但是这中间都经历了一个sub_401710函数,也需要关注。
```
v13 = 0;
((void (__cdecl *)(char *, int *))sub_401470)(v16, &v13);
return v13 == 0125624;
可以看到v16在这里得到初值:
v16 = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15;
```
3.sub_401470函数进去可以看到如下信息(去掉反调试和干扰代码整理后),可以看到a2(第二个参数就是传递来的v16了)就是不断进行比较,接下来大胆假设a2就是相等的,那么v16就是dbappsec了。
```
unsigned int *__usercall sub_401470@<eax>(int a1@<ebx>, _BYTE *a2, unsigned int *a3)
{
int *_EAX; // eax
char v5; // al
char _AL; // al
unsigned int *result; // eax
if ( *a2 != 'd' )
*a3 ^= 3u;
else
*a3 |= 4u;
if ( a2 != 'b' )
{
*a3 &= 0x61u;
_EAX = (int *)*a3;
}
else
{
_EAX = (int *)a3;
*a3 |= 0x14u;
}
if ( a2 != 'a' )
*a3 &= 0xAu;
else
*a3 |= 0x84u;
if ( a2 != 'p' )
*a3 >>= 7;
else
*a3 |= 0x114u;
if ( a2 != 'p' )
*a3 *= 2;
else
*a3 |= 0x380u;
if ( a2 != 's' )
{
v5 = (char)a3;
*a3 ^= 0x1ADu;
}
else
{
*a3 |= 0xA04u;
v5 = (char)a3;
}
if ( a2 != 'e' )
*a3 |= 0x4Au;
else
*a3 |= 0x2310u;
if ( a2 != 'c' )
{
*a3 &= 0x3A3u;
return (unsigned int *)*a3;
}
else
{
result = a3;
*a3 |= 0x8A10u;
}
return result;
}
```
4.byte_416050是一个动态数组,我们调试看一看,v16到底怎么来的,OD往下翻,00401A4E这一行对应的while ( v5 < 8 ),然后继续往下慢慢看,对比ida里面的代码看,可知这是一个反调试,if后面就是v16赋值的那一句了。
```
00401A4E >83BD E4FDFFFF>cmp dword ptr ss:,0x8
00401A55 . |0F8D B0010000 jge crack2.00401C0B
00401A5B . |0FB695 F6FDFF>movzx edx,byte ptr ss:
00401A62 . |83C2 01 add edx,0x1
00401A65 . |8895 F6FDFFFF mov byte ptr ss:,dl
00401A6B . |0FB685 F5FDFF>movzx eax,byte ptr ss:
00401A72 . |0FB68D F6FDFF>movzx ecx,byte ptr ss:
00401A79 . |0FB691 506041>movzx edx,byte ptr ds:
00401AE8 . /74 16 je short crack2.00401B00 ;
00401AEA . |0FB695 F6FDFF>movzx edx,byte ptr ss:
00401AF1 . |0FB685 F5FDFF>movzx eax,byte ptr ss:
00401AF8 . |03D0 add edx,eax
00401AFA . |8895 F7FDFFFF mov byte ptr ss:,dl
00401B00 > \0FB68D F7FDFF>movzx ecx,byte ptr ss:
```
5.从401B00这里开始就很关键了,一定到定位到v16赋值的地方,下面逐行解读汇编代码
```
00401B00 > \0FB68D F7FDFF>movzx ecx,byte ptr ss: ;ecx=v12
00401B07 .0FB695 F2FDFF>movzx edx,byte ptr ss: ;edx=v7
00401B0E .03CA add ecx,edx ;ecx=v7+v12
00401B10 .888D F7FDFFFF mov byte ptr ss:,cl
00401B16 .0FB685 F7FDFF>movzx eax,byte ptr ss: ;eax=ecx
00401B1D .8A88 50604100 mov cl,byte ptr ds:
00401B23 .888D F7FDFFFF mov byte ptr ss:,cl
00401B29 .8B95 D8FDFFFF mov edx,dword ptr ss: ;edx=v4
00401B2F .0FB68415 FCFD>movzx eax,byte ptr ss: ;eax=v15
00401B37 .0FB68D F7FDFF>movzx ecx,byte ptr ss: ;ecx=byte_416050[(unsigned __int8)(v7 + v12)]
00401B3E .33C1 xor eax,ecx ;v16=eax^ecx
```
6.我们已知v16是dbappsec,通过F2下断点能读出ecx的值0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd,v15就是密码。
```
if ( isdigit(a2) )
{
v8 = a2 - 48;
v9 = v8 + 16 * v9;
if ( !((int)(v6 + 1) % 2) )//v6如果为奇数,条件成立
{
v15 = v9; v15就是前一位*16+后一位
v9 = 0;
}
++v6;
```
7.所以最后可写出如下脚本,用C实在不知道咋转16进制,懵逼,还好java基础也懂一点,勉强写出来了。
```
package ctf;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Iterator;
public class test01 {
public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
int arr[] = {0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd};
char password[] = {'d','b','a','p','p','s','e','c'};
byte flag[] = new byte;
for (int i = 0; i < password.length; i++) {
flag=(byte) (password^arr);
}
System.out.println(bytesToHex(flag));
}
public static String bytesToHex(byte bytes[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
int number = bytes & 0xff;
String hex = Integer.toHexString(number);
if (number >= 0 && number < 16) {
sb.append("0" + hex);
} else {
sb.append(hex);
}
}
return sb.toString();
}
}
```
8.运行后输出:4eb5f3992391a1ae,放到md5在线加密网站,加密得到flag:flag{d2be2981b84f2a905669995873d6a36c}
## 0x5 参考资料:
1.(http://strivemario.work/archives/b40b8192.html)
2.(https://blog.csdn.net/qq_39542714/article/details/106834898)
3.(https://wwtc.lanzoum.com/iV3sJ0y0rfzc)
4.(https://www.52pojie.cn/thread-1584115-1-1.html)
谢谢分享,我今天也用到10转16了.
_itoa_s(待转换十进制数,存放转换结果的数组或指针,16); 能够坚持下来,真心非常厉害,赞一个{:1_921:},讲得也非常不错。 不错,能坚持学习很好 大佬就是大佬,牛逼