加密算法逆向实践

加密算法逆向实践

Serendy Magician

问题

问题-1:找到开头“This program cannot be run …” 这个字符串的内存起始地址(即字符串的指针),并准确计算该字符串的长度;

问题-2:需要结合具体的加密操作逻辑,说明所发现的加密算法。

输入正确 flag 后的运行截图如下:

img

问题分析

问题1

首先找到——FOR FUN—–的位置,使用IDA Pro

Shift+F12进行字符串定位

追入

发现有一个%s夹在两行———-中间,可以判断这个输入的%s就是“This program cannot be run …”

查看main函数

发现在调用printf函数时输入了一个参数,其地址为40004Eh

==因此我们就找到了字符串“This program cannot be run …”的起始地址40004Eh==

接下来找字符串长度,此时使用x32dbg进行动态调试

因为我们已经知道了该字符串的起始地址,因此我们可以在程序运行到Entrypoint时直接定位到该字符串:

==数一数字符串长度 可知该字符串长度是43个字节==

问题2

首先发现exe文件在输入错误的flag之后直接结束或者退出了,因此我们只能直接看汇编代码对该程序进行研究

首先看提示的字符串” ———FOR FUN———-“以及”Please input your flag below:”,可以从这两个提示字符串入手研究该程序

先看main函数的整体结构:

首先调用了函数sub_402320,随后输出提示信息————–FOR FUN——————,接着连续调用了三个函数,因此可以从一个一个函数入手研究这个程序

第1个函数

发现在调用printf函数输出——-FOR FUN——–之前调用了一个函数sub_402320

追入该函数查看,利用IDA Pro的反汇编功能研究该函数:

解读这段反汇编代码:这段代码的功能是执行一个函数表中的一系列函数,并在程序退出时再次执行一个特定函数。它的基本思路是:如果全局变量dword_407080的值为0,则将其设为1,然后执行一个函数表中的一系列函数,最后调用sub_4014C0函数,该函数的参数是sub_402290函数的地址。如果dword_407080的值不是0,则直接返回它(result)。

具体来说,下面是每个步骤的解释:

  1. result = dword_407080;
    将dword_407080的值赋给result变量。

  2. if (!dword_407080)
    如果dword_407080的值为0,执行以下代码块,否则跳过该代码块。

  3. dword_407080 = 1;
    将dword_407080的值设为1。

  4. for (i = 0; dword_4033E0[i + 1]; ++i)
    查找函数表dword_4033E0中最后一个函数的索引。该函数表是一个由地址组成的数组,最后一个地址为0,以便循环能够停止。

  5. for (; i; –i)
    执行函数表dword_4033E0中的函数,从最后一个函数开始一直到第一个函数。

  6. ((void (*)(void))dword_4033E0[i])();
    将函数表dword_4033E0中第i个元素的地址强制转换为void (*)(void)类型的函数指针,然后执行该函数。

  7. return sub_4014C0((_onexit_t)sub_402290);
    调用sub_4014C0函数,并将sub_402290函数的地址转换为_onexit_t类型的函数指针作为参数。_onexit_t是一个函数指针类型,它指向一个函数,该函数在程序退出时执行。

  8. return result;
    如果dword_407080的值不为0,则直接返回该值。

这段函数的功能是调用函数表

第2个函数

追入sub_4015C0这个函数

查看其汇编代码,这个函数的功能上,首先输出了提示信息:“Please input your flag below:”

接下来函数调用了scanf函数接收我们输入的flag:

可以看到这个scanf函数是用于接收一个长度不小于9个字符的字符串输入,**byte_407444就是输入的字符串**

接下来,该函数计算了输入的字符串长度,并将其与存储在内存地址 byte_4040E4 处的值进行比较:

可以看到,如果输入的字符串长度不相等,则跳转到loc_40160F处,loc_40160F的功能是恢复函数栈寄存器并退出程序;如果相等则返回

因此我们需要知道byte_4040E4的值,追入查看byte_4040E4

可以看到4040E4的值是9,也就是说,输入的字符串长度应当是9

因此,第二个函数的功能是输出提示信息,接收输入的字符串并将输入字符串的长度与预设的长度值9进行比较,如果长度不相等则退出程序

第3个函数

回到main函数,继续研究下一个函数sub_401770

查看其反汇编函数:

可以看到,本段代码的功能是对字符串进行合法性判断

以下是代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (i = 0; (unsigned __int8)byte_4040E4 != i; ++i) // 对每个字符,循环直到字符为0x00
{
v1 = (unsigned __int8)byte_407444[i]; // 取出当前字符
v2 = 48; // v2 为 ASCII 码 48,即字符 '0'
while (v1 != v2 && byte_407444[i] != 95) // 如果当前字符不是 '0' 且不是下划线 '_'
{
if (++v2 == 58) // 如果 v2 递增后等于 ASCII 码 58,即字符 '9'+1,说明当前字符为大写字母
{
v3 = 65; // v3 为 ASCII 码 65,即字符 'A'
while (v1 != v3) // 如果当前字符不是大写字母
{
if (++v3 == 91) // 如果 v3 递增后等于 ASCII 码 91,即字符 'Z'+1,说明当前字符不合法
exit(100876); // 结束程序并返回错误码 100876
}
break;
}
}
if ((unsigned __int8)byte_4040E4 - 1 == i) // 如果遍历完字符串后还未结束循环,则结束循环
break;
}
return sub_401700(); // 调用 sub_401700() 函数并返回结果

因此,这个函数的目的是遍历一个字符串,并检查字符串中的每个字符是否是数字、大写字母或下划线,如果不是,则结束程序并返回错误码。

第3个函数调用的函数

如果字符串合法,则会跳转到sub_401700我们追入这个函数查看

查看其反汇编代码,发现这个函数就是关键的进行字符串比较的函数

这个函数的功能是将输入的字符串byte_407444a1234567890Abcd 字符串数组进行比较,如果相等,则将 dword_4040C0 数组中对应的元素设置为 v3并返回

此时我们发现,关键的数组出现了,就是这个a1234567890Abcd

追入查看:

它的值是:1234567890_ABCDEFGHIJKLMNOPQRSTUVWXYZ

第4个函数

回到main函数,继续查看第四个函数:

看到:

发现这里对字符串进行了仿射加密:对输入的明文字符串利用a1234567890Abcd的字符集1234567890_ABCDEFGHIJKLMNOPQRSTUVWXYZ进行仿射加密

其中仿射加密的密钥a=v6=9, b=v1=3

第4个函数调用的函数

随后调用了一个函数sub_40210C,追入查看,并定位到关键代码段:

这段代码中:

将字符串合并为了一个长度为32字符长度的定长字符串,因此我们可以判断这个函数是一个哈希算法,用以输出定长的256位散列值

随后定位到前面的关键函数sub_401A14,推测这个函数就是用来进行哈希的函数,追入查看:

可以判断这是一个MD5加密算法!!

因此函数sub_40210C的功能是将Block 变量分成多个32 字节的块,然后每个块都通过 sub_401A14 函数计算出其哈希值。最后,将所有块的哈希值合并成一个 128 位的哈希值,并将其转换为字符串格式写入 Buffer 变量中。

回到函数sub_40161C,接下来函数进行了一个字符串比较:

Str1是sub_40120C加密过后得到的散列值,因此判断Str2就是所比较的密文(散列值),追入查看:

Str2的值是:f8728f24e01c1aaf54e23f7f0d591384

如果比较成功,即输入的值加密后和预设的密文散列值相同,进入下一个比较,如果byte_407449的值为32Z2,则比较成功,跳出成功得到flag的消息框

我们发现byte_407449和输入字符串的byte_407444相差5,因此v4的strcmp**407449所比较的字符串就是输入字符串的后4个字节,因此我们可以知道flag的后四个字节是32Z2**

解密思路

在网上找了个MD5解密轮子(Link:https://www.somd5.com/),解密MD5得到明文:`K502G`

然后我们就可以奖字符串进行拼接进行仿射解密了:K502G32Z2

仿射解密脚本如下(Python):

其中注意,由于解密是逆变换,所以密钥a=3, b=9

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
def main():
c = input('输入密文:')
a = 3 #根据逆向结果得到的密钥
b = 9

p = affine_decrypt(c, a, b)
print("解密结果:" + p)


def affine_decrypt(ciphertext, a, b):
plaintext = ""
m = len("1234567890_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
a_inv = 0
# 计算 a 的逆元素
for i in range(1, m):
if (i * a) % m == 1:
a_inv = i
break
# 对密文进行解密
for char in ciphertext:
if char == '_':
char = ' '
elif char == '-':
char = '.'
# 将字符映射到数字
num = "1234567890_ABCDEFGHIJKLMNOPQRSTUVWXYZ".index(char)
# 计算解密后的数字
num = (a_inv * (num - b)) % m
# 将数字映射回字符
plaintext += "1234567890_ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num]
return plaintext

if __name__ == '__main__':
main()

然后把这个字符串拼接起来得到flag:5M1LE_LOL

输入这个flag,得到成功的提示:

第一次尝试:

因此可以得到flag:3WC9H32Z2–>它不对…. o(TヘTo)

难道是md5解错了?还是仿射回去的时候错了?

笑死 原来是仿射解密的密钥搞反了 解密是逆变换 密钥要反过来

而且32Z2也要一起丢进去仿射解密

  • Title: 加密算法逆向实践
  • Author: Serendy
  • Created at : 2023-05-02 00:02:48
  • Updated at : 2023-05-11 13:27:58
  • Link: https://mapleqian.github.io/2023/05/02/加密算法逆向实践/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments