首页
社区
课程
招聘
[原创]看雪 2022 KCTF 春季赛 第三题 石像病毒
2022-5-16 06:16 7255

[原创]看雪 2022 KCTF 春季赛 第三题 石像病毒

2022-5-16 06:16
7255

IDA逆向

 

主程序:

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
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  void *v3; // esp
  char v7; // [esp-10h] [ebp-1048h]
  struct struc_1 v; // [esp+0h] [ebp-1038h] BYREF
  CPPEH_RECORD ms_exc; // [esp+1020h] [ebp-18h]
 
  v3 = alloca(4128);
  memset(&v, 0xCCu, sizeof(v));
  ms_exc.registration.ScopeTable = (PSCOPETABLE_ENTRY)((int)ms_exc.registration.ScopeTable ^ __security_cookie);
  memset(v.s, 0, sizeof(v.s));
  memset(v.ciphertext, 0, sizeof(v.ciphertext));
  gets_s(v.s, 4000u);
  if ( strlen(v.s) == 32 )
  {
    for ( v.field_40 = 0; v.field_40 < 512; ++v.field_40 )
    {
      v.field_3C = 0;
      MEMORY[0] = v.field_40;
      ms_exc.registration.TryLevel = -2;
    }
    strcpy(v.field_24, "Enj0y_1t_4_fuuuN");
    _DX = '_\0';
    *(_WORD *)&v.field_24[17] = 0;
    v.field_24[19] = 0;
    memset(v.field_8, 0, sizeof(v.field_8));
    for ( v.field_0 = 0; v.field_0 < 512; ++v.field_0 )
    {
      __asm { insb }
      ms_exc.registration.TryLevel = -2;
      _DX = LOWORD(v.field_0) + 1;
    }
    j_md5(v.field_24, 0x10u, v.field_8);
    j_aes(v.field_8, 0x10u, v.s, v.ciphertext, 32);
    if ( !memcmp(v.ciphertext, finalcompare, 0x20u) )
      j_printf("OK\n", v7);
    else
      j_printf("NO\n", v7);
    return 0;
  }
  else
  {
    j_printf("NO\n", v7);
    return 0;
  }
}

背景:SEH

注意到 MEMORY[0] = v.field_40;,这是第一次引发异常的位置。
引发异常的位置还有若干个;程序利用 SEH(Structured Exception Handling,结构化异常处理) 机制处理异常,在处理过程中对程序进行了一定的修改。

 

这部分的汇编如下:

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
.text:004028B8 loc_4028B8:                             ; CODE XREF: _main_0:loc_40290B↓j
.text:004028B8                 mov     ecx, [ebp+v.field_40]
.text:004028BE                 add     ecx, 1
.text:004028C1                 mov     [ebp+v.field_40], ecx
.text:004028C7
.text:004028C7 loc_4028C7:                             ; CODE XREF: _main_0+F6↑j
.text:004028C7                 cmp     [ebp+v.field_40], 200h
.text:004028D1                 jge     short loc_40290D
.text:004028D3 ;   __try { // __except at loc_402901
.text:004028D3                 mov     [ebp+ms_exc.registration.TryLevel], 0
.text:004028DA                 mov     [ebp+v.field_3C], 0
.text:004028E4                 mov     edx, [ebp+v.field_3C]
.text:004028EA                 mov     al, byte ptr [ebp+v.field_40]
.text:004028F0                 mov     [edx], al
.text:004028F0 ;   } // starts at 4028D3
.text:004028F2                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004028F9                 jmp     short loc_40290B
.text:004028FB ; ---------------------------------------------------------------------------
.text:004028FB
.text:004028FB loc_4028FB:                            ; DATA XREF: .rdata:stru_40A030↓o
.text:004028FB ;   __except filter // owned by 4028D3
.text:004028FB                 mov     eax, 1
.text:00402900                 retn
.text:00402901 ; ---------------------------------------------------------------------------
.text:00402901
.text:00402901 loc_402901:                             ; DATA XREF: .rdata:stru_40A030↓o
.text:00402901 ;   __except(loc_4028FB) // owned by 4028D3
.text:00402901                 mov     esp, [ebp+ms_exc.old_esp]
.text:00402904                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:0040290B
.text:0040290B loc_40290B:                             ; CODE XREF: _main_0+139↑j
.text:0040290B                 jmp     short loc_4028B8
.text:0040290D ; ---------------------------------------------------------------------------
.text:0040290D

IDA 已经标记出了 __try、__except filter 和 __except 块(在反编译的C代码中看不到)。

 

.rdata 节也有相应的数据结构,例如:

1
2
3
4
5
6
7
8
9
10
11
12
.rdata:0040A030 stru_40A030     dd 0FFFFFFE4h           ; GSCookieOffset
.rdata:0040A030                                         ; DATA XREF: _main_0+5↑o
.rdata:0040A030                 dd 0                    ; GSCookieXOROffset
.rdata:0040A030                 dd 0FFFFEFB8h           ; EHCookieOffset
.rdata:0040A030                 dd 0                    ; EHCookieXOROffset
.rdata:0040A030                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:0040A030                 dd offset loc_4028FB    ; ScopeRecord.FilterFunc
.rdata:0040A030                 dd offset loc_402901    ; ScopeRecord.HandlerFunc
.rdata:0040A030                 dd 0FFFFFFFEh           ; ScopeRecord.EnclosingLevel
.rdata:0040A030                 dd offset loc_4029AF    ; ScopeRecord.FilterFunc
.rdata:0040A030                 dd offset loc_4029B5    ; ScopeRecord.HandlerFunc
.rdata:0040A058                 align 10h

关于SEH机制,在搜索资料的过程中找到了两篇个人认为不错的文章:

对于本题,只需要理解到本题程序发生异常后的控制流都是先运行 __except filter 块再运行 __except 块即可。

 

在此基础上也可以patch程序让程序的控制流恢复正常。(但对于做题来讲速度就太慢了)

分析

通过程序结构和常量,初步识别出 sub_402DB0 是 md5,sub_402070 是 AES (是否魔改过暂时未知)。

 

动态调试。异常会被调试器捕获,需要透传给程序,在 x64dbg 调试器中按 shift+F7/F8/F9

 

md5 过程中在 0x402D1C 触发了一次除零异常,然后进入 __except filter,在 0x402D2B 调用 0x402C60 修改了 md5 常量表的一个值。

 

对于 AES,

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
int __cdecl aes(void *Src, size_t Size, char *plaintext, char *ciphertext, unsigned int a5)
{
  int j; // [esp+4h] [ebp-1C8h]
  unsigned int i; // [esp+8h] [ebp-1C4h]
  int v8[6]; // [esp+10h] [ebp-1BCh] BYREF
  int v9[11]; // [esp+28h] [ebp-1A4h] BYREF
  char *v10; // [esp+54h] [ebp-178h]
  char *v11; // [esp+58h] [ebp-174h]
  char v12[360]; // [esp+60h] [ebp-16Ch] BYREF
 
  v11 = ciphertext;
  v10 = v12;
  memset(&v9[6], 0, 16);
  memset(v9, 0, 16);
  memset(v8, 0, 16);
  if ( !Src || !plaintext || !ciphertext )
    return -1;
  if ( Size > 0x10 )
    return -1;
  if ( a5 % 0x10 )
    return -1;
  memcpy(v9, Src, Size);
  sub_4010BE((int)v9, 16, (int)v12);
  for ( i = 0; i < a5; i += 16 )
  {
    j_bytes_to_aes_state((int)v8, (int)plaintext);
    j_addroundkey((int)v8, (int)v10);
    for ( j = 1; j < 10; ++j )
    {
      v10 += 16;
      j_subbytes((int)v8);
      j_shiftrows((int)v8);
      j_mixcolumns((int)v8);
      j_addroundkey((int)v8, (int)v10);
    }
    j_subbytes((int)v8);
    j_shiftrows((int)v8);
    j_addroundkey((int)v8, (int)(v10 + 16));
    j_aes_state_to_bytes((int)v8, (int)v11);
    v11 += 16;
    plaintext += 16;
    v10 = v12;
  }
  return 0;
}

sub_4010BE(AES 的 key expansion) 里,0x401685 处有无效指令异常(可以手动patch成nop,IDA就能正确反编译此函数并识别出 __except 块灯信息了),在 __except filter 0x40168F 处调用到 sub_402330 函数交换了 sbox 0x71 和 0xA3 两项的值。(动态调试发现是每轮都会交换)

 

实际上,上面的分析全部都不用做,直接等到 sub_4010BE 结束从内存中提取展开后的 11 个 AES 轮密钥。

 

接下来是 AES 的四种轮操作,自己找一份标准 AES 实现,打印出每步的计算结果与程序做比对,看程序是否有对标准算法做修改:

  • addroundkey 没有修改。不过要注意 AES 内部状态是列优先排布的 4*4 矩阵,而 v8 变量是这个矩阵的行优先表示,一些第三方库可能有不同的表示方式。
  • subbytes:动态调试发现sbox是修改后的状态,解密时需要同步修改 invsbox
  • shiftrows:动态调试发现移位方向相反,即实际上做了 invshiftrows
  • mixcolumns:没有修改,但是在 0x401DB0 的 __except filter 中调用 sub_4023D0 修改了最终待比较的常量的第 16、17 个字符

脚本

根据分析结果修改 AES 解密的逻辑

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
from phoenixAES import AddKey, SBox, ShiftRow, MC, InvSBox, InvShiftRow, InvMC
import phoenixAES
 
roundkeys = [
    bytes.fromhex("ffb1652fd086ed310f5c289a9d054840"),
    bytes.fromhex("f6ef0e7c2669e34d2935cbd7b4308397"),
    bytes.fromhex("7e620a92580be9df713e2208c50ea19f"),
    bytes.fromhex("a5c4a1a4fdcf487b8cf16a7349ffcbec"),
    bytes.fromhex("6bffb7b39630ffc81ac195bb533e5e57"),
    bytes.fromhex("301205fba622fa33bce36f88efdd31df"),
    bytes.fromhex("aecdc41c08ef3e2fb40c51a75bd16078"),
    bytes.fromhex("12f4fa8c1a1bc4a3ae179504f5c6f57c"),
    bytes.fromhex("02124eea18098a49b61e1f4d43d8ea31"),
    bytes.fromhex("c5082f76dd01a53f6b1fba7228c75043"),
    bytes.fromhex("df3ce913023d4c2c6922f65e41e5a61d")
]    # dumped from memory
 
 
sbox = phoenixAES._AesSBox
sbox[0x71], sbox[0xA3] = sbox[0xA3], sbox[0x71]
 
phoenixAES._AesInvSBox = [sbox.index(i) for i in range(256)]
 
def AddKey(state, key):
    r = bytearray(16)
    for i in range(4):
        for j in range(4):
            r[4*i+j] = state[4*i+j] ^ key[4*i+3-j]
    return r
 
InvShiftRow = ShiftRow
 
 
def aesdec(ciphertext, roundkeys):
    assert len(ciphertext) % 16 == 0
    r = b""
    for i in range(0, len(ciphertext), 16):
        state = ciphertext[i:i+16]
        state = AddKey(state, roundkeys[10])
        state = InvShiftRow(state)
        state = InvSBox(state)
        for i in range(9, 0, -1):
            state = AddKey(state, roundkeys[i])
            state = InvMC(state)
            state = InvShiftRow(state)
            state = InvSBox(state)
        state = AddKey(state, roundkeys[0])
        r += state
    return r
 
state = bytearray.fromhex("57 7C F5 6D 56 96 77 45  B0 BD A1 C7 89 A5 AB DC   F2 F4 4B FE BE F5 F5 5C  4D 30 42 0F 2B 3B E6 CB".replace(' ', ''))
state[16] = 256-12
state[17] = 256-14
 
print(aesdec(state, roundkeys))

得到flag:

1
flag{db5c6a8dfec4d0ec5569899640}

[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回