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

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

2020-5-18 18:26
6626



第十三题《猪突豨勇》共有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:

.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。

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的:


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


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



测试得到log,列举一段:

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都获取到了。



观察字节码可以看到:


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这些都是变量名。



可以输出查看:

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




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

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