首页
社区
课程
招聘
[推荐]看雪·安恒 2020 KCTF 春季赛 | 十三题设计思路及解析
发表于: 2020-5-18 18:26 6769

[推荐]看雪·安恒 2020 KCTF 春季赛 | 十三题设计思路及解析

2020-5-18 18:26
6769



第十三题《猪突豨勇》共有1615人围观,最终仅有1支战队成功破解。



在比赛过程中,金左手战队再次惊艳全场,5月16日深夜,金左手战队的ccfer拿到《猪突豨勇》的一血,成功登上攻击方排行榜的榜首。







春季赛到这里就告一段落啦,感谢每一支战队的参与,你们的存在让整个赛场更闪亮。


为了保证赛事的公平公正,春季赛的排名将由专业裁判进行审核,正式结果将在明天公布,请大家耐心等待,持续关注后续消息。如果您对比赛结果有任何异议,尽请提出。

接下来让我们一起看一下第十三题的设计思路和详细解析吧。




赛题点评


本题点评由看雪密码学专家readyu提供:


最后一题, 金左手战队的ccfer大牛,拿下一血,以画龙点睛之笔收官了比赛。随便还发现了题目flag的hex字符大小写多解。



这一题的“36次”蛋炒饭,每一轮加密算法都修改了参数。算法分三个步骤:用户态34次,js虚拟机1次,kernel调用1次。js虚拟机,kernel里对应的算法参数是修改过的。

第二步骤里,作者用了一个不太常见的duktape的js 虚拟机,加密代码写成js脚本,被编译为1个32681字节的opcode来执行。程序里编译得很干净,没有明显的特征信息,这是一个很大的障碍。需要从opcode里找出32个dword子秘钥 ,和一个256字节的sbox,以及ror循环移位的4个参数。如果没有找到duktape虚拟机原型,只有这个arm程序,那就很难调试js脚本,攻击方可能就止步在这里了。

攻击方修改并编译duk虚拟机,加载opcode,把vm handler的关键指令(异或,移位)打印log出来观察,抽丝剥茧,成功获取密钥参数,攻克了此题。




出题团队简介




本题出题战队 飘向北方:




团队简介:

毕业于广西大学,现居住于深圳,自带冷场天赋,专业拆台30年,常年承接各种相亲冷场,聚会拆台业务,有意向者可站内信联系。






设计思路


一、类型


智能设备CTF


二、Flag


18542cf63f2508f9632e6f8a49e0c298


三、题目设计


题目针对ARM智能设备进行模拟。

本题只有一种核心算法,SM4算法。

算法的计算过程是:

对输入的序列号进行36次SM4解密,判断最后解密出来的结果是否等于0x57656c636f6d6520746f204b616e5875。

36次变种SM4分3种类型:1~34次SM4为直接运算,第35次运行在js虚拟机中,第36次依赖Linux crypto API调用内核的加密算法。

如36次解密后解密结果等于0x57656c636f6d6520746f204b616e5875,
则提示"you got it", 并进入登录界面。

否则提示继续输入flag。

SM4 的变化方式为修改算法的S盒参数、FK参数、CK参数。

破解方法为跟踪并恢复算法相关参数。也可以跟出SM4算法的round key,直接解密。

题目未加壳。


四、解题思路


比赛提供images.tar.gz 文件,解压可以得到如下文件:

├── rootfs.ext2

├── versatile-pb.dtb

└── zImage


参赛者可以在 linux 系统下运行如下命令运行镜像文件:

qemu-system-arm -M versatilepb -kernel ./zImage -dtb ./versatile-pb.dtb -drive file=./rootfs.ext2,if=scsi -append "root=/dev/sda console=ttyAMA0,115200" -nographic


该命令在 Ubuntu 18.04.4 ,qemu 2.11 测试通过,不建议使用Windows版本的qemu 运行。


五、题目主题词


糖牛的“黯然销魂蛋炒饭”

香江,珍宝画舫,食神争霸总决赛现场,衣香鬓影,名流云集。

周星星与糖牛正在激烈地争夺食神的宝座,他们最后都选择了在“黯然销魂蛋炒饭”这道菜上一决高下!

糖牛手捧着刚刚完成的“黯然销魂蛋炒饭”,忍不住傲然道:周星星,当年在灌江口,我跟你学厨。

第一年,你让我砍柴,我砍了整整一年的柴;

第二年,你让我挑水,我挑了整整一年的水;

第三年,你让我烧火,我烧了整整一年的火;

第四年,你让我扇风,我扇了整整一年的风;

直到第五年,你才肯教给我这道“黯然销魂蛋炒饭”。

这十年来,我每日苦练这道“黯然销魂蛋炒饭”,未敢有一日松懈。

今天,我完成了这道“黯然销魂蛋炒饭”的三十六重变化,每一种变化都能让食客获得一种独特的美食体验。如今,我看你怎么赢我!哈哈哈哈!哈哈哈哈!

谁料,周星星却仰天大笑道:糖牛,十五年前你跟我学厨,我就看出你脑后有反骨。

是以始终对你有所保留。虽然你教会了“黯然销魂蛋炒饭”的第三十六重变化,

可今天我要告诉你,我的“黯然销魂蛋炒饭”有七十二种变化。

真正的老饕,在品尝了我的“蛋炒饭”之后,你的三十六重变化每一重都会让人味同嚼蜡,你且说我如何不能赢你!!!哈哈哈哈!哈哈哈哈!



解析过程




本题解题思路由金左手的ccfer提供: 








一、小猪上路


展开images,bin下拿到主程序kanxue2020:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:00010934                 CMP     R0, #0x20                //输入有效长度0x20,后面转成16字节
...
.text:00010940                 LDR     R12, =byte_23C28C        //输入存储在全局变量
...
.text:00010A04                 BL      sub_1898C                  //decode_step1
.text:00010A08                 MOV     R11, SP
.text:00010A0C                 BL      sub_18598                   //decode_step2
...
.text:00010AC0                 MOV     R0, R5
.text:00010AC4                 BL      sub_18718                   //decode_step3
.text:00010AC8                 CMP     R0, #0
.text:00010ACC                 BNE     loc_108BC
.text:00010AD0                 LDR     R3, =aWelcomeToKanxu     //"Welcome to KanXue CTF 2020"
.text:00010AD4                 LDR     R2, [R3]                   
.text:00010AD8                 LDR     R3, [R9]
.text:00010ADC                 CMP     R2, R3                    //比较结果经过3步decode的结果和"Welcome to KanXue CTF 2020"前16字符比较
.text:00010AE0                 BNE     loc_108BC
...
.text:00010B2C                 MOV     R4, R0                    //这里就是光明之巅
.text:00010B30                 LDR     R0, =aYouGotIt             //"you got it[*]!\r"
.text:00010B34                 BL      zz_printf

decode_step1


即“黯然销魂蛋炒饭”的前34个变化,除了代码长没啥,IDA中可F5,每个变化用不同的初始化向量重新生成key,每个变化的256字节置换表都不同的全局变量。
 

decode_step2


这个是比较难搞的部分,花了很多时间,是个js虚拟机,经高人看了看字节码便指点出这是个叫duktape的js引擎。

字节码在byte_2348CB,先加载字节码,然后调用函数h(hexstr(step1_result))计算返回结果。

decode_step3


首先注意到:
.text:00073324 ADD R1, PC, R1 ; "skcipher"

然后调试到里面会遇若干次svc调用,这是带kernel交互的啊,我感到迷茫,从没调试过这玩意。

大帅锅同学这时候把调试方法和kernel展开文件准备好了,我学习了一下就继续分析算法了。

深入调试得到sub_C0171EE0是setkey,sub_C0171D74是decrypt。

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
int __fastcall zz_setkey(int a1, int a2)
{
  int v2; // lr
  signed int v3; // r6
  int v4; // r2
  _DWORD *v5; // r1
  int v6; // r6
  int v7; // r4
  int v8; // lr
  int v9; // r12
  signed int v10; // r9
  int v11; // r5
  signed int v12; // t1
  unsigned int v13; // r3
  int v14; // ST00_4
  int v15; // r3
  int v16; // r3
  int v17; // r2
  int v18; // t1
  int result; // r0
  int v20; // [sp+4h] [bp-3Ch]
  int v21; // [sp+8h] [bp-38h]
  int v22; // [sp+Ch] [bp-34h]
  int v23; // [sp+10h] [bp-30h]
  int v24; // [sp+14h] [bp-2Ch]
  
  v2 = 0;
  v3 = 0x12578F07;
  v24 = 0;
  while 1 )
  {
    v4 = *(unsigned __int8 *)(a2 + v2) | (*(unsigned __int8 *)(a2 + v2 + 1) << 8) | (*(unsigned __int8 *)(a2 + v2 + 2) << 16) | (*(unsigned __int8 *)(a2 + v2 + 3) << 24);
    *(int *)((char *)&v20 + v2) = (((unsigned int)v4 ^ __ROR4__(v4, 16)) >> 8) & 0xFFFF00FF ^ __ROR4__(v4, 8) ^ v3;
    v2 += 4;
    if ( v2 == 16 )
      break;
    v3 = *(_DWORD *)((char *)&unk_C03A0324 + v2);
  }
  v5 = &unk_C03A0334;
  v6 = v20;
  v7 = v21;
  v8 = v22;
  v9 = v23;
  v10 = 0x14ECBD93;
  v11 = a1 - 4;
  while 1 )
  {
    v13 = v9 ^ v7 ^ v8 ^ v10;
    HIBYTE(v14) = byte_C03A0224[v13 >> 24];
    BYTE1(v14) = byte_C03A0224[BYTE1(v13)];
    BYTE2(v14) = byte_C03A0224[BYTE2(v13)];
    LOBYTE(v14) = byte_C03A0224[(unsigned __int8)v13];
    v15 = __ROR4__(v14, 11) ^ __ROR4__(v14, 24) ^ v14 ^ v6;
    *(_DWORD *)(v11 + 4) = v15;
    v11 += 4;
    if ( v5 == (_DWORD *)&unk_C03A03B0 )
      break;
    v6 = v7;
    v7 = v8;
    v8 = v9;
    v9 = v15;
    v12 = v5[1];
    ++v5;
    v10 = v12;
  }
  v16 = a1 + 128;
  v17 = a1 + 124;
  do
  {
    v18 = *(_DWORD *)(v16 - 4);
    v16 -= 4;
    *(_DWORD *)(v17 + 4) = v18;
    v17 += 4;
  }
  while ( a1 != v16 );
  result = 0;
  if ( v24 )
    sub_C0017628();
  return result;
}
  
int __fastcall zz_decrypt(_DWORD *key, _BYTE *out, _BYTE *in)
{
  _DWORD *v3; // r6
  _BYTE *v4; // r4
  int *v5; // lr
  int v6; // r3
  int v7; // r5
  int v8; // r12
  _DWORD *v9; // r0
  int v10; // r6
  int v11; // r4
  int i; // lr
  int v13; // t1
  unsigned int v14; // r3
  int v15; // ST00_4
  unsigned int v16; // r3
  _BYTE *v17; // r4
  int *v18; // lr
  int result; // r0
  int v20; // t1
  int v21; // [sp+4h] [bp-34h]
  int v22; // [sp+8h] [bp-30h]
  int v23; // [sp+Ch] [bp-2Ch]
  int v24; // [sp+10h] [bp-28h]
  int v25; // [sp+14h] [bp-24h]
  
  v3 = key + 0x30;
  v4 = in 16;
  v25 = 0;
  v5 = &v21;
  do
  {
    v6 = (unsigned __int8)*in | ((unsigned __int8)in[1] << 8) | ((unsigned __int8)in[2] << 16) | ((unsigned __int8)in[3] << 24);
    in += 4;
    *v5 = (((unsigned int)v6 ^ __ROR4__(v6, 16)) >> 8) & 0xFFFF00FF ^ __ROR4__(v6, 8);
    ++v5;
  }
  while in != v4 );
  v7 = v21;
  v8 = v22;
  v9 = key + 0x2F;
  v10 = (int)(v3 + 0x1F);
  v11 = v23;
  for ( i = v24; ; i = v16 )
  {
    v13 = v9[1];
    ++v9;
    v14 = v8 ^ v11 ^ v13 ^ i;
    HIBYTE(v15) = byte_C03A0224[v14 >> 24];
    BYTE1(v15) = byte_C03A0224[BYTE1(v14)];
    BYTE2(v15) = byte_C03A0224[BYTE2(v14)];
    LOBYTE(v15) = byte_C03A0224[(unsigned __int8)v14];
    v16 = __ROR4__(v15, 20) ^ __ROR4__(v15, 28) ^ v15 ^ __ROR4__(v15, 12) ^ __ROR4__(v15, 6) ^ v7;
    v7 = v8;
    if ( v9 == (_DWORD *)v10 )
      break;
    v8 = v11;
    v11 = i;
  }
  v22 = v11;
  v23 = i;
  v21 = v8;
  v24 = v16;
  v17 = out + 16;
  v18 = &v23;
  while 1 )
  {
    result = (unsigned __int64)v16 >> 16;
    out[3] = v16;
    out[2] = v16 << 16 >> 24;
    out[1] = result;
    *out = v16 / 0x1000000;
    out += 4;
    if ( v17 == out )
      break;
    v20 = *v18;
    --v18;
    v16 = v20;
  }
  if ( v25 )
    sub_C0017628();
  return result;
}



这个setkey和encrypt与setp1里的34重蛋炒饭相似度很高啊(记住这个要点,后面step2就靠这个猜想了)




二、杀猪儆猴


decode_step1和decode_step3算法结构相似,只是密钥和置换表不同。

我没有逆setkey部分,直接内存中dump出setkey之后的结果,传给decrypt就行了。

decode_step1里1~34种蛋炒饭,举例第一次setkey结束的情况:

.text:00018EB8 STR R3, [SP,#0xAF0+key+0x3C] //这是setkey最后一步
.text:00018EBC MOV R11, #0 //从这开始循环解密数据。

虽然34种蛋炒饭比较长,但都是相同结构,费些体力就解决了。
 
decode_step3是kernel中前面已经贴了F5的代码,按简介这个就是第36种蛋炒饭了,和decode_step1同理可解决。
 


剩下的最后难点就是decode_step2了:

1.在不知道是js引擎的情况下调试了若干次,晕头转向

2.在知道了是js引擎,但不知道是哪种js引擎的情况下调试了若干次,继续晕头转向

3.在知道了是duktape的js引擎的情况下调试了若干次,仍然晕头转向


 
这个时候貌似要靠猜想了,第35种蛋炒饭?

咱就假设它和其它已知的35种蛋炒饭一样的算法结构行不?试试看吧。

已知的蛋炒饭算法都是异或和循环移位。

找来duktape的源码,编译examples\cmdline可以测试字节码,经验证结果是匹配的。

在代码里找到void duk__vm_bitwise_binary_op(...)这个函数,把异或和移位操作都打印出log。

例如xor的:


1
2
3
4
case DUK_OP_BXOR >> 2: {
    i3 = i1 ^ i2;
    printf("%08X ^ %08X = %08X\n",i1,i2,i3);    //这行是我加的
    break;


重新编译,运行dukcmd -b -i op.bin



测试得到log,列举一段:

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
duk> h("11111119222222273333333544444443")    //调用函数
...省略
22222227 33333335 11111112                //从捕捉到自己的输入开始,前面估计是setkey的
44444443 ^ FCF4D1C2 = B8B09581                //参考已知蛋炒饭的算法0xFCF4D1C2这个应该是setkey后的结果key[0]
11111112 ^ B8B09581 = A9A18493
A0000000 >> 0000001C = 0000000A
09000000 >> 00000018 00000009
00A00000 >> 00000014 = 0000000A
00010000 >> 00000010 00000001
00008000 >> 0000000C = 00000008
00000400 >> 00000008 00000004
00000090 >> 00000004 00000009
00000003 >> 00000000 00000003
00000023 << 00000018 23000000
0000006F << 00000010 = 006F0000
0000001B << 00000008 = 00001B00
000000CA << 00000000 = 000000CA
236F1BCA << 00000006 = DBC6F280
236F1BCA >> 0000001A = 00000008
236F1BCA ^ DBC6F288 = F8A9E942
236F1BCA << 0000000E = C6F28000
236F1BCA >> 00000012 = 000008DB
236F1BCA << 00000014 = BCA00000
236F1BCA >> 0000000C = 000236F1
C6F288DB ^ BCA236F1 = 7A50BE2A
F8A9E942 ^ 7A50BE2A = 82F95768
236F1BCA << 00000018 = CA000000
236F1BCA >> 00000008 = 00236F1B
82F95768 ^ CA236F1B = 48DA3873
11111119 ^ 48DA3873 = 59CB296A
33333335 44444443 77777776                //大概大这里结构开始重复,注意看后面移位的次数
59CB296A ^ 22827EBC = 7B4957D6                //参考已知蛋炒饭的算法0x22827EBC这个应该是setkey后的结果key[1]
77777776 ^ 7B4957D6 = 0C3E20A0
00000000 >> 0000001C = 00000000
0C000000 >> 00000018 = 0000000C
00300000 >> 00000014 00000003
000E0000 >> 00000010 = 0000000E
00002000 >> 0000000C = 00000002
00000000 >> 00000008 00000000
000000A0 >> 00000004 = 0000000A
00000000 >> 00000000 00000000
0000000D << 00000018 = 0D000000
00000007 << 00000010 00070000
00000052 << 00000008 00005200
00000071 << 00000000 00000071
0D075271 << 00000006 = 41D49C40
0D075271 >> 0000001A = 00000003


这样就可以把key都获取到了。



观察字节码可以看到:


1
2
3
4
5
00000300h: 5F 30 78 32 61 30 38 00 00 00 00 07 5F 30 78 32 ; _0x2a08....._0x2
00000310h: 32 62 33 00 00 00 00 01 61 00 00 00 00 0C 5A 4D ; 2b3.....a.....ZM
00000320h: 4F 70 77 71 4A 46 63 41 3D 3D 00 00 00 00 0C 64 ; OpwqJFcA==.....d
00000330h: 32 48 44 72 4D 4B 4F 77 34 4D 3D 00 00 00 00 0C ; 2HDrMKOw4M=.....
00000340h: 77 34 41 47 58 54 76 43 6C 51 3D 3D 00 00 00 00 ; w4AGXTvClQ==....


_0x2a08,_0x22b3,a这些都是变量名。



可以输出查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
duk> a
= {sbbbc:[[62,162,122,86,253,15,93,68,131,64,159,134,13,108,101,96],[207,233,88,163,199,250,83,105,248,16,147,132,59,228,201,235],
[82,156,198,8,251,219,47,144,245,31,212,34,80,118,55,217],[20,90,107,221,115,252,171,149,121,45,204,189,246,180,7,184],
[33,215,247,38,124,151,17,218,50,79,234,9,3,28,99,179],[71,222,65,106,4,123,164,160,169,229,137,220,177,24,94,52],
[195,140,241,0,14,84,12,142,244,168,154,66,40,210,104,178],[97,26,205,148,76,32,87,141,139,78,152,186,161,63,183,206],
[155,239,75,213,27,208,98,72,181,227,203,175,197,73,226,43],[57,225,176,202,196,254,36,193,126,21,130,165,37,110,2,128],
[113,111,209,236,109,22,102,145,211,35,243,46,153,166,223,39],[146,100,58,85,60,190,70,133,95,120,232,136,224,214,173,54],
[10,112,125,1,188,191,231,19,30,158,135,127,91,114,255,187],[119,29,167,77,238,69,170,240,150,5,157,51,41,6,61,25],
[194,185,200,174,192,143,237,230,182,129,116,49,81,92,172,67],[53,11,74,103,44,18,89,48,56,249,242,138,117,42,23,216]],
ccccckksgh:[1545928790,1819523012,1300536528,2040543102,927101669,2266960222,541058818,658447962,
1530502291,1640414457,2082075646,870663072,3952126004,2775248416,2211896835,3306793059,
2343141319,1441383902,4277648326,210210204,3142034540,1984503314,421625248,3717453135,
3748020838,2594357764,3689027317,1003144796,779877921,4265274753,2465069848,1588512741],
ffffffdk:[1228829654,750719941,2602314668,580818561],
bbb:{_func:true},ssssl:{_func:true},prefixInteger:{_func:true},ssssssbs:{_func:true},GGGGULB:{_func:true},PPPPPPULBE:{_func:true},
ssggg:{_func:true},kkkkkd:{_func:true},h16AddZero:{_func:true},cccctti:{_func:true},tttttth:{_func:true},ppppph:{_func:true},h:{_func:true}}


这前面那个256字节的数组sbbbc经验证就是置换表。



其实如果自己实现setkey就更好搞了,把ccccckksgh和ffffffdk传给setkey就行了。
 

三、吃蛋炒饭喽


整理前面分析流程,写出代码没几行,大部分都是key数据和置换表。

运行得到结果:18542cf63f2508f9632e6f8a49e0c298

而且发现因为没有区分字母大小写而存在多解共512个。
 
dukcmd工具、字节码和keygen见附件(附件下载地址:https://bbs.pediy.com/thread-259564.htm




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

最后于 2020-5-19 13:19 被Editor编辑 ,原因:
收藏
免费
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册