-
-
[原创]看雪 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_43B62B
、sub_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
处的调用是输出 "成功"
,0x475431
和 0x478424
处的调用是输出 "失败"
。
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直播授课