首页
社区
课程
招聘
[原创]看雪 2022 KCTF 春季赛 第九题 同归于尽
2022-5-31 06:49 9265

[原创]看雪 2022 KCTF 春季赛 第九题 同归于尽

2022-5-31 06:49
9265

IDA打开,发现所有的字符串字面值都经过了简单的加密。

 

先看 _main 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  scanf(v51, v138);
  if ( v138[0] != 'A' )    //
  {
    v52 = sub_415B2C(v139);
    v53 = (char *)sub_499715(v52);
    printf(v53);
    v54 = (_DWORD *)sub_414097((int)v139);
    v55 = sub_499741(v54);
LABEL_6:
    system((int)v55);
    return 0;
  }
  v56 = 0;
  do
    ++v56;
  while ( v138[v56] );
  if ( 100 * v56 != 3200 )    //
  {

得出输入长度是 3200 // 100 = 32 字节,第一个字符是 'A'

 

程序开头创建了一个线程并监听端口,顺序追到 sub_49C89D,是 recv 的各种消息的处理函数。注意到其中几行(暂时不知道在哪里会用到):

1
2
3
4
5
switch ( buf )
{
  case 1:
    for ( i = 0; i < v10; ++i )
      v5[i] ^= 0x55u;

socket 函数的交叉引用,找到发送消息的地方 sub_49C47F。向上找调用者,是 sub_43B62Bsub_432990 等非常复杂的函数,看起来加了某种混淆。

 

翻看 IDA 左侧的函数列表,下半部分基本都是自动识别出的蓝色的库函数,上半部分白色函数大部分都是字符串构建与解密函数,但是这一片白色函数的最后几个,如 sub_49D244,里面包含了 sm4 加密算法的常量。

 

顺着找交叉引用,结合 sm4 算法的实现,标记出 sub_49D244是密钥扩展,sub_49D07F是单个块加密,sub_49D025 是多个块加密。
sub_49AF99 是最加密的最外层函数,但是调用它的 sub_4631E9 又是一个非常复杂的函数。

 

开始上手动态调试。输入长32字节,第一个字符是'A',保守起见32个字符只输入 [0-9A-Za-z] 范围内的字符。

 

0x401AAF 遇到了第一处异常,是 int 2Dh 反调试(回忆起 IDA 加载程序时提示的 pdb 路径包含 AntiDebug)。x32dbg 中可以 Shift+F9 忽略异常继续调试,但程序里可能有暗坑,暂时不考虑。

 

在前面找到的几个 sm4 加密函数下断点:sub_49D244 第一个参数指向的16个字节是密钥,先提取出来;sub_49D025 第三个参数是加密长度(32),第四个参数指向的是待加密内容,发现前 16 字节有值,后 16 字节全是 0,也先提取出来;运行到函数返回,从第五个参数指向的地址提取出加密结果。

 

找一份标准的 sm4 加密进行测试,发现程序的加密结果与标准算法一致。

 

对于待加密字节与原始字节的关系,如果输入的值有规律,则这里也能观察到规律;改变一个输入字节,这里也只改变一个字节,尝试异或一下得到了 0x55。联系到 sub_49C89D 里面循环异或 0x55 的代码,猜测待加密的字节就是输入值hexdecode之后再逐位异或0x55,测试后得到验证。

 

调用加密的函数太复杂不想看。从 _main 函数开头易知 sub_49B4ED 是 printf,查找交叉引用发现在 _main 函数之外只在 sub_46D092 有三处调用。
在调试器中把 EIP 直接改到这三处调用前面,看到 0x47A497 处的调用是输出 "成功"0x4754310x478424 处的调用是输出 "失败"

 

sub_46D092 在 IDA 无法反编译(提示 "stack frame is too big"),但是 Ghidra 可以(Ghidra 的反编译效果虽然差一点,但对畸形函数的支持很好)。

 

定位到这三个位置:(分别在 3280、4561、9279 行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  if (*(int *)(unaff_EBP + 0x240) < 1) {
LAB_004783e1:    // LAB_004783e1
    uVar5 = 0;
    do {
      uStackY75624 = 0x4783ef;
      pcVar4 = (char *)FUN_0040462d((void *)(unaff_EBP + 0x140),uVar5);
      if ('`' < *pcVar4) {
        uStackY75624 = 0x478400;
        pcVar4 = (char *)FUN_0040462d((void *)(unaff_EBP + 0x140),uVar5);
        if (*pcVar4 < '{') {
          uStackY75624 = 0x47841c;
          iVar2 = FUN_004176e5((undefined4 *)(unaff_EBP + -0xab90));
          pcVar4 = (char *)FUN_00499c2d(iVar2);
          uStackY75624 = 0x478429;
          FID_conflict:_wprintf(pcVar4);   // "失败"
          bVar1 = false;    // bVar1
          break;
        }
      }
      uVar5 = uVar5 + 1;
    } while (uVar5 < 0x20);
1
2
3
4
5
6
7
if (bVar1) {    // bVar1
  uStackY75624 = 0x47a48f;
  iVar2 = FUN_00416b87((undefined4 *)(unaff_EBP + -0xca68));
  pcVar4 = (char *)FUN_00499e5a(iVar2);
  uStackY75624 = 0x47a49c;
  FID_conflict:_wprintf(pcVar4);    // "成功"
}
1
2
3
4
5
uStackY75624 = 0x475429;
iVar2 = FUN_004153a3((undefined4 *)(unaff_EBP + -0x7d68));
pcVar4 = (char *)FUN_0049a3ad(iVar2);
uStackY75624 = 0x475436;
FID_conflict:_wprintf(pcVar4);   // "失败"

第一块代码是检查输入字符不含小写字母,因此可以确定输入只包含 [0-9A-Z];第二块代码要求 bVar1 为 true,搜索一下读 bVar1 的引用发现只在第一处赋值为了false。

 

动态调试,发现第一处的 if (*(int *)(unaff_EBP + 0x240) < 1) 条件不满足。搜索 0x240,找到下面这里(7255行):

1
2
3
4
5
6
7
8
else {
  do {
    if (*(char *)(unaff_EBP + 0x1b0 + iVar2) != *(char *)(unaff_EBP + -0x48 + iVar2)) {
      dVar7 = dVar7 + 56.3;
    }
    iVar2 = iVar2 + 1;
  } while (iVar2 < *(int *)(unaff_EBP + 0x240));
  if (dVar7 <= 50.0) goto LAB_004783e1;    // LAB_004783e1

很明显的循环判等,而且 LAB_004783e1 就是上面第一块代码的位置。

 

在这里下断点,提取出待比较的值,发现其中一边就是前面 sm4 的加密结果。

 

写脚本:先解密sm4,在逐字节异或0x55,最后hexencode,得到32字节的序列号,发现第一个字节不是'A';输入程序中也不正确。

 

怀疑有反调试导致提取出的密钥或加密结果不正确。为了避免调试干扰,把提取密钥的位置 0x49D244 patch成死循环,运行程序,然后再附加,发现提取到的密钥确实有变化。

 

重新计算序列号,得到了正确的结果。

1
AFF7AEFB8AEFF697B6B915FA9818CB16

最终脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
from sm4 import SM4Key
 
# key = bytes.fromhex("DF 1B EB FF 00 79 00 B9 88 00 00 00 B8 55 00 00")    # wrong key
key = bytes.fromhex("60 9C B8 FF 00 00 00 B9 88 00 00 00 B8 55 00 00")    # correct key
 
final = bytes.fromhex("55 37 86 E8 9B 5D 05 F5 5F DC 04 DD E1 2A 77 21  90 7C BE 0E 68 F4 E3 60 8D 2A A8 9A FC FF BE 78")
 
key0 = SM4Key(key)
src = key0.decrypt(final)
 
flag = bytes(c ^ 0x55 for c in src)
 
print(flag[:16].hex().upper())    # AFF7AEFB8AEFF697B6B915FA9818CB16

(有个小坑:网上搜索 sm4 的 python实现,很多文章推荐 gmssl 库,但是测试发现它的 ECB 加密模式的密文与明文长度竟然不相等(似乎是加了padding,但ECB模式不应该这样),而且按上面的思路解密得到的结果也不对,不太清楚哪里出了问题;后来找到一篇文章提到的 sm4 库是正常的。这两个库都可以通过 pip 直接安装。)


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-5-31 06:53 被mb_mgodlfyn编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回