简介
Hash 类算法是一种摘要算法,摘要结果是不可逆的。所以一般在逆向中我们通常碰到 Hash 算法要通过它给出的一些信息来进行碰撞爆破。
下面我们首先了解一下常见的 Hash 算法。
算法特征
MD5
MD5(Message-Digest Algorithm 5)是信息学中使用广泛的哈希算法
这个算法具有很多性质:
-
压缩性:对于任意长度的输入,输出长度总是相同的
-
抗修改性:对原数据的一点点修改都会导致最终结果的最大变化。
-
抗碰撞性:已知原数据和 MD5 值很难生成与原数据不同但 MD5 值相同的数据。
可以理解为:生成任意一段数据的 "数字指纹",对文件或数据的微小改动都会之间导致数字指纹的巨大变化。
Hash 算法常见一般有两种形式的调用:
- 封装成函数
uint8_t digest[16];
uint8_t input[]="xxxxxx";
MD5_hash(input,sizeof(input)-1,digest);
- 分三步完成哈希
MD5_CTX ctx;
uint8_t digest[16];
uint8_t input[]="xxxxxx";
MD5_Init(&ctx);
MD5_Update(&ctx,input,sizeof(input)-1);
MD5_Final(&ctx,digest);
在openssl源码的函数实现中,可以在靠近开头的位置看到四个固定常量。
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0X10325476
识别特征:
-
四个固定的常量(初始化向量)
-
不论输入是多长,输出永远是16字节
-
符合上述两种调用方式之一
SHA1
SHA1 算法与 MD5 类似,只不过 SHA1 有五个常量:
H0 = 0x67452301
H1 = 0xEFCDAB89
H2 = 0X98BADCFE
H3 = 0X10325476
H4 = 0XC3D2E1F0
识别特征:
-
五个固定的常量(初始化向量)
-
不论输入是多长,输出永远是 20 字节
-
符合上述两种调用方式之一
SHA256
SHA256 则有八个常量:
H0 = 0x67452301
H1 = 0xEFCDAB89
H2 = 0x98BADCFE
H3 = 0x10325476
H4 = 0xC3D2E1F0
H5 = 0x9b05688c
H6 = 0x1f83d9ab
H7 = 0x5be0cd19
识别特征:
-
八个固定的常量(初始化向量)
-
不论输入是多长,输出永远是32字节
-
符合上述两种调用方式之一
逆向方式
算法分析
通过 ida 的 findcrypt 插件我们可以直接查找到程序中的 Hash 常量来判断加密算法,但是这通常只对自己实现或静态链接openssl库才有作用。如果使用openssl链接库编译动态链接,那么 findcrypt 是找不到的。

也不能说找不到,当动态调试程序的时候使用 findcrypt 插件进行查找可以找到整个openssl链接库的常量。这样我们根本无法分析程序是哪个加密算法。
在使用动态链接的情况下,通常可以通过 ida 函数窗口的函数分析出加密算法。
看到下例中的md5字样,我们可以判断程序为md5加密算法。
网站查询
通过在线网站数据库查询并 "解密" Hash值。
脚本爆破
通过编写脚本进行碰撞爆破。
通用脚本:
import string
import hashlib
import itertools
#设置目标哈希值
target = bytes.fromhex("")
#设置字符集(可以根据需要修改,以下为大小写字母和数字)
charset = (string.ascii_letters + string.digits).encode()
#根据选择的哈希算法创建哈希对象
hash_type = 'sha256'
if hash_type == 'md5':
hash_func = hashlib.md5
elif hash_type == 'sha1':
hash_func = hashlib.sha1
elif hash_type == 'sha256':
hash_func = hashlib.sha256
else:
print("Unsupported hash type")
exit()
for i in itertools.product(charset, repeat=6): #这里的6是字符的长度,可以修改为任何你想要的长度
guess = bytes(i)
hashed = hash_func(guess).digest()
if hashed == target:
print(f"Found match: {guess.decode()}")
break
hashcat
使用 hashcat 这个工具进行碰撞爆破。
常用参数:
-
-m 指定哈希类型
-
-a 指定破解模式
-
-V 查看版本信息
-
-o 将输出结果储存到指定文件
-
--force 忽略警告
-
--show 仅显示破解的 hash 密码和对应的明文
-
--remove 从源文件中删除破解成功的 hash
-
--username 忽略 hash表 中的用户名
-
-b 测试计算机破解速度和相关硬件信息
-
-O 限制密码长度
-
-T 设置线程数
-
-r 使用规则文件
-
-1 自定义字符集 -1 0123asd ?1={0123asd}
-
-2 自定义字符集 -2 0123asd ?2={0123asd}
-
-3 自定义字符集 -3 0123asd ?3={0123asd}
-
-i 启用增量破解模式
-
--increment-min 设置密码最小长度
-
--increment-max 设置密码最大长度
破解模式介绍:
-
0 straight 字典破解
-
1 combination 将字典中密码进行组合(1 2>11 22 12 21)
-
3 brute-force 使用指定掩码破解
-
6 Hybrid Wordlist + Mask 字典+掩码破解
-
7 Hybrid Mask + Wordlist 掩码+字典破解
集成的字符集:
-
?l 代表小写字母
-
?u 代表大写字母
-
?d 代表数字
-
?s 代表特殊字符
-
?a 代表大小写字母、数字以及特殊字符
-
?b 0x00-0xff
例题
[SUCTF 2018 招新赛]hash
- 分析
ida 反编译分析。看到了主函数中的 flag 前缀,下面使用 findcrypt 分析算法。

使用 findcrypt 后我们搜索到了程序中的 MD5 常量,可以判断程序使用的是 MD5 算法。

结合上面信息分析程序代码逻辑。
程序要求输入字符的前 6 个字符为SUCTF{,最后一个字符为}。sub_4011A0函数使用了 MD5 的常量,应该是加密函数。最后将结果存到v18,因为下面的代码疑似以两位十六进制打印了v18变量,这可能是存在函数返回值的变量。
下面有一个用于判断的 MD5 值,与其判断的Buffer在与其判断之前。将符合i&1==0条件的字符跟 1 进行了异或,i&1即结果是偶数位的字符,所以我们爆破它之前需要跟 1 再次进行异或。

写出逆向脚本:
md5="bf772f6ed89838b9gb9f7abf3cc09413"
md5=list(md5)
for i in range(len(md5)):
if i&1 == 0:
md5[i]=chr(ord(md5[i])^1)
result="".join(md5)
print(result)
#cf673f7ee88828c9fb8f6acf2cb08403
然后我们可以先在 md5 查询网站查询。

查询结果为:birthday。
接下来我们继续逆向剩下的代码程序。
将加密后的buffer与 md5 密文进行比较,如果相等则执行第二个if语句部分的代码。
下面我们跟进分析sub_401A90函数。

分析函数代码,首先将取输入的长度。然后以输入长度为界限进行迭代处理。将每个字符根据下标进行i*2+1偏移。i就是字符串的下标。

根据代码逻辑,对比较的字符串进行逆向处理。
enc="`ut9t;"
result=""
for i in range(len(enc)):
n=i*2+1
result+=chr(ord(enc[i])-n)
print(result)
#_ro2k0
将第一段 flag 和 第二段 flag 拼接起来得:SUCTF{birthday_ro2k0}
2024极客大挑战 blasting_master
- 分析
分析主函数代码,发现读取了输入并去除换行符。然后由byte_42DE作为标志决定是否执行sub_1464函数,函数参数为我们输入的内容。然后再对指定大小内存进行比较检查对错。
根据以上逻辑我们可以判断程序默认是执行sub_1464函数的,这个函数极可能就是加密函数。而unk_2020以及unk_4060可能就是存储密文的变量以及存储加密结果的变量。dword_4040很明显就是比较长度了。
我们先根据代码逻辑进行函数及变量重命名方便理解。

重命名后:
接下来我们首先使用 findcrypt 进行算法特征查找,但是最后 findcrypt 并没有找到算法特征。

但是我们在函数窗口发现了算法特征,可以看到动态链接函数中的 MD5 字样。我们可以由此判断encrypt加密算法是 MD5 算法。

进入encrypt函数分析,可以看到代码逻辑非常乱。
我们先根据代码逻辑修改变量和函数类型。

修改后的:

这里v3变量获取了我们的输入,之后将v3作为参数传入了sub_13E2参数,并且将dec数组作为第二个参数传入函数。
这里根据前面的逻辑判断,sub_13E2大概就是封装的 MD5 加密函数。dec是存放结果的数组。
下面的逻辑就是对加密结果进行混淆的逻辑了。
跟进sub_13E2函数查看,验证我们的判断是正确的。

我们接下来分析函数中对加密结果进行混淆的逻辑。

以上代码循环执行了十六次,即混淆了十六次。而混淆的数据以及存储的位置每次又都是不同的。我们可以判断这里我们的 md5 加密同样进行了十六次,而我们的 flag 前缀确定的是SYS{,所以我们可以将密文数据分为 16 组进行爆破。
- exp
import hashlib
cipher = [0xB2, 0x50, 0xA0, 0xBC, 0x3A, 0x7F, 0x54, 0x6D, 0x96, 0x07, 0x0F, 0x71, 0x9A, 0x72, 0xEB, 0xA5, 0xA0, 0xB5, 0x71, 0xA4, 0x6A, 0xB8, 0xBA, 0xFA, 0xE4, 0x31, 0xC3, 0x71, 0x54, 0x29, 0xA7, 0x59, 0x20, 0x2B, 0x13, 0x21, 0xBD, 0x67, 0x5F, 0x8D, 0x65, 0x3A, 0x02, 0x27, 0x08, 0x4F, 0x92, 0x9C, 0xB5, 0x7C, 0xDF, 0x69, 0x34, 0xB8, 0x82, 0x2D, 0xF6, 0xCA, 0x7A, 0x65, 0x98, 0x63, 0xDC, 0x51, 0x2A, 0x34, 0x97, 0x4F, 0xF8, 0xBC, 0x23, 0x1F, 0x38, 0xA8, 0xA6, 0x2F, 0xA9, 0x0D, 0x64, 0x4C, 0xAC, 0x2F, 0xF9, 0xF5, 0x2D, 0xB1, 0x91, 0xA8, 0xD5, 0x76, 0xD9, 0x2D, 0xC6, 0xAC, 0x2E, 0x69, 0x32, 0xD5, 0x64, 0x1D, 0xC1, 0x3C, 0xEC, 0xF5, 0x2C, 0x90, 0xED, 0xF4, 0x17, 0x8B, 0x55, 0x4C, 0xE4, 0x6C, 0x3B, 0xB3, 0xDA, 0x29, 0xC0, 0x7B, 0x39, 0xDF, 0x92, 0x73, 0xFC, 0xC9, 0xC2, 0xA8, 0x68, 0x11, 0x22, 0x2B, 0x64, 0x3F, 0x12, 0x9B, 0x95, 0x73, 0x2A, 0x05, 0xD3, 0x3F, 0x2E, 0x33, 0xF1, 0x85, 0xED, 0x07, 0x7B, 0x86, 0x8F, 0x62, 0x2D, 0x79, 0x03, 0xAC, 0x80, 0xCE, 0xF5, 0xB2, 0xA0, 0x0C, 0xF7, 0xE1, 0xC5, 0x0E, 0x63, 0x27, 0xD1, 0x65, 0x23, 0xEA, 0x5A, 0x1C, 0x02, 0x0B, 0x32, 0xBA, 0x1F, 0xE5, 0xC7, 0x22, 0xA5, 0x66, 0x77, 0xEA, 0x5B, 0xE4, 0x64, 0xAB, 0x8B, 0x60, 0xB6, 0xDF, 0x00, 0xDC, 0xF7, 0x6D, 0x93, 0xEC, 0x2F, 0x2F, 0x68, 0x07, 0x50, 0xE0, 0xD1, 0x1A, 0x3F, 0xC6, 0x4E, 0x2E, 0xC6, 0xBB, 0xAE, 0x08, 0x40, 0xD8, 0x5B, 0x11, 0xB5, 0xDC, 0x15, 0x35, 0x7F, 0x63, 0x49, 0x3E, 0x5B, 0x9C, 0x0D, 0xFC, 0x0D, 0xB6, 0x80, 0xB7, 0x2B, 0x00, 0xEF, 0x3C, 0x0C, 0x2F, 0xEB, 0x86, 0x44, 0x57, 0x74, 0x9E, 0x5F, 0x1F, 0x8B, 0xA1, 0xC9, 0x01, 0xF1, 0xD8, 0xF4, 0x92, 0x82, 0x95, 0x6F, 0x85, 0xD2, 0x15, 0x22, 0x1F, 0xF0, 0x9F, 0xD1, 0xAB, 0x51, 0x39, 0x9A, 0xB6, 0xC4, 0xDA, 0xFB, 0x38, 0x8D, 0xE6, 0x8C, 0x57, 0x19, 0x5E, 0x94, 0xDA, 0x57, 0xCC, 0xF0, 0xB9, 0x0A, 0x4A, 0x17, 0x82, 0xFC, 0xC5, 0x4F, 0x4B, 0x5A, 0xA5, 0xF4, 0xE5, 0x3E, 0xFA, 0x3A, 0x0A, 0xF4, 0xB4, 0x8E, 0x7F, 0x25, 0x84, 0x75, 0x90, 0xCD, 0x35, 0x87, 0xEB, 0xC3, 0xCE, 0x81, 0x2B, 0x86, 0xC9, 0x16, 0x7E, 0x85, 0x68, 0x2D, 0xF1, 0xDB, 0x8E, 0x74, 0x15, 0xCF, 0x95, 0x51, 0x07, 0x88, 0x5E, 0x1B, 0xE9, 0x37, 0xC9, 0x5B, 0xBA, 0x61, 0xEB, 0x9F, 0x7B, 0xE4, 0x89, 0x10, 0xF0, 0x6E, 0xCD, 0x75, 0x71, 0xAD, 0x09, 0x74, 0x58, 0x49, 0xA3, 0xF5, 0x33, 0x83, 0x75, 0x22, 0x95, 0x1B, 0xE3, 0x3C, 0x48, 0x05, 0x5C, 0xAD, 0xA8, 0x6B, 0xFD, 0x41, 0xEB, 0xAF, 0xC6, 0x02, 0x28, 0xC6, 0x5E, 0xCF, 0x36, 0xAE, 0x50, 0xCE, 0x93, 0xF2, 0x70, 0x88, 0x9D, 0x3F, 0x4A, 0x9F, 0x86, 0xE7, 0x67, 0x64, 0xB0, 0x02, 0x96, 0x0C, 0xAB, 0x9F, 0xEB, 0x4B, 0x03, 0x44, 0x92, 0xDE, 0x6C, 0xF4, 0xCE, 0x32, 0x4F, 0x4F, 0x38, 0xE2, 0x52, 0x59, 0xCA, 0x95, 0x4A, 0x11, 0xD8, 0x30, 0xA2, 0x7B, 0xD5, 0x3A, 0xE6, 0x11, 0xDA, 0x3A, 0x4A, 0x33, 0x61, 0x39, 0x65, 0x26, 0xD2, 0x78, 0xBC, 0xED, 0xBD, 0xA5, 0x8B, 0x2B, 0x87, 0x4C, 0x95, 0x47, 0x25, 0x02, 0xBA, 0x83, 0x3D, 0xDC, 0xE4, 0x6A, 0xAD, 0x67, 0xDD, 0x22, 0xB1, 0xBD, 0x2B, 0x7C, 0x53, 0x11, 0x3C, 0xD9, 0x23, 0x06, 0x3D, 0x20, 0xBA, 0x28, 0xC8, 0x2D, 0x89, 0x51, 0x57, 0x63, 0x82, 0xA0, 0xC8, 0xA8, 0xDE, 0x29, 0x61, 0xC1, 0x53, 0x51, 0xB0, 0xBC, 0x37, 0x04, 0xEE, 0xC9, 0x35, 0x8A, 0xA8, 0xA2, 0x66, 0xBA, 0x6F, 0x24, 0xB6, 0x3F, 0x62, 0x41, 0x6D, 0x10, 0x46, 0xCB, 0x06, 0x12, 0x39, 0xD9, 0x0E, 0xF9, 0xDC, 0x19, 0xA7, 0x65, 0xB8, 0xC0, 0x40, 0xBE, 0xF6, 0x99, 0x9A, 0xAF, 0x02, 0x16, 0x37, 0x4D, 0xA5, 0x75, 0x4C, 0x42, 0x4B, 0x1A, 0xF0, 0x52, 0xDA, 0x38, 0xF3, 0x6B, 0xA9, 0x1A, 0xDC, 0xFA, 0x80, 0xB0, 0x60, 0xB1, 0xFD, 0x73, 0x7B, 0x78, 0xD9, 0x62, 0x83, 0x26, 0xBF, 0x16, 0x33, 0x71, 0x79, 0x6F, 0x11, 0x2F, 0xE9, 0xA7, 0xBB, 0x46, 0x46, 0xD6, 0x8F, 0xF6, 0x21, 0x7E, 0xFC, 0x68, 0x12, 0x86, 0x6B, 0xFC, 0x51, 0xC9, 0x70, 0x7A, 0x74, 0xBC, 0x8F, 0x6E, 0x0B, 0x86, 0x42, 0x6F, 0x5C, 0xFD, 0xF7, 0x4E, 0x27, 0x71, 0xFE, 0x37, 0xE6, 0xC8, 0x62, 0x47, 0xFC, 0xD5, 0x6C, 0xBA, 0x5C, 0xD9, 0x29, 0x5A, 0x73, 0xAE, 0xC3, 0x8F, 0xF0, 0x46, 0x95, 0x32, 0x42, 0x2D, 0xD0]
md5_list = lambda input_str: list(hashlib.md5(input_str.encode()).digest())
cip = [cipher[16 * i: 16 * (i + 1)] for i in range(40)]
data1 = 'S' # 初始化 data1 为 'S'
data2 = 'Y' # 初始化 data2 为 'Y'
data3 = 'C' # 初始化 data3 为 'C'
print("SYC", end='') # 打印 "SYC"
for i in range(40):
# x 从 32 到 127,表示所有字符的 ASCII 码
for x in range(32, 128):
input_str = data1 + data2 + data3 + chr(x)
md5_lis = md5_list(input_str)
# 检查 cip[i][j] 是否符合解密条件
if all(cip[i][j] == ((md5_lis[j] ^ (j + 42)) * 7 + (j % 15) * 82) % 256 for j in range(16)):
# 如果所有字节都匹配,打印字符 x,并更新 data1, data2 和 data3
print(chr(x), end='')
data1 = data2
data2 = data3
data3 = chr(x)
if data3 == '}':
exit(0)
