前言

打比赛的时候遇到了加解密脚本编写,所以想写一篇总结,正好把逆向常见的那几个加密算法也总结总结,虽然自己也不太想学密码,但是该来的跑不掉
本来这篇文章我想的标题是常见加密算法,但考虑到MD5是hash,base是编码,所以就给这篇文章起名为常见密码学算法

编码

把数据从一种格式转换为另一种格式,用于传输或存储,不需要密码

base64

Base64编码每3个字节为一个编码组,对应的24个二进制位,然后按照每6位进行分组,分为4组,计算每组的值通过映射Base64编码表获得对应的值
下述实例为base64.b64decode(Man)=TWFu
b032393761228828d783be5953f4c8a2

根据上述图表,一个编码组的所有种类是2的6次方,也就是base64的字符集就得有64个字符相照应,这也是64的由来,64就是字符集的大小

c代码实现

1
2
3
4
out[o++] = b64_table[(b0 >> 2) & 0x3F]; //取 b0 的高 6 位
out[o++] = b64_table[((b0 & 0x3) << 4) | ((b1 >> 4) & 0x0F)];//取 b0 的低 2 位 + b1 的高 4 位,拼成 6 位。
out[o++] = b64_table[((b1 & 0x0F) << 2) | ((b2 >> 6) & 0x03)];//取 b1 的低 4 位 + b2 的高 2 位。
out[o++] = b64_table[b2 & 0x3F];//取 b2 的低 6 位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#换码base64加密
import base64
table1 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/=' # 自定义Base64 编码表
table2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' # 标准Base64 编码表

def custom_base64_decode(data): #解密
std_b64 = ''.join(table2[table1.index(c)] if c in table1 else c for c in data) #简单的映射变换
return base64.b64decode(std_b64) #标准base解密
def custom_base64_encode(raw_bytes): #加密
std_b64 = base64.b64encode(raw_bytes).decode() #标准加密
return ''.join(table1[table2.index(c)] if c in table2 else c for c in std_b64)
encoded = 'pCNxpTIUejgMhkgQdiQOdjB3bjwTdPgJgzcSdyQRgPoNcz16c44Vh31Z'
flag = custom_base64_decode(encoded)
print(flag.decode('utf-8', errors='ignore'))
# 验证反向编码
print(custom_base64_encode(flag))

对称加密

块密码

TEA / XTEA / XXTEA

TEA(Tiny Encryption Algorithm,微型加密算法)是一种结构简单、速度快、实现容易的分组对称加密算法,曾广泛用于早期嵌入式设备、游戏加密、移动通信协议。

注意如果加密中的sum是累加器,解密就得是累减器。累加中的sum初始加密是0,累减中的sum就是32delta减到0的过程,当然sum不一定是0,加密中的sum也不一定是累加
解密就是倒着来一遍,*累加变成累减,累减变成累加

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
#include <stdio.h>
#include <stdint.h>
// TEA 加密函数
void encrypt(uint32_t* v, const uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0; //累加器
uint32_t delta = 0x9e3779b9; //黄金比例常数,用于打乱循环效果
for (int i = 0; i < 32; i++) { //设置32 轮加密,可修改
sum += delta;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]); //通过v1和k和sum得到新的v0
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);//通过新的v0和k和sum得到新的v1
}
v[0] = v0;
v[1] = v1;
}
// TEA 解密函数
void decrypt(uint32_t* v, const uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t delta = 0x9e3779b9;
uint32_t sum = delta * 32; //初始sum为delta*加密轮数
for (int i = 0; i < 32; i++) {
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);//还原v1
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);//还原v0
sum -= delta;
}
v[0] = v0;
v[1] = v1;
}
int main() {
uint32_t v[2] = {0x12345678, 0x9abcdef0}; //明文长度必须是 8 字节(64 位)的倍数,明文分组(64位 → 两个32位整数)
uint32_t k[4] = {0x11111111, 0x22222222, 0x33333333, 0x44444444}; //密钥(128位 → 四个32位整数)
printf("原文: %08X %08X\n", v[0], v[1]);
encrypt(v, k);
printf("加密: %08X %08X\n", v[0], v[1]);
decrypt(v, k);
printf("解密: %08X %08X\n", v[0], v[1]);
return 0;
}

XTEA 是 TEA 的扩展,也称做 TEAN,四个子密钥采取不正规的方式进行混合以阻止密钥表攻击
XTEA与TEA的不同点在for循环里

1
2
3
4
5
6
7
8
//加密
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);//XTEA 每轮的key选择由sum决定,密钥使用顺序更混乱;
sum += DELTA; //XTEA 通过把 sum 的更新时机放到两次数据运算之间
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
//解密
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= DELTA;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);

前面的xtea和tea,在加密前需要两两分组,然后组内多轮相交
XXTEA 每轮都会让每个块都和它的前后块交互:每个块同时受到上下邻块的影响 → 扩散非常彻底。并且XXTEA支持任意长度(n×32位)

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
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define DELTA 0x9e3779b9
// ----------------------------
// XXTEA 加密
// v: 数据块数组 (长度 n)
// n: 数据块数量(每块32位)
// k: 128位密钥数组(4个32位整数)
// ----------------------------
void xxtea_encrypt(uint32_t *v, int n, const uint32_t k[4]) {
if (n < 2) return; // 至少两个32位块才能加密
uint32_t sum = 0;
uint32_t rounds = 6 + 52/n; // 推荐轮数公式
uint32_t z = v[n-1], y, e, p;
while (rounds-- > 0) {
sum += DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < n-1; p++) {
y = v[p+1];
// 核心混合公式:
v[p] += (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p & 3) ^ e] ^ z));
z = v[p];
}
//最后一块没有参加循环,最后一块与第一块混合,保证循环
y = v[0];
v[n-1] += (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p & 3) ^ e] ^ z));
z = v[n-1];
}
}
// XXTEA 解密
void xxtea_decrypt(uint32_t *v, int n, const uint32_t k[4]) {
if (n < 2) return;
uint32_t rounds = 6 + 52/n;
uint32_t sum = rounds * DELTA;
uint32_t z, y = v[0], e, p;

while (sum != 0) {
e = (sum >> 2) & 3;
for (p = n-1; p > 0; p--) {
z = v[p-1];
v[p] -= (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p & 3) ^ e] ^ z));
y = v[p];
}
z = v[n-1];
v[0] -= (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(0 & 3) ^ e] ^ z));
y = v[0];
sum -= DELTA;
}
}

DES

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
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes

key = b'12345678' # DES 密钥长度必须 8 字节
data = b'hello world' # 11 字节明文

# DES 块大小 = 8 字节,填充到块大小倍数
data_padded = pad(data, 8)#默认使用 PKCS#5 填充
print("填充后的明文:", data_padded) # b'hello world\x05\x05\x05'

# ECB 模式
cipher_ecb = DES.new(key, DES.MODE_ECB)
encrypted_ecb = cipher_ecb.encrypt(data_padded)
print("DES-ECB 加密:", encrypted_ecb.hex())

decrypted_ecb_padded = cipher_ecb.decrypt(encrypted_ecb)
decrypted_ecb = unpad(decrypted_ecb_padded, 8)
print("DES-ECB 解密:", decrypted_ecb)

# CBC 模式
# 生成随机 IV(初始化向量)
iv = get_random_bytes(8) # DES 块大小 = 8 字节
cipher_cbc = DES.new(key, DES.MODE_CBC, iv)
encrypted_cbc = cipher_cbc.encrypt(data_padded)
print("DES-CBC 加密:", encrypted_cbc.hex())

cipher_cbc_dec = DES.new(key, DES.MODE_CBC, iv)
decrypted_cbc_padded = cipher_cbc_dec.decrypt(encrypted_cbc)
decrypted_cbc = unpad(decrypted_cbc_padded, 8)
print("DES-CBC 解密:", decrypted_cbc)

AES

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes

key = b'1234567812345678' #加密密钥,AES-128要求密钥为16 字节
data = b'hello world' # 11 字节

# 填充到 16 的倍数,AES是块加密算法,每次只能加密固定长度的块:如果明文长度不是16字节的倍数,直接加密会报错或数据不完整
data_padded = pad(data, 16) #默认使用 PKCS#7 填充
print(data_padded) # b'hello world\x05\x05\x05\x05\x05'

#ecb模式
# 加密
cipher = AES.new(key, AES.MODE_ECB)
encrypted = cipher.encrypt(data_padded)
print("AES-ECB 加密:", encrypted.hex())
# 解密
decrypted_padded = cipher.decrypt(encrypted)
decrypted = unpad(decrypted_padded, 16)# 去掉填充
print("AES-ECB 解密:", decrypted)

#CBC 模式
# 生成随机 IV(初始化向量)
iv = get_random_bytes(16) # AES 块大小 = 16 字节

# 加密
cipher_cbc = AES.new(key, AES.MODE_CBC, iv)
encrypted_cbc = cipher_cbc.encrypt(data_padded)
print("AES-CBC 加密:", encrypted_cbc.hex())
# 解密
cipher_cbc_dec = AES.new(key, AES.MODE_CBC, iv)
decrypted_cbc_padded = cipher_cbc_dec.decrypt(encrypted_cbc)
decrypted_cbc = unpad(decrypted_cbc_padded, 16) #
print("AES-CBC 解密:", decrypted_cbc)

流密码

RC4

RC4 算法广泛应用于 SSL/TLS 协议和 WEP/WPA 协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import ARC4

key = b'mysecretkey' # 确保你的key长度是16字节
data = b'Hello, World!' # 要加密的数据
cipher = ARC4.new(key) #创建一个 RC4(ARC4)流加密器
# 加密数据
encrypted_data = cipher.encrypt(data)
print("Encrypted:", encrypted_data)


# 解密时需要重新创建 cipher
cipher_dec = ARC4.new(key)
decrypted_data = cipher_dec.decrypt(encrypted_data)
print("Decrypted:", decrypted_data)
具体实现
  1. 根据密钥初始化置换盒 S,生成置换盒S
  2. 根据置换盒S生成密钥流,将明文与密钥流异或就能生成密文,密文与密钥流异或就能还原成明文。

原理很简单,上述加解密的原理本质就是异或
上述实现步骤主要分为两步,分别是初始化置换盒 S,和根据置换盒S生成密钥流,这两步都要逐字节处理信息,在这个过程中还会通过交换来打乱混淆,并且还伴随着累加。
至此如果不知道密钥,就无法实际计算出相同密钥流。

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
#include <stdio.h>
#include <string.h>

// 初始化置换盒 S
void rc4_init(unsigned char *s, unsigned char *key, int keylen) {
int i, j = 0;
unsigned char temp;
for (i = 0; i < 256; i++) s[i] = i; // 初始化 S 为 0~255,线系排序
for (i = 0; i < 256; i++) {
j = (j + s[i] + key[i % keylen]) % 256;// 通过密钥映射和累加打乱 S
// 在逐字节初始化置换盒 S的同时,还在不断的交换 s[i] 和 s[j]
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}
// 加密 / 解密(同一个函数)
void rc4_crypt(unsigned char *s, unsigned char *data, int datalen) {
int i = 0, j = 0, k, t;
unsigned char temp;
for (k = 0; k < datalen; k++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
// 交换 s[i] 和 s[j]
temp = s[i];
s[i] = s[j];
s[j] = temp;
t = (s[i] + s[j]) % 256;// 生成密钥流 t

data[k] ^= s[t];// 将密钥流与明文/密文异或,这就是为什么加解密是同一个函数
}
}

// 测试示例
int main() {
unsigned char key[] = "secret"; // 密钥
unsigned char data[] = "Hello, RC4!"; // 明文
unsigned char s[256]; // S 盒
rc4_init(s, key, strlen((char *)key)); // 初始化
rc4_crypt(s, data, strlen((char *)data)); // 加密
printf("加密后: ");
for (int i = 0; i < strlen((char *)data); i++)
printf("%02X ", data[i]);
printf("\n");

// 再次初始化并解密(RC4 对称)
rc4_init(s, key, strlen((char *)key));// 再次初始化
rc4_crypt(s, data, strlen((char *)data)); //使用相同的函数处理即可解密
printf("解密后: %s\n", data);

return 0;
}

对称加密密钥封装格式

用户输入的是口令(password),而分组密码要的是固定长度的 key(AES-256 要 32 字节)和 iv(16 字节)。把口令变成 key/iv 的这一步叫 KDF(密钥派生),它本身不是加密:

  • 口令短、熵低,直接当密钥用会被字典、暴力、彩虹表打穿
  • KDF 把「口令 + salt」派生成 key/iv,让同一个口令也能产出符合算法长度要求的伪随机密钥
  • salt 主要防预计算(彩虹表):同一口令配不同 salt,派生结果就不同;salt 不用保密,跟密文一起存

封装格式就是把「salt + 密文」(有时还有算法、迭代次数)按固定布局拼成文件,让解密方能复现出同一对 key/iv。

OpenSSL Salted Format

OpenSSL 的做法是:文件头放 8 字节魔数 Salted__ + 8 字节随机 salt,后面跟密文;key/iv 用 EVP_BytesToKey 从「口令 + salt」派生 —— 反复把上一块的 hash 连同口令和 salt 一起再做 hash,直到凑够 key + iv 的长度
salt 写进文件里,解密方用同一口令就能复现出同一对 key/iv

openssl enc 加密出来的文件不是裸密文,而是自带一个 16 字节的头:

1
2
3
0x00  8 字节   53 61 6c 74 65 64 5f 5f   "Salted__"
0x08 8 字节 salt(随机)
0x10 ... 真正的密文
  • 看到Salted__这 8 字节抬头就可以推断是 openssl 封装;
  • salt 的作用:同一个口令加密不同文件,输出各不相同,防彩虹表;但口令弱照样能爆破

密钥派生(openssl enc 默认走 EVP_BytesToKey,迭代 1 次):

1
2
3
4
D_1 = H(password || salt)
D_2 = H(D_1 || password || salt)
...
key || iv = (D_1 || D_2 || ...)[: key_len + iv_len]

H 在 OpenSSL 1.1.0 之后默认是 SHA-256,之前默认是 MD5

实例

rc4 +md5

1
2
3
4
5
6
7
8
9
10
from hashlib import md5
from Crypto.Cipher import ARC4
import base64

raw = base64.b64decode('U2FsdGVkX19EMQcjfEdJWJ2vm6GxFaRQ1k+R+BDKjRXzhg==')
salt, ct = raw[8:16], raw[16:]

d1 = md5(b'key' + salt).digest() # D_1
key = d1 + md5(d1 + b'key' + salt).digest() # D_1 || D_2 = 32 字节
print(ARC4.new(key).decrypt(ct)) # b'flag{rc4_is_funny}'

RC4 + SHA-256

1
2
3
4
5
6
7
8
9
10
from hashlib import sha256
from Crypto.Cipher import ARC4
import base64

raw = base64.b64decode('U2FsdGVkX1+aTyx9geO1BqGbYyVuoZxa9AehvLM4AE4DyQ==')
salt, ct = raw[8:16], raw[16:]

# SHA-256 一块就是 32 字节,正好够 32 字节 key 用,不用再拼第二块
key = sha256(b'key' + salt).digest()
print(ARC4.new(key).decrypt(ct)) # b'flag{rc4_is_funny}'

openssl enc -d -aes-256-cbc -md md5 -pass pass:admin123 -in enc.bin

1
2
3
4
5
6
7
8
9
10
11
12
from hashlib import md5
from Crypto.Cipher import AES
import base64

raw = base64.b64decode('U2FsdGVkX18+HJOxvzO7qoZxyVz5UGU771XiV2HbvqrvbETnSq2W7VKtWjV4TtPvkLO/UvUADkNSQw3nWdWVsA==')
salt, ct = raw[8:16], raw[16:]

d1 = md5(b'admin123' + salt).digest()
d2 = md5(d1 + b'admin123' + salt).digest()
d3 = md5(d2 + b'admin123' + salt).digest()
key, iv = (d1 + d2), d3 # key 32 字节,iv 紧接着取 16 字节
print(AES.new(key, AES.MODE_CBC, iv).decrypt(ct))

openssl enc -d -aes-256-cbc -in enc.bin -pass pass:admin123

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

raw = base64.b64decode('U2FsdGVkX1/CKoUTTwhLXe3giWcZWcxaFviovQYuUz9RcUP8BnQMElj3qswEIWxmHVIsR/xj6cG1kivA1VqeKg==')
salt, ct = raw[8:16], raw[16:]

d1 = sha256(b'admin123' + salt).digest() # 前 32 字节 = key
d2 = sha256(d1 + b'admin123' + salt).digest() # 紧接的 16 字节 = iv
print(AES.new(d1, AES.MODE_CBC, d2[:16]).decrypt(ct))

非对称加密

RSA

原始rsa是纯数学加密,就是直接把明文通过数学公式变成密文,而没有额外随机性或结构。
现代rsa引入填充机制,在加密前加入随机掩码,解决纯数学加密的“同明文密文固定”问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

# 1️⃣ 生成 RSA 密钥对(2048 位)
key = RSA.generate(2048)
private_key = key.export_key() #导出私钥(PEM 格式)
public_key = key.publickey().export_key() #导出公钥(PEM 格式)
# PEM 格式是 Base64编码的二进制数据,通常以特定头尾包裹,如-----BEGIN RSA PRIVATE KEY----- 开头
print("私钥:\n", private_key.decode()[:100], "...") # 只显示前100字符
print("公钥:\n", public_key.decode()[:100], "...")

# 2️⃣ 公钥加密
message = b"Hello RSA!"
pub_key = RSA.import_key(public_key)# 导入公钥字符串
cipher_encrypt = PKCS1_OAEP.new(pub_key)#使用 PKCS#1 OAEP 填充方式
encrypted_data = cipher_encrypt.encrypt(message) #生成密文为二进制数据
print("加密后:", encrypted_data.hex()[:60], "...") #转换成16进制字符串显示前60字符

# 3️⃣ 私钥解密
priv_key = RSA.import_key(private_key)
cipher_decrypt = PKCS1_OAEP.new(priv_key)
decrypted_data = cipher_decrypt.decrypt(encrypted_data)
print("解密后:", decrypted_data.decode())

哈希函数

哈希(Hash,也叫散列或摘要)是一种单向映射函数:你能从明文得到哈希,不能从哈希反推出明文。
输入任意数据 → 输出固定长度字符串(通常是十六进制)。

文件的hash,就是将文件数据作为输入,计算出hash,这时只要修改文件,文件的hash值就会改变

MD5

MD5可以将任意长度的数据 → 映射为 128 位(16 字节)的固定长度哈希值,最后呈现结果是结果32位十六进制字符

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

s = "hello"
h = hashlib.md5(s.encode()) # 哈希对象

raw = h.digest() # 16 字节
hex_str = h.hexdigest() # 32 个十六进制字符

print(len(raw)) # 16
print(len(hex_str)) # 32
print(raw) # b']A@*\xbcK*v\xb9q\x9d\x91\x10\x17\xc5\x92'
print(hex_str) # 5d41402abc4b2a76b9719d911017c592
print(hex_str == raw.hex()) # True

SHA 系列

SHA 跟MD5相比输出长,更安全

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

text = "hello world"

sha1 = hashlib.sha1(text.encode()).hexdigest() #40 个十六进制字符,20字节
sha256 = hashlib.sha256(text.encode()).hexdigest() #64 个十六进制字符,32字节
sha512 = hashlib.sha512(text.encode()).hexdigest()#128 个十六进制字符,64字节
print("SHA1:", sha1) #2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
len(sha1) #40
len(sha1.digest()) #20 , 以字节为单位
print("SHA256:", sha256) #b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
print("SHA512:", sha512) #309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f