解题思路:
加密手段:
数据结构分析:
逆向分析中的问题:
获得flag:
学到的知识:
题目类型:
[[双进程保护]]
smc代码自修改
[[结构化异常SEH处理机制]]
Interrupt 3(03中断检测)
[[Merkle-Hellman背包加密]]
题目信息:

简介:
wp借鉴:
BUUCTF [SWPU2019]EasiestRe_皮皮蟹!的博客-CSDN博客
[SWPU2019]EasiestRe-cnblog - 自我摧残之策 - 博客园 (cnblogs.com)
[SWPU2019]EasiestRe - 编程猎人 (programminghunter.com)
[buuctf.reverse] 121-125_2021gkctf somuchcode-CSDN博客
https://www.cnblogs.com/harmonica11/p/13525663.html
https://www.52pojie.cn/thread-1580691-1-1.html#41440091_[swpu]-easiestre
https://www.cnblogs.com/lordtianqiyi/articles/16456626.html
核心伪代码分析:
整体代码逆向分析:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | int __cdecl main_0(int argc, const char **argv, const char **envp){ PVOID lpBaseAddress; // [esp+18h] [ebp-558h] ... if ( IsDebuggerPresent() ) // 监测到调试状态,进入分支 { falseflagfun();// 这里是真正的加密解密函数位置,由于这个程序是双进程保护,父进程会对程序进行修改,直接手动修改就可以代替父进程的运行,直接动态调试 return 0; } else // 正常启动进入普通分支 { GetStartupInfoA(&StartupInfo); GetModuleFileNameA(0, Filename, 0x104u); if ( CreateProcessA(Filename, 0, 0, 0, 0, 3u, 0, 0, &StartupInfo, &ProcessInformation) )// 创建子进程并检测断点 { v5 = 1;LABEL_6: while ( v5 ) { dwContinueStatus = 0x10002; WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF);// 等待debug时间 switch ( DebugEvent.dwDebugEventCode ) { case 1u: if ( DebugEvent.u.Exception.ExceptionRecord.ExceptionCode == 0x80000003 )// 表示调试器断点异常int 3触发 { v8 = 1; dwContinueStatus = 65538; // 将变量 dwContinueStatus 的值设置为 65538。这可能是一个用于指示调试器如何继续执行的状态码 lpBaseAddress = DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress;// 获取发出异常的地址 ReadProcessMemory( // 读取异常位置的0x23个字节,存储到buffer里 ProcessInformation.hProcess, DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress, Buffer, 0x23u, NumberOfBytesRead); // 实际读取到的字节数:NumberOfBytesRead if ( NumberOfBytesRead[0] ) { for ( i = 1; i < 35 && Buffer[i] == 0x90; ++i )// 检测读取到的内存有多少个nop指令 ; } if ( i == 1 ) v8 = 0; if ( v8 ) { switch ( i ) // nop指令数量不同处理 { case 4: // 如果有4位需要补则直接让eip+1继续执行 Context.ContextFlags = 65543; hThread = OpenThread(0x1FFFFFu, 0, DebugEvent.dwThreadId); if ( !GetThreadContext(hThread, &Context) ) goto LABEL_31; ++Context.Eip; if ( SetThreadContext(hThread, &Context) ) { dwContinueStatus = 65538; CloseHandle(hThread); } goto LABEL_33; case 5:LABEL_31: ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x80010001); goto LABEL_6; case 7: // 如果有7位则用v16的内容补齐 WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, v16, 7u, NumberOfBytesWritten); if ( NumberOfBytesWritten[0] == 7 ) { ReadProcessMemory(ProcessInformation.hProcess, lpBaseAddress, Buffer, 7u, NumberOfBytesRead); dwContinueStatus = 65538; } goto LABEL_33; case 30: // 如果有30位则用v15的内容补齐 WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, v15, 0x1Eu, NumberOfBytesWritten); if ( NumberOfBytesWritten[0] == 30 ) dwContinueStatus = 65538; goto LABEL_33; default: goto LABEL_33; ...} |
整体父程序分析!
分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | char Main(){ int v1[62]; // [esp+8h] [ebp-138h] BYREF char Str[56]; // [esp+100h] [ebp-40h] BYREF size_t v3; // [esp+138h] [ebp-8h] v3 = 0; j__memset(Str, 0, 0x32u); v1[52] = 2; v1[53] = 3; v1[54] = 7; v1[55] = 14; v1[56] = 30; v1[57] = 57; v1[58] = 120; v1[59] = 251; j__memset(v1, 0, 0xC8u); printf("Please Input Flag:\n"); scanf("%s", Str); v3 = j__strlen(Str); __debugbreak(); printf("you are too short!"); return 0;} |
这里就是程序的输入入口了,但是真实的代码要运行以后通过触发int 3断点后才会出现!
1 2 3 4 5 6 7 8 9 | .text:00408AF8 int 3 ; Trap to Debugger.text:00408AF9 nop.text:00408AFA nop.text:00408AFB nop.text:00408AFC nop.text:00408AFD nop.text:00408AFE nop.text:00408AFF push offset aYouAreTooShort ; "you are too short!".text:00408B04 call printf |
根据父进程代码可知当程序中int 3 下面存在6个nop时将v16的数据写入!
1 2 3 4 5 6 | from idaapi import *from idc import *a = [0x90, 0x83, 0x7D, 0xF8, 0x18, 0x7D, 0x11]b = 0x408AF8for i in range(7): ida_bytes.patch_byte(b + i, a[i]) |
成功写入!!
继续观察该函数发现:
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 | char Main(){ int v1[52]; // [esp+8h] [ebp-138h] BYREF int v2[10]; // [esp+D8h] [ebp-68h] BYREF char Str[56]; // [esp+100h] [ebp-40h] BYREF int len; // [esp+138h] [ebp-8h] len = 0; j__memset(Str, 0, 0x32u); v2[0] = 2; v2[1] = 3; v2[2] = 7; v2[3] = 14; v2[4] = 30; v2[5] = 57; v2[6] = 120; v2[7] = 251; j__memset(v1, 0, 0xC8u); printf("Please Input Flag:\n"); scanf("%s", Str); len = j__strlen(Str); if ( len >= 24 ) { sub_40247D(Str, v2, v1); sub_40384B(v1); } printf("you are too short!"); return 0;} |
继续一步步跟进函数发现!
sub_40247D(Str, v2, v1); -》sub_408860(Str, a2, a3);-》sub_40460B(a2, v10);
发现sub_40460B(a2, v10);这个函数内部也存在一个int 3 且下面的数据nop一共有30个!所以继续使用idapython脚本将程序patch!
1 2 3 4 5 6 7 8 | from idaapi import *from idc import *v15 = [ 0x90, 0x0F, 0xB6, 0x55, 0xF7, 0x8B, 0x45, 0x08, 0x8B, 0x04, 0x90, 0x0F, 0xAF, 0x45, 0xFC, 0x33, 0xD2, 0xF7, 0x75, 0xF8, 0x0F, 0xB6, 0x4D, 0xF7, 0x8B, 0x45, 0x0C, 0x89, 0x14, 0x88]b = 0xF78824for i in range(30): ida_bytes.patch_byte(b + i, v15[i]) |
成功patch
1 2 3 4 5 6 7 8 | char __cdecl sub_F787E0(int a1, int a2){ unsigned __int8 i; // [esp+3h] [ebp-9h] for ( i = 0; i < 8u; ++i ) *(a2 + 4 * i) = 41 * *(a1 + 4 * i) % 0x1EBu; return 1;} |
继续向下看:
分析一下sub_F7384B(v0); // 最后的函数,里面同样存在int 3,但是父进程只是发出了报错异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void __cdecl __noreturn sub_4083C0(int a1){ char v1[96]; // [esp+B4h] [ebp-8Ch] BYREF int v2; // [esp+118h] [ebp-28h] int v3; // [esp+11Ch] [ebp-24h] CPPEH_RECORD ms_exc; // [esp+128h] [ebp-18h] v3 = 0; v2 = a1; j__memset(v1, 0, sizeof(v1)); ms_exc.registration.TryLevel = 0; __debugbreak(); // 这里的int 3报错会被try catch劫持到真正的控制流 ExitProcess(0);} |
为了不动态调试直接手动加一条jmp指令就可以了!!!
修改成功后的伪代码!
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 | void __cdecl __noreturn sub_F783C0(int a1){... v8 = a1; j__memset(&v7[11], 0, 0x60u);..... for ( i = 0; i < 0x18; ++i ) { if ( *(v8 + 4 * i) != v3[i] )//这里才是真正关键的比较!! v9 = 1; } if ( v9 ) { for ( j = 0; j < 16; ++j ) v4[j] ^= 0x66u; printf(&unk_1031E50);//这里会输出失败 j__system("pause"); } else { for ( k = 0; k < 15; ++k ) v6[k] ^= 0x66u; printf("%s\n");//这里会输出成功 j__system("pause"); }}/* Orphan comments:这里的int 3报错会被try catch劫持到真正的控制流*/ |
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | char __cdecl sub_5D8860(char *Str, int data1, int result){ unsigned __int8 v4; // [esp+Fh] [ebp-4Dh] int j; // [esp+10h] [ebp-4Ch] int k; // [esp+10h] [ebp-4Ch] int i; // [esp+14h] [ebp-48h] unsigned __int8 v8; // [esp+1Bh] [ebp-41h] int v9[4]; // [esp+20h] [ebp-3Ch] BYREF int var1[9]; // [esp+30h] [ebp-2Ch] BYREF int v11; // [esp+54h] [ebp-8h] v11 = 4660; memset(var1, 0, 32); v9[0] = 0; v9[1] = 0; v8 = j__strlen(Str); sub_5D460B(data1, var1); // n是公钥乘数41 m模数是:0x1eb for ( i = 0; i < v8; ++i ) { if ( i ) Str[i] ^= *(result + 4 * i - 4); // 利用上一位已经加密成功的数据进行异或 else *Str ^= v11; // 将第一个字符与常数异或 v4 = 1; j__memset(v9, 0, 8u); for ( j = 0; j < 8; ++j ) { if ( (v4 & Str[i]) != 0 ) // 计算出输入的每一个str字符的每一个字节为0还是1 { *(v9 + j) = 1; } else { if ( j >= 8 ) j____report_rangecheckfailure(); *(v9 + j) = 0; } v4 *= 2; } for ( k = 0; k < 8; ++k ) *(result + 4 * i) += var1[7 - k] * *(v9 + k);// 计算出str的二进制0和1以后和背包加密的公钥相乘得到需要的结果 } return 1;} |
上面是加密过程!
下面是解密过程:
下面是分析背包解密了:
- 获取揭秘需要的逆元
# 获取揭秘需要的逆元 from Crypto.Util.number import * >>> print(inverse(41,0x1eb)) #41是要求逆元的数,0x1eb是模数 >>> 12
- 获取密文
1 2 3 4 5 | {0x000003D1, 0x000002F0, 0x00000052, 0x00000475, 0x000001D2, 0x000002F0, 0x00000224, 0x0000051C, 0x000004E6, 0x0000029F, 0x000002EE, 0x0000039B, 0x000003F9, 0x0000032B, 0x000002F2, 0x000005B5, 0x0000024C, 0x0000045A, 0x0000034C, 0x0000056D, 0x0000000A, 0x000004E6, 0x00000476, 0x000002D9}; |

3. 获取私钥
key=[2,3,7,14,30,57,120,251]

解密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | iv=0x1234inv=12c=[0x3d1,0x2f0,0x52,0x475,0x1d2,0x2f0,0x224,0x51c,0x4e6,0x29f,0x2ee,0x39b,0x3f9,0x32b,0x2f2,0x5b5,0x24c,0x45a,0x34c,0x56d,0xa,0x4e6,0x476,0x2d9]key=[2,3,7,14,30,57,120,251]flag=[]for i in range(24): t=c[i]*inv%491 p="" for i in range(8): if key[7-i]>t: p+="0" else: p+="1" t-=key[7-i] flag.append(int(p[::-1],2)) |
最后还有一个异或运算:
1 2 3 4 | print(chr((flag[0]^0x1234)&0xff),end="")for i in range(1,len(flag)): print(chr((flag[i]^c[i-1])&0xff),end="")#swpuctf{y0u_@re_s0_coo1} |
脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | iv=0x1234inv=12c=[0x3d1,0x2f0,0x52,0x475,0x1d2,0x2f0,0x224,0x51c,0x4e6,0x29f,0x2ee,0x39b,0x3f9,0x32b,0x2f2,0x5b5,0x24c,0x45a,0x34c,0x56d,0xa,0x4e6,0x476,0x2d9]key=[2,3,7,14,30,57,120,251]flag=[]for i in range(24): t=c[i]*inv%491 p="" for i in range(8): if key[7-i]>t: p+="0" else: p+="1" t-=key[7-i] flag.append(int(p[::-1],2))print(chr((flag[0]^0x1234)&0xff),end="")for i in range(1,len(flag)): print(chr((flag[i]^c[i-1])&0xff),end="")#swpuctf{y0u_@re_s0_coo1} |