37

看雪.京东 2018 CTF 第七题 密室逃脱 点评与解析

 5 years ago
source link: http://www.10tiao.com/html/523/201806/2458290309/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在淅淅沥沥的雨水中迎来周末,

气温有所下降,上海的温度正宜人。

各位小伙伴们可以开心的打CTF 比赛啦 ~

来看看过去的第七题结束后,

第七题出题者以被18人攻破的成绩,位列第二位


本题过后 攻击方前十名有略微的变动:

aps成功冲进前十位

AloneWolf 从第九名上升至第六名

oooAooo 也上升一位至前三名

革命尚未成功,朋友们仍需努力,

 加油!



看雪版主&评委 netwind 点评


本题作者提供了一个非常精彩的算法题目,一共进行了20轮变换操作,每轮变换都用了混淆函数运算和矩阵运算,每轮变换的输入是16个数据, 输出也是16个数据, 本轮输出数据作为下一轮输入,20轮变换之后 将变换结果与存储于程序中的理想结果进行比对 以确定输入序列号是否正确。分析出算法模型后便可以快速求解,或调用算法代码暴力枚举亦可解。



看雪.京东 2018 CTF 第七题 作者简介


看场雪

bbs.pediy.com/user-697893

第七题出题者简介:


ID:看场雪

星座:射手座

性格:追求完美

兴趣:白盒,芯片安全,BD,机器人,网络验证码,虚拟化安全

最喜欢的BGM:Supermassive Black Hole



看雪.京东 2018 CTF 第七题 设计思路


这是一个数字迷宫,分为20层楼。


16个人各选择一把初始钥匙,开始游戏。


16个人进入第一层,按照一定的规则,每个人都会在另外2个人的帮助下开启一个房间,拿到1把第二层楼的钥匙。


当16个人都拿到了各自的二层楼钥匙后,一起上楼。


第二层重复相同的操作,16个人再一起上到第三层。

...

直至16个人拿到了的20层天台的钥匙。


天台上有一架直升机,需要16把正确的钥匙才能开走。


试试看 你能否找出16把正确的初始钥匙


有人可能会想,只要砸碎直升机的门,不就可以把直升机开走了吗?

(那不算胜利哦)


在你开着直升机远去的时候,迷宫大楼的中间,那些曾经被你点亮的房间 会组成一个横幅。


只有在你拿到了正确钥匙的情况下,才会出现成功逃脱的横幅(那才算胜利哟)


程序运行起来后,请输入16个初始钥匙。


如果验证正确,最后会显示出正确的横幅。


否则,算作挑战失败。



看雪.京东 2018 CTF 第七题解析


*本解析来自看雪论坛 qwertyaa



逆向部分



这道题非常友好,还给了提示,不过这里“每个人都会在另外2个人的帮助下开启一个房间”,似乎应该是“每个人都会在3个人的帮助下开启一个房间”。


程序不长,我们先按着提示,把这个程序用IDA分析,并完整的逆出来,最后可以随便输出一个串和IDA动态调试(在没有反调试、花指令的前提下,用IDA的源码级调试非常方便)的结果比对来验证逆的对不对,如下:


#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
void doPause(){
#ifdef linux
   printf("Press enter to exit.");
   fflush(stdout);
   system("read unusedVar");
#else
   system("pause");
#endif
}
typedef unsigned char uchar;
typedef unsigned int uint;
uchar key[16]={0x14,0x22,0x1E,0x10,0x38,0x30,0x18,0x10,0x4,0x1A,0x24,0x8,0x2,0x26,0x38,0x2A};
char trans[]="abcdefghijklmnopqrstuvwxyz+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char input[20];
uchar box1[16][16],box2[64][64][64];
void judge(){
   uchar box[22][16];
   for(int i=0;i<16;i++){
       int j=0;
       while(j<64&&trans[j]!=input[i])j++;
       if(j>=64) {
           puts("input error");
           doPause();
           exit(-1);
       }
       box[0][i]=j;
   }
   puts("");
   for(int i=0;i<20;i++){
       for(int j=0;j<16;j++){
           int a[3]={0},p=0;
           for(int k=0;k<16;k++){
               int l=(k+j)%16;
               if(box1[j][l])
                   a[p++]=box[i][l];
           }
           box[i+1][j]=box2[a[0]][a[1]][a[2]];
       }
   }
   for(int i=0;i<16;i++)if(key[i]!=box[19][i]){
       puts("serial error");
       return;
   }
   for(int i=0;i<16;i++)printf("%c",trans[box[9][i]>>1]);
   puts("");
}
void readFile(const char*fn,void*buf){
   FILE*f=fopen(fn,"rb");
   fseek(f,0,SEEK_END);
   uint size=ftell(f);
   fseek(f,0,SEEK_SET);
   fread(buf,size,1,f);
   fclose(f);
}
int main(){
   readFile("dump.hex",box1);
   readFile("dump2.hex",box2);

   puts("Only 'a'-'z','A'-'Z','0'-'9' accepted.");
   puts("Only 16 chars accepted.");
   printf("Serial:");
   scanf("%17s",input);
   if(strlen(input)>16)input[0]=0;//ployfill for scanf_s
   judge();

   doPause();
   return 0;
}


其中这里的dump.hex原本是judge函数里的一个局部变量v24,可用IDA直接dump下来,dump2.hex原本是全局变量byte_139FEF0,同样可直接dump下来。


dump可用这样一段IDC代码来实现(来源于网络):

auto fp, begin, end, i;
fp = fopen("dump2.hex", "wb");
begin = 0x139FEF0;
end = begin+64*64*64;
for ( i = begin; i < end; i ++ ) fputc(Byte(i), fp);
fclose(fp);



分析部分



观察可知,box1是每行只有三个元素的01矩阵,每行为1的位置恰有3个,每行为1的元素分布如下(由于题目中搜索这个矩阵的时候每行都是从(0+行号)%16到(15+行号)%16,下面也是按这个顺序给出,同时我们将下面这个表记为p[16][3]):


0x0,0x1,0x2

0x3,0x4,0x0

0x5,0x6,0x0

0x5,0x7,0x1

0x4,0x6,0x1

0x8,0x2,0x3

0x9,0x2,0x4

0x7,0xa,0x3

0x8,0xc,0x5

0xb,0xd,0x6

0xc,0xd,0x7

0xb,0xe,0x9

0xe,0x8,0xa

0xf,0x9,0xa

0xf,0xb,0xc

0xf,0xd,0xe


接着我们观察box2,发现恰好等于x的字节恰有4096个,于是猜测并成功验证(其实我只验证了一部分,不过够了):


f_y,z(x)=box2[x][y][z]

f_x,z(y)=box2[x][y][z]

f_x,y(z)=box2[x][y][z]


都是双射函数,也就是说对于方程box2[a][b][c]=d,我们知道a,b,c中的2个可以直接求余下的1个。


而题目中每层的变换如下(由x[16]到y[16]):


y[i]=box2[x[p[i][0]]][x[p[i][1]]][x[p[i][2]]]


所以我们倒着来的时候每层相当于有如下16元方程组(求x[16],其余为已知变量):


y[i]=box2[x[p[i][0]]][x[p[i][1]]][x[p[i][2]]],i=0,1,2...15 


观察p可知,如果我们知道x[0]、x[1]、x[3],剩下的每个x我们都可以通过box2“知2求1”的特性求出(这里对于每个方程都有三个依赖,依赖减掉两个之后就可以求出这个方程的余下一个了,这过程有点类似于拓扑排序)。


这样我们先预处理出p和p中的依赖关系表和“知2求1”的过程:


//p和p中的依赖关系
int p[16][3];
int rp[16][4]={0};
for(int j=0;j<16;j++){
   int q=0;
   for(int k=0;k<16;k++){
       int l=(k+j)%16;
       if(box1[j][l])
           p[j][q++]=l,rp[l][rp[l][3]++]=j;
   }
}
//“知2求1”的过程
uchar rbox1[64][64][64],rbox2[64][64][64],rbox3[64][64][64];
for(int i=0;i<64;i++){
   for(int j=0;j<64;j++){
       for(int k=0;k<64;k++)rbox1[box2[i][j][k]][j][k]=i;
   }
}
for(int i=0;i<64;i++){
   for(int j=0;j<64;j++){
       for(int k=0;k<64;k++)rbox2[i][box2[i][j][k]][k]=j;
   }
}
for(int i=0;i<64;i++){
   for(int j=0;j<64;j++){
       for(int k=0;k<64;k++)rbox3[i][j][box2[i][j][k]]=k;
   }
}


然后写出先暴力枚举x[0]、x[1]、x[3]然后求余下的x[i]逆并用剩下的方程校对枚举的x[i]是否正确的过程:

uchar rbox1[64][64][64],rbox2[64][64][64],rbox3[64][64][64];
int p[16][3];
int rp[16][4]={0};
bool test(uchar x,uchar y,uchar z,uchar key[16],uchar res[16]){
   const int A=0,B=1,C=3;
   res[A]=x;
   res[B]=y;
   res[C]=z;
   uchar d[16];
   for(int i=0;i<16;i++)d[i]=3;
   bool vt[16]={0};
   vt[A]=vt[B]=vt[C]=1;
   for(int z=0;z<3;z++)d[rp[A][z]]--;
   int q[20]={A,B,C};
   int f=0,l=2;
   while(f<l){
       f++;
       for(int z=0;z<3;z++){
           int v=rp[q[f]][z];
           d[v]--;
           if(d[v]<=1){
               l++;
               int a0=p[v][0];
               int a1=p[v][1];
               int a2=p[v][2];
               int k=key[v];
               if(!vt[a0]){
                   res[a0]=rbox1[k][res[a1]][res[a2]];
                   vt[a0]=1;
                   q[l]=a0;
               }else if(!vt[a1]){
                   res[a1]=rbox2[res[a0]][k][res[a2]];
                   vt[a1]=1;
                   q[l]=a1;
               }else if(!vt[a2]){
                   res[a2]=rbox3[res[a0]][res[a1]][k];
                   vt[a2]=1;
                   q[l]=a2;
               }else{
                   if(k!=box2[res[a0]][res[a1]][res[a2]])return false;
                   l--;
               }
           }
       }
   }
   return l==15;
}
void rev(uchar key[16],uchar res[16]){
   int cnt=0;
   for(int i=0;i<64;i++){
       for(int j=0;j<64;j++){
           for(int k=0;k<64;k++){
               if(test(i,j,k,key,res))return;
           }
       }
   }
}


这样我们就可以得出最终的flag为 rPFf9bJl3tXj93ZJ


而最后输出的内容实际上由第10层开启的房间的编号得出(这恰好和pdb目录信息中的MidPointVerify吻合),内容是yourserialisgood。我猜作者大概就是先写好了这串内容后逆推出一开始的flag的。(当然由于kanxue的要求,这里要恰好没有’+‘、‘-’出现)


完整的逆推程序如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef unsigned char uchar;
typedef unsigned int uint;
uchar key[16]={0x14,0x22,0x1E,0x10,0x38,0x30,0x18,0x10,0x4,0x1A,0x24,0x8,0x2,0x26,0x38,0x2A};
char trans[]="abcdefghijklmnopqrstuvwxyz+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char input[20];
uchar box1[16][16],box2[64][64][64],rbox1[64][64][64],rbox2[64][64][64],rbox3[64][64][64];
void judge(){
   uchar box[22][16];
   for(int i=0;i<16;i++){
       int j=0;
       while(j<64&&trans[j]!=input[i])j++;
       box[0][i]=j;
   }
   for(int i=0;i<20;i++){
       for(int j=0;j<16;j++){
           int a[3]={0},p=0;
           for(int k=0;k<16;k++){
               int l=(k+j)%16;
               if(box1[j][l])
                   a[p++]=box[i][l];
           }
           box[i+1][j]=box2[a[0]][a[1]][a[2]];
       }
   }
   for(int k=0;k<=19;k++){
       for(int i=0;i<16;i++)printf("%02x ",box[k][i]);
       puts("");
   }
   for(int i=0;i<16;i++)if(key[i]!=box[19][i])puts("serial error");

   for(int i=0;i<16;i++)printf("%c",trans[box[9][i]>>1]);
   puts("");
}
void readFile(const char*fn,void*buf){
   FILE*f=fopen(fn,"rb");
   fseek(f,0,SEEK_END);
   uint size=ftell(f);
   fseek(f,0,SEEK_SET);
   fread(buf,size,1,f);
   fclose(f);
}
int p[16][3];
int rp[16][4]={0};
bool test(uchar x,uchar y,uchar z,uchar key[16],uchar res[16]){
   const int A=0,B=1,C=3;
   res[A]=x;
   res[B]=y;
   res[C]=z;
   uchar d[16];
   for(int i=0;i<16;i++)d[i]=3;
   bool vt[16]={0};
   vt[A]=vt[B]=vt[C]=1;
   for(int z=0;z<3;z++)d[rp[A][z]]--;
   int q[20]={A,B,C};
   int f=0,l=2;
   while(f<l){
       f++;
       for(int z=0;z<3;z++){
           int v=rp[q[f]][z];
           d[v]--;
           if(d[v]<=1){
               l++;
               int a0=p[v][0];
               int a1=p[v][1];
               int a2=p[v][2];
               int k=key[v];
               if(!vt[a0]){
                   res[a0]=rbox1[k][res[a1]][res[a2]];
                   vt[a0]=1;
                   q[l]=a0;
               }else if(!vt[a1]){
                   res[a1]=rbox2[res[a0]][k][res[a2]];
                   vt[a1]=1;
                   q[l]=a1;
               }else if(!vt[a2]){
                   res[a2]=rbox3[res[a0]][res[a1]][k];
                   vt[a2]=1;
                   q[l]=a2;
               }else{
                   if(k!=box2[res[a0]][res[a1]][res[a2]])return false;
                   l--;
               }
           }
       }
   }
   return l==15;
}
void rev(uchar key[16],uchar res[16]){
   int cnt=0;
   for(int i=0;i<64;i++){
       for(int j=0;j<64;j++){
           for(int k=0;k<64;k++){
               if(test(i,j,k,key,res))return;
           }
       }
   }
}
void runOnce(uchar key[16]){
   for(int j=0;j<16;j++){
       int a[3]={0},p=0;
       for(int k=0;k<16;k++){
           int l=(k+j)%16;
           if(box1[j][l])
               a[p++]=key[l];
       }
       printf("%02x ",box2[a[0]][a[1]][a[2]]);
   }
   puts("");
}
int main(){
   readFile("dump.hex",box1);
   readFile("dump2.hex",box2);

   for(int j=0;j<16;j++){
       int q=0;
       for(int k=0;k<16;k++){
           int l=(k+j)%16;
           if(box1[j][l])
               p[j][q++]=l,rp[l][rp[l][3]++]=j;
       }
   }
   for(int i=0;i<64;i++){
       for(int j=0;j<64;j++){
           for(int k=0;k<64;k++)rbox1[box2[i][j][k]][j][k]=i;
       }
   }
   for(int i=0;i<64;i++){
       for(int j=0;j<64;j++){
           for(int k=0;k<64;k++)rbox2[i][box2[i][j][k]][k]=j;
       }
   }
   for(int i=0;i<64;i++){
       for(int j=0;j<64;j++){
           for(int k=0;k<64;k++)rbox3[i][j][box2[i][j][k]]=k;
       }
   }

   uchar res[22][16];
   memcpy(res[19],key,sizeof key);
   for(int i=19;i>0;i--){
       for(int j=0;j<16;j++)printf("%02x ",res[i][j]);puts("...");
       rev(res[i],res[i-1]);
       for(int j=0;j<16;j++)printf("%02x ",res[i-1][j]);puts("");
       runOnce(res[i-1]);
   }
   char flag[17];flag[16]=0;
   for(int i=0;i<16;i++){
       flag[i]=trans[res[0][i]];
   }
   puts(flag);

   strcpy(input,flag);
   judge();
   return 0;
}


附件内容是上文所述的dump.hex和dump2.hex。







合作伙伴

京东集团是中国收入最大的互联网企业之一,于2014年5月在美国纳斯达克证券交易所正式挂牌上市,业务涉及电商、金融和物流三大板块。

 

京东是一家技术驱动成长的公司,并发布了“第四次零售革命”下的京东技术发展战略。信息安全作为保障业务发展顺利进行的基石发挥着举足轻重的作用。为此,京东信息安全部从成立伊始就投入大量技术和资源,支撑京东全业务线安全发展,为用户、供应商和京东打造强大的安全防护盾。

 

随着京东全面走向技术化,大力发展人工智能、大数据、机器自动化等技术,将过去十余年积累的技术与运营优势全面升级。面向AI安全、IoT安全、云安全的机遇及挑战,京东安全积极布局全球化背景下的安全人才,开展前瞻性技术研究,成立了硅谷研发中心、安全攻防实验室等,并且与全球AI安全领域知名的高校、研究机构建立了深度合作。

 

京东不仅积极践行企业安全责任,同时希望以中立、开放、共赢的态度,与友商、行业、高校、政府等共同建设互联网安全生态,促进整个互联网的安全发展。


CTF 旗帜已经升起,等你来战!

扫描二维码,立即参战!



看雪.京东 2018 CTF 





看雪2018安全开发者峰会

2018年7月21日,拥有18年悠久历史的老牌安全技术社区——看雪学院联手国内最大开发者社区CSDN,倾力打造一场技术干货的饕餮盛宴——2018 安全开发者峰会,将在国家会议中心隆重举行。会议面向开发者、安全人员及高端技术从业人员,是国内开发者与安全人才的年度盛事。此外峰会将展现当前最新、最前沿技术成果,汇聚年度最强实践案例,为中国软件开发者们呈献了一份年度技术实战解析全景图。



戳下图,立即购票,享5折优惠!







戳原文,立刻加入战斗!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK