SECCON-CTF-2014-Decrypt-It-easy
k
e
y
w
o
r
d
s
:
keywords:
keywords: c语言rand()
,CRT
D e s c r i p t i o n Description Description
$ ./rnd crypt1.png ecrypt1.bin
附带给出rnd
,ecrypt1.bin
A n a l y s i s Analysis Analysis
由于在题目写到的类似命令的格式中,rnd
似乎是执行文件,linux
命令file
查看文件类型
$ file rnd
/rnd: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=6a1443272dd530117d3c63884e195120a845c499, stripped
看到是elf
文件,实际上就是linux
系统版本的exe
可执行文件(windows
可执行文件是exe
)
反编译rnd
文件,得到主函数
int __cdecl main(int a1, char **a2)
{
unsigned int v3; // eax
FILE *v4; // [esp+10h] [ebp-10h]
FILE *v5; // [esp+14h] [ebp-Ch]
char ptr; // [esp+1Fh] [ebp-1h] BYREF
if ( a1 <= 2 )
return 1;
v3 = time(0);
srand(v3);
v4 = fopen(a2[1], "rb");
v5 = fopen(a2[2], "wb");
while ( fread(&ptr, 1u, 1u, v4) == 1 )
{
ptr ^= rand() % 256;
fwrite(&ptr, 1u, 1u, v5);
}
fclose(v4);
fclose(v5);
return 0;
}
发现原来的文件数据与rand()
生成的随机数异或,并且使用了srand()
设置种子
并且种子seed = time(0)
,相当于当时的时间(是格林尼治时间1970年1月1日00:00:00到当前时刻的时长,时长单位是秒)
由于rand()
函数的性质,只要seed
一致,rand()
生成的随机数就是一样的,也就意味着我们可以借此恢复原文件数据(只要随机数与此时已知的ecrypt1.bin
文件数据相异或即可)
那么首先要找到当时的时间,而实际上ecrypt1.bin
就是通过这样的加密过程生成的,那么文件的生成时间一定是当时作为seed
的值,可以通过linux
命令stat
;得到
最近访问:2022-03-23 20:17:39.817896184 +0800
最近更改:2014-11-22 22:46:30.000000000 +0800
最近改动:2022-03-23 20:17:34.565896062 +0800
“最近更改”时间,需要将此时间转换为时间戳的形式
import time
t = "2014-11-22 22:46:30"
#转换成时间数组
timeArray = time.strptime(t, "%Y-%m-%d %H:%M:%S")
#转换成时间戳
timestamp = time.mktime(timeArray)
#1416667590.0
最后代入解密脚本即可(由于使用的是c语言的rand()
函数,如果是使用其他语言的随机数生成函数,可能是使用的不同的生成随机数算法,导致生成的随机数不同)
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE *cipher = fopen(argv[1], "rb");
FILE *plain = fopen(argv[2], "wb");
unsigned int seed = atoi(argv[3]);
int c;
srand(seed);
c = (fgetc(cipher) & 0xff) ^ (rand() & 0xff);
while (!feof(cipher)) {
fputc(c, plain);
c = (fgetc(cipher) & 0xff) ^ (rand() & 0xff);
}
fclose(plain);
fclose(cipher);
}
由于原来的加密程序是elf
,也即是linux
系统下执行的加密过程;所以我们需要在linux
编译这个解密脚本(windows
下编译此脚本生成的exe
无法使用)
gcc decode.c -o decode
进行解密
decode ecrypt1.bin output.png 1416667590.0
得到png
图片
发现这里的
N
N
N相对很小,可以直接找在线网站进行分解,也就已知三对p,q
;而
N
N
N这么小,可以猜测所加密使用的
m
m
m也很小,那么就意味着flag
被拆开成了三部分的较小的
m
m
m
由于加密过程完全没有按套路出牌(可能有相关论文介绍这种加密的解密方式,但是没有找到),既然 N N N这么小,并且 p , q p,q p,q也已知,可以尝试爆破
直接在模 N N N的情况下爆破是困难的,因为 N N N已经达到了 2 38 2^{38} 238左右,是不可能进行爆破的(也就是遍历在该模数情况下的所有值)
那么可以减小模数,将
N
N
N换做
p
,
q
p,q
p,q作为模数,因此把原来三个同余式转换为六个同余式;那么又由于原来三个同余式所加密的明文不同,需要分开考虑解密,也就是将两个同余式解密
{
C
p
≡
m
p
⋅
(
m
p
+
B
)
(
m
o
d
p
)
C
q
≡
m
q
⋅
(
m
q
+
B
)
(
m
o
d
q
)
\begin{cases} C_p \equiv m_p\cdot (m_p + B) \pmod p\\ C_q \equiv m_q\cdot (m_q + B) \pmod q \end{cases}
{Cp≡mp⋅(mp+B)(modp)Cq≡mq⋅(mq+B)(modq)
其中
C
p
C_p
Cp代表在模
p
p
p情况下的
C
C
C,同理
m
p
m_p
mp
当然这里的所有 B B B都是小于 p , q p,q p,q的,所以不用在原来的基础上模 p , q p,q p,q
那么由于 p , q p,q p,q是较小的,已经小于 2 20 2^{20} 220了,完全可以进行爆破(遍历该模数下所有的值,找出满足以上同余式的值)
找到之后就可以构造出
{
m
≡
m
p
(
m
o
d
p
)
m
≡
m
q
(
m
o
d
q
)
\begin{cases} m\equiv m_p \pmod p\\ m\equiv m_q \pmod q \end{cases}
{m≡mp(modp)m≡mq(modq)
同余式右侧数值以及模数已知,我们可以使用中国剩余定理(CRT)求解同余式方程组的根
由于可能有多个值满足条件,最后的结果转换为字节类型,观察是否是flag
的格式即可判断筛选
S o l v i n g c o d e Solving~code Solving code
from functools import reduce
import gmpy2
from Crypto.Util.number import *
p = [868019, 875543, 597263]
q = [913799, 904727, 890459]
C = [0x8D5051562B, 0x5FFA0AC1A2, 0x6008DDF867]
B = [0xFFEEE,0xFFFEE,0xFEFEF]
def CRT(moudle,a):
M = reduce((lambda x,y : x * y),moudle)
result = 0
for mi in moudle:
Mi = M // mi
inv_Mi = gmpy2.invert(Mi,mi)
result = (result + a[moudle.index(mi)] * Mi * inv_Mi) % M
return result % M
for i in range(0,len(p)):
for a1 in [m_p for m_p in range(p[i]) if (C[i] % p[i]) == (m_p * (m_p + B[i])) % p[i]]:
# 遍历所有小于p[i]的值
for a2 in [m_q for m_q in range(q[i]) if (C[i] % q[i]) == (m_q * (m_q + B[i])) % q[i]]:
# 遍历所有小于q[i]的值
x = CRT([p[i],q[i]],[a1,a2])
try:
if "2" not in bytes.decode(long_to_bytes(x)): # 简单过滤掉了一个不是flag的又是可见字符的字符串
print(bytes.decode(long_to_bytes(x)),end="")
except:
continue