前言:
坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源: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; // [esp+3h] [ebp-405h]
char v4[256]; // [esp+4h] [ebp-404h] BYREF
char Format[256]; // [esp+104h] [ebp-304h] BYREF
char v6[256]; // [esp+204h] [ebp-204h] BYREF
char v7[256]; // [esp+304h] [ebp-104h] 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是输入的密码。
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[i]) )
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; // [esp+Ch] [ebp-18h]
int v3; // [esp+10h] [ebp-14h]
_BYTE *v4; // [esp+14h] [ebp-10h]
int i; // [esp+18h] [ebp-Ch]
char v7; // [esp+20h] [ebp-4h]
char v8; // [esp+22h] [ebp-2h]
unsigned __int8 v9; // [esp+23h] [ebp-1h]
for ( i = 0; i < 256; ++i )
byte_416050[i] = i;
v2 = 0;
v9 = 0;
v3 = 0;
result = a1;
v4 = a1;
do
LOBYTE(result) = *v4;
while ( *v4++ );
while ( v2 < 256 )
{
v8 = byte_416050[v2];
v9 += v8 + a1[v3];
v7 = byte_416050[v9];
++v3;
byte_416050[v9] = v8;
byte_416050[v2] = 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; // [esp+18h] [ebp-22Ch]
int v4; // [esp+1Ch] [ebp-228h]
int v5; // [esp+28h] [ebp-21Ch]
unsigned int v6; // [esp+30h] [ebp-214h]
char v7; // [esp+36h] [ebp-20Eh]
char v8; // [esp+37h] [ebp-20Dh]
char v9; // [esp+38h] [ebp-20Ch]
unsigned __int8 v10; // [esp+39h] [ebp-20Bh]
unsigned __int8 v11; // [esp+3Ah] [ebp-20Ah]
char v12; // [esp+3Bh] [ebp-209h]
int v13; // [esp+3Ch] [ebp-208h] BYREF
char v14; // [esp+40h] [ebp-204h] BYREF
char v15[255]; // [esp+41h] [ebp-203h] BYREF
char v16[256]; // [esp+140h] [ebp-104h] 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[v6]) )
{
v8 = a2[v6] - 48;
}
else if ( isxdigit(a2[v6]) )
{
if ( *((_DWORD *)NtCurrentPeb()->ProcessHeap + 3) != 2 )
a2[v6] = 34;
v8 = (a2[v6] | 0x20) - 87;
}
else
{
v8 = ((a2[v6] | 0x20) - 97) % 6 + 10;
}
__rdtsc();
__rdtsc();
v9 = v8 + 16 * v9;
if ( !((int)(v6 + 1) % 2) )
{
v15[v3++ - 1] = v9;
v9 = 0;
}
++v6;
}
while ( v5 < 8 )
{
v10 += byte_416050[++v11];
v12 = byte_416050[v11];
v7 = byte_416050[v10];
byte_416050[v10] = v12;
byte_416050[v11] = v7;
if ( (NtCurrentPeb()->NtGlobalFlag & 0x70) != 0 )
v12 = v10 + v11;
v16[v5] = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1];
if ( (unsigned __int8)*(_DWORD *)&NtCurrentPeb()->BeingDebugged )
{
v10 = -83;
v11 = 43;
}
sub_401710(v16, a1, v5++);
v4 = v5;
if ( v5 >= (unsigned int)(&v15[strlen(&v14)] - 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[v5] = byte_416050[(unsigned __int8)(v7 + v12)] ^ v15[v4 - 1];
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[1] != 'b' )
{
*a3 &= 0x61u;
_EAX = (int *)*a3;
}
else
{
_EAX = (int *)a3;
*a3 |= 0x14u;
}
if ( a2[2] != 'a' )
*a3 &= 0xAu;
else
*a3 |= 0x84u;
if ( a2[3] != 'p' )
*a3 >>= 7;
else
*a3 |= 0x114u;
if ( a2[4] != 'p' )
*a3 *= 2;
else
*a3 |= 0x380u;
if ( a2[5] != 's' )
{
v5 = (char)a3;
*a3 ^= 0x1ADu;
}
else
{
*a3 |= 0xA04u;
v5 = (char)a3;
}
if ( a2[6] != 'e' )
*a3 |= 0x4Au;
else
*a3 |= 0x2310u;
if ( a2[7] != '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:[ebp-0x21C],0x8
00401A55 . |0F8D B0010000 jge crack2.00401C0B
00401A5B . |0FB695 F6FDFF>movzx edx,byte ptr ss:[ebp-0x20A]
00401A62 . |83C2 01 add edx,0x1
00401A65 . |8895 F6FDFFFF mov byte ptr ss:[ebp-0x20A],dl
00401A6B . |0FB685 F5FDFF>movzx eax,byte ptr ss:[ebp-0x20B]
00401A72 . |0FB68D F6FDFF>movzx ecx,byte ptr ss:[ebp-0x20A]
00401A79 . |0FB691 506041>movzx edx,byte ptr ds:[ecx+0x416050]
00401AE8 . /74 16 je short crack2.00401B00 ;
00401AEA . |0FB695 F6FDFF>movzx edx,byte ptr ss:[ebp-0x20A]
00401AF1 . |0FB685 F5FDFF>movzx eax,byte ptr ss:[ebp-0x20B]
00401AF8 . |03D0 add edx,eax
00401AFA . |8895 F7FDFFFF mov byte ptr ss:[ebp-0x209],dl
00401B00 > \0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209]
5.从401B00这里开始就很关键了,一定到定位到v16赋值的地方,下面逐行解读汇编代码
00401B00 > \0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209] ; ecx=v12
00401B07 . 0FB695 F2FDFF>movzx edx,byte ptr ss:[ebp-0x20E] ; edx=v7
00401B0E . 03CA add ecx,edx ; ecx=v7+v12
00401B10 . 888D F7FDFFFF mov byte ptr ss:[ebp-0x209],cl
00401B16 . 0FB685 F7FDFF>movzx eax,byte ptr ss:[ebp-0x209] ; eax=ecx
00401B1D . 8A88 50604100 mov cl,byte ptr ds:[eax+0x416050]
00401B23 . 888D F7FDFFFF mov byte ptr ss:[ebp-0x209],cl
00401B29 . 8B95 D8FDFFFF mov edx,dword ptr ss:[ebp-0x228] ; edx=v4
00401B2F . 0FB68415 FCFD>movzx eax,byte ptr ss:[ebp+edx-0x204] ; eax=v15[v4 - 1]
00401B37 . 0FB68D F7FDFF>movzx ecx,byte ptr ss:[ebp-0x209] ; 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[v6]) )
{
v8 = a2[v6] - 48;
v9 = v8 + 16 * v9;
if ( !((int)(v6 + 1) % 2) ) //v6如果为奇数,条件成立
{
v15[v3++ - 1] = 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[8];
for (int i = 0; i < password.length; i++) {
flag[i]= (byte) (password[i]^arr[i]);
}
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[i] & 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.OD和IDA调试同步插件
2.BUUCTF--crackMe
3.OD ret-sync同步插件
4.IDA Pro 7.7.220118 (SP1) 全插件绿色版