首页
社区
课程
招聘
[原创]SWPU2019_EasiestRe双进程保护(子进程调试父进程逆向分析)
发表于: 2024-6-6 01:12 2467

[原创]SWPU2019_EasiestRe双进程保护(子进程调试父进程逆向分析)

2024-6-6 01:12
2467

解题思路:

加密手段:
数据结构分析:
逆向分析中的问题:
获得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 = 0x408AF8
for 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 = 0xF78824
for 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;
}

上面是加密过程!

下面是解密过程:

下面是分析背包解密了:

  1. 获取揭秘需要的逆元
# 获取揭秘需要的逆元
 from Crypto.Util.number import *
>>> print(inverse(41,0x1eb))  #41是要求逆元的数,0x1eb是模数
>>> 12
  1. 获取密文
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=0x1234
inv=12
c=[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=0x1234
inv=12
c=[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}

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2024-6-6 10:26 被Brinmon编辑 ,原因: 忘记传附件了
上传的附件:
收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 3672
活跃值: (2337)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
2
有附件么,大佬
2024-6-6 09:46
0
雪    币: 963
活跃值: (1278)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
3
Delevy 有附件么,大佬
这是BUUSCTF上的一道题,第四页,SWPU2019_EasiestRe,忘记传了
2024-6-6 10:22
0
游客
登录 | 注册 方可回帖
返回
//