normal

normal

normal1

flag:C1CTF{th1s_Bas364_is_qcjlCwgS}

运行题目文件:

Welcome to C1CTF 2018!
Plz find the flag:

使用ExeinfoPe查看,确认为无壳64位ELF文件。放入IDA中查看:

int64 fastcall main(int a1, char a2, char a3)
{
int i; // [rsp+4h] [rbp-17Ch]
char v5[64]; // [rsp+20h] [rbp-160h] BYREF
char s2[264]; // [rsp+60h] [rbp-120h] BYREF
unsigned __int64 v7; // [rsp+168h] [rbp-18h]

v7 = readfsqword(0x28u);
puts(“Welcome to C1CTF 2018!”);
printf(“Plz find the flag:”);
isoc99_scanf(“%60s”, v5);
for ( i = 0; i < strlen(v5); ++i )
{
if ( (i & 1) != 0 )
v5[i] ^= 0x60u;
else
v5[i] ^= 0x91u;
}
sub_A84(v5, s2);
if ( !strcmp(“OBufaa21Td86rWS8Wob8iGhZYocbr5vxZfcCoWv3“, s2) )
puts(“Well done!”);
else
puts(“Try again!”);
return 0LL;
}

程序大致为将输入存储到数组,然后根据数组下标奇偶进行异或,再放入加密函数sub_A84中进行加密,最后将加密结果与比较字符串OBufaa21Td86rWS8Wob8iGhZYocbr5vxZfcCoWv3进行比较,然后根据结果输出对错。

我们跟进这个加密函数:

_BYTE *__fastcall sub_A84(**int64 a1, _BYTE *a2)
{
_BYTE *result; // rax
char v4; // [rsp+19h] [rbp-7h]
unsigned int8 v5; // [rsp+1Ah] [rbp-6h]
unsigned int8 i; // [rsp+1Bh] [rbp-5h]
int v7; // [rsp+1Ch] [rbp-4h]

v4 = 0;
v5 = 0;
v7 = 0;
while ( *(_BYTE *)(v7 + a1) )
{
a2++ = sub_7FA((unsigned __int8)(((int)(unsigned int8 *)(v7 + a1) >> (v5 + 2)) | v4));
v5 = (v5 + 2) & 7;
v4 = 0;
for ( i = 0; i < v5; ++i )
v4 |= ((1 << i) & *(unsigned int8 *)(v7 + a1)) << (6 - v5);
if ( v5 <= 5u )
++v7;
}
result = a2;
*a2 = 0;
return result;
}

跟进函数sub_7FA

__int64 __fastcall sub_7FA(char a1)
{
__int64 result; // rax

switch ( a1 )
{
case 0:
result = 110LL;
break;
case 1:
result = 111LL;
break;
case 2:
result = 112LL;
break;
case 3:
result = 113LL;
break;
case 4:
result = 114LL;
break;
case 5:
result = 115LL;
break;
case 6:
result = 116LL;
break;
case 7:
result = 117LL;
break;
case 8:
result = 118LL;
break;
case 9:
result = 119LL;
break;
case 10:
result = 120LL;
break;
case 11:
result = 121LL;
break;
case 12:
result = 122LL;
break;
case 13:
result = 97LL;
break;
case 14:
result = 98LL;
break;
case 15:
result = 99LL;
break;
case 16:
result = 100LL;
break;
case 17:
result = 101LL;
break;
case 18:
result = 102LL;
break;
case 19:
result = 103LL;
break;
case 20:
result = 104LL;
break;
case 21:
result = 105LL;
break;
case 22:
result = 106LL;
break;
case 23:
result = 107LL;
break;
case 24:
result = 108LL;
break;
case 25:
result = 109LL;
break;
case 26:
result = 48LL;
break;
case 27:
result = 49LL;
break;
case 28:
result = 50LL;
break;
case 29:
result = 51LL;
break;
case 30:
result = 52LL;
break;
case 31:
result = 53LL;
break;
case 32:
result = 54LL;
break;
case 33:
result = 55LL;
break;
case 34:
result = 56LL;
break;
case 35:
result = 57LL;
break;
case 36:
result = 65LL;
break;
case 37:
result = 66LL;
break;
case 38:
result = 67LL;
break;
case 39:
result = 68LL;
break;
case 40:
result = 69LL;
break;
case 41:
result = 70LL;
break;
case 42:
result = 71LL;
break;
case 43:
result = 72LL;
break;
case 44:
result = 73LL;
break;
case 45:
result = 74LL;
break;
case 46:
result = 75LL;
break;
case 47:
result = 76LL;
break;
case 48:
result = 43LL;
break;
case 49:
result = 47LL;
break;
case 50:
result = 77LL;
break;
case 51:
result = 78LL;
break;
case 52:
result = 79LL;
break;
case 53:
result = 80LL;
break;
case 54:
result = 81LL;
break;
case 55:
result = 82LL;
break;
case 56:
result = 83LL;
break;
case 57:
result = 84LL;
break;
case 58:
result = 85LL;
break;
case 59:
result = 86LL;
break;
case 60:
result = 87LL;
break;
case 61:
result = 88LL;
break;
case 62:
result = 89LL;
break;
case 63:
result = 90LL;
break;
default:
result = 63LL;
break;
}
return result;
}

发现是一个映射表,传入的运算结果作为下标映射字符。我们再分析传参的表达式:

(unsigned int8)(((int)*(unsigned __int8 *)(v7 + a1) >> (v5 + 2)) | v4)

v5为0,2,4,6的循环,(v7+a1)为传入的数组中的每一位元素,长度为八位。那么这个右移的位运算就是在取它的不同位。v4初值为零,,我们看之后改变v4值的式子:

for ( i = 0; i < v5; ++i )
v4 |= ((1 << i) & *(unsigned __int8 *)(v7 + a1)) << (6 - v5);

发现v4取得是数组中在前面传参式子中未被取到的位,那么或运算会将v4补到下一次运算上。分析下来我们可以知道,这就是base64的加密特征,所以我们根据base64编码的基本原理还原加密前数组:

[139,81,60,19,206,34,240,19,162,86,165,63,248,19,206,17,242,10,109,35,230,7,194,29]

(做题尝试的时候发现前6位为C1CTF{,所以偷个懒没有算^^)

再根据奇偶异或回去就可以得到答案了。

1
2
3
4
5
6
7
8
arr=[229,8,160,19,206,34,240,19,162,86,165,63,248,19,206,17,242,10,253,35,230,7,194,29]
str=''
for i in range(len(arr)):
if (i & 1)!=0:
arr[i]^=0x60
else:
arr[i]^=0x91
print('C1CTF{'+str.join(map(chr,arr)))

normal2

flag:hxb2018{853ecfe52aeb60989e8d3351}

IDA分析程序,有三个函数,先看第一个函数:sub_7FF65DF91000(String1)

传递进该函数的参数String1是个未初始化的字符数组,且该函数与输入无关。所以这是一个生成String1的值的函数。简单观察是将初始的16个值赋给String1在进行扩展,我们将其理解为一个密钥生成的过程。

在看第二个函数:*sub_7FF65DF912A0((unsigned int8 )Buffer, (int64)String1)

整体分析下来他进行了如下操作:先将输入到缓冲区Buffer的前16个字节与密钥String1前16字节异或,然后开始一个9轮的循环,每轮循环都先将Buffer的前16个字节映射到一个数组里面,然后进行行移位的操作,我们将16个字节的数据看成一个矩阵,从第一行开始每行元素依次向左位移0,1,2,3.接着执行一个函数:**sub_7FF65DF911B0((__int64)v2)**,分析他的函数:

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
char *__fastcall sub_7FF734C311B0(__int64 a1)
{
char *result; // rax
__int64 v2; // rbp
char v3; // si
char v4; // r9
char v5; // di
char v6; // r11
char v7; // bl

result = (char *)(a1 + 2);
v2 = 4i64;
do
{
v3 = *(result - 2);
v4 = *(result - 1);
v5 = result[1];
v6 = *result;
result += 4;
v7 = v4 ^ v3 ^ v6 ^ v5;
*(result - 6) = v7 ^ v3 ^ (2 * (v4 ^ v3)) ^ (0x1B * ((unsigned __int8)(v4 ^ v3) >> 7));
*(result - 5) = v7 ^ v4 ^ (2 * (v6 ^ v4)) ^ (0x1B * ((unsigned __int8)(v6 ^ v4) >> 7));
*(result - 4) = v7 ^ v6 ^ (2 * (v6 ^ v5)) ^ (0x1B * ((unsigned __int8)(v6 ^ v5) >> 7));
*(result - 3) = v7 ^ v5 ^ (2 * (v5 ^ v3)) ^ (0x1B * ((unsigned __int8)(v5 ^ v3) >> 7));
--v2;
}
while ( v2 );
return result;
}

是在对矩阵进行列操作,由前面分析基本确定这是一个**AES加密*,比对AES加密的列混淆步骤的代码,确定这是列混淆过程,是将原数据矩阵左乘一个固定矩阵。这步直接逆向很困难,询问师傅得知,若比对得知是标准的列混淆就用脚本直接解,一般都是标准的,若不太一样则仔细分析加密。最后再和密钥String1*的下一个16字节进行异或。9轮循环走完,还要再走一轮同样的操作,不过要省去列混淆的那一步。

最后一个函数在一个32轮的循环里,循环执行前先给String1的64字节赋零,然后执行函数,最后再将String1与给定数据进行字符串比较。动态调试发现此函数就是将Buffer中加密过的16字节数据和未加密的16字节打印到String1上。

分析结束我们知道这是一个标准的AES加密。我们用python的第三方库pycryptodome去求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import AES
mode = AES.MODE_ECB
key = b'\x1B\x2E\x35\x46\x58\x6E\x72\x86\x9B\xA7\xB5\xC8\xD9\xEF\xFF\x0C'
text = b'\x93\x4d\x87\x06\xbe\xd7\x4c\xd6\xee\xa6\x83\xc7\xbe\x86\xb2\xeb'
cryptos = AES.new(key,mode)
cipher_text = cryptos.decrypt(text)
arr=[]
for i in cipher_text:
arr.append(i)
SecondHalf=[0x32,0x61,0x65,0x62,0x36,0x30,0x39,0x38,0x39,0x65,0x38,0x64,0x33,0x33,0x35,0x31]
for i in range(16):
arr.append(SecondHalf[i])
flag=''
print(flag.join(map(chr,arr))+'}')

由于后16字节数据未作任何处理,我们直接转化成字符串一起输出,最后加上’}’就可以了。这里我们选择的AES模式为:ECB模式。

这里做一个笔记:

AES有5种加密模式:CBCECBCTRCFBOFB

电码本模式(Electronic Codebook Book (ECB)

这种模式是将整个明文分成若干段相同的小段,然后对每一小段进行加密。

  优点:
  1.简单;
  2.有利于并行计算;
  3.误差不会被传送;
  缺点:
  1.不能隐藏明文的模式;
  2.可能对明文进行主动攻击;

密码分组链接模式(Cipher Block Chaining (CBC))

这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。

  优点:
  1.不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
  缺点:
  1.不利于并行计算;
  2.误差传递;
  3.需要初始化向量IV

计算器模式(Counter (CTR))

计算器模式不常见,在CTR模式中,
有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次。

密码反馈模式(Cipher FeedBack (CFB))

优点:
  1.隐藏了明文模式;
  2.分组密码转化为流模式;
  3.可以及时加密传送小于分组的数据;
  缺点:
  1.不利于并行计算;
  2.误差传送:一个明文单元损坏影响多个单元;
  3.唯一的IV;

输出反馈模式(Output FeedBack (OFB))

 优点:
  1.隐藏了明文模式;
  2.分组密码转化为流模式;
  3.可以及时加密传送小于分组的数据;
  缺点:
  1.不利于并行计算;
  2.对明文的主动攻击是可能的;
  3.误差传送:一个明文单元损坏影响多个单元;

CFB和OFB较为复杂,这里不多赘述

标准AES加解密脚本;

1
2
3
4
5
6
7
8
9
10
from Crypto.Cipher import AES

key = b'' #秘钥,b就是表示为bytes类型
text = b'' #需要加密的内容,bytes类型
aes = AES.new(password,AES.MODE_ECB) #创建一个aes对象
# AES.MODE_ECB 表示模式是ECB模式
en_text = aes.encrypt(text) #加密明文
print(en_text) #bytes类型
den_text = aes.decrypt(en_text) # 解密密文
print(den_text)#bytes类型

normal3

flag:nctf{bc2e3b4c2eb03258c5102bf9de77f57dddad9edb70c6c20febc01773e5d81947}

放入IDA分析最关键的就是在输入后的*sub_400666(0)*函数。跟进查看函数的具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall sub_400666(int a1)
{
int v1; // eax
__int64 result; // rax

if ( a1 <= 63 )
{
v1 = dword_601064++;
*(&s1 + v1) = byte_601100[a1];
sub_400666(2 * a1 + 1);
return sub_400666(2 * (a1 + 1));
}
return result;
}

很显然这是一个函数对自身的递归调用。byte_601100为存放着我们输入的那一段内存的首地址。所以这个递归函数的作用即通过递归改变下标a1的值然后将byte_601100[a1]赋给s1,也就是打乱输入,然后和给定字符串比较验证。byte_601100我在调试的时候查看他的值都只能查看到他这个首地址存储的值,无法看到内存中全部的值。解决方法是查看好byte_601100的地址,在Hex View窗口中摁G键,输入byte_601100的地址,跳转直接查看内存就好啦。这个递归算法大概是通过两步传参 (2 * a1 + 1)(2 * (a1 + 1))来改变下标,然后通过判断语句if ( a1 <= 63 )来使得操作能覆盖64个输入的字节。0,1,3,7,15,31,63,到127,不满足判断,开始返回,直到返回到参数为15的那一层函数,再进行下一步return sub_400666(2 * (a1 + 1))(如果是31的话执行此式不满足判断会直接返回),然后以此类推,对输入的64进行打乱。其实对此递归算法的理解并不影响解题。因为算法固定,所以,打乱前和打乱后的下标所具有的一一对应的关系不变,所以只需要模拟出这个递归算法,然后将打乱后下标的顺序打印到数组里面来对给定的用来验证的字符串进行还原就好了。故写出脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def recursion(a):
global arr2
if a<=63:
arr2.append(a)
recursion(2*a+1)
return recursion(2*(a+1))
return
arr2=[]
a=0
recursion(a)
print(arr2)
compare_data="bcec8d7dcda25d91ed3e0b720cbb6cf202b09fedbc3e017774273ef5d5581794"
flag_arr=[0 for i in range(64)]
for i in range(64):
flag_arr[arr2[i]]=compare_data[i]
flag=''
print('nctf{' + flag.join(flag_arr) + '}')

后面还有一个验证也是一个道理,既然能通过第一个验证说明我们的输入就已经被固定了,由最后的输出又得到我们的输入直接就是flag,所以只用模拟还原一个递归算法就好。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2022-2023 Syclover.Kama
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信