算法还原之base64
基础概念
加密与解密的关系
- 加密:是将原始的、可读的信息(明文)转换为另一种形式(密文)的过程,这样就可以防止未授权的访问。加密算法和密钥用于这一转换过程。加密的目的是确保信息的机密性,即使密文在传输或存储过程中被截获,没有正确的密钥也无法理解其内容。
- 解密:是加密的逆过程,它将密文转换回原始的明文。只有拥有正确密钥的人才能解密信息并获取原始内容。
加密和解密的关系可以概括为以下几点:
- 互为逆过程:加密和解密使用相同类型的算法,但执行相反的操作。加密算法设计时必须确保可以逆向操作,以便解密。
- 密钥的使用:加密和解密通常依赖于密钥。在加密过程中使用密钥生成密文,在解密过程中使用相同的密钥从密文恢复明文。密钥的安全管理是加密系统能否有效工作的关键。
- 安全性:加密系统的设计目标是确保即使攻击者知道加密算法,也无法在没有密钥的情况下解密信息。因此,加密算法通常是公开的,而密钥则必须保密。
- 应用场景:加密和解密广泛应用于多种场景,如网络安全中的数据传输、数据存储、数字签名等,以确保信息的保密性、完整性和可用性。
在现代密码学中,加密和解密算法多种多样,包括对称加密(如AES)、非对称加密(如RSA)、散列函数等,它们在不同的应用场景中提供了不同级别的安全性。
加密的简单分类
加密技术可以分为几个基本类别,每个类别都有其特定的用途和特点。以下是一些简单的分类:
对称加密(Symmetric Encryption):
- 使用相同的密钥进行加密和解密。
速度快,适用于大量数据的加密。
典型的算法包括AES(Advanced Encryption Standard)、DES(Data Encryption Standard)、3DES(Triple Data Encryption Algorithm)等。
非对称加密(Asymmetric Encryption):
- 使用一对密钥:公钥和私钥。公钥用于加密,私钥用于解密。
安全性高,但速度较慢,适用于小量数据的加密。
典型的算法包括RSA、ECC(Elliptic Curve Cryptography)、Diffie-Hellman等。
哈希函数(Hash Functions):
- 将输入(无论大小)映射到一个固定长度的哈希值。
用于确保数据的完整性,因为即使输入数据有很小的变化,哈希值也会发生显著变化。
不是加密算法,因为哈希是不可逆的,不能用于解密。
典型的算法包括MD5、SHA-1、SHA-256等。
实现标准的base64
1. base64原理
Base64编码的原理是将每三个字节的数据(共24位)划分为四个6位的段,然后每个6位的段转换为一个对应的可打印字符。Base64编码表包含64个字符:大写字母A到Z、小写字母a到z、数字0到9、加号(+)和斜杠(/)。
下面是一个Python的代码示例,展示了如何实现标准的base64编码和解码
import base64 |
注意,Base64编码并不是一种加密方法,它不提供数据的安全性,只是将数据转换成一种适合在不同系统间传输的格式
2. 示例说明
假设要对字符串 “Man” 进行base64编码,整个过程如下所示:
第一步:“M”、“a”、”n”对应的ASCII码值分别为77,97,110,对应的二进制值是01001101、01100001、01101110。如图第二三行所示,由此组成一个24位的二进制字符串。
第二步:如图红色框,将24位每6位二进制位一组分成四组。
第三步:在上面每一组前面补两个0,扩展成32个二进制位,此时变为四个字节:00010011、00010110、00000101、00101110。分别对应的值(Base64编码索引)为:19、22、5、46。
第四步:用上面的值在Base64编码表中进行查找,分别对应:T、W、F、u。因此“Man”Base64编码之后就变为:TWFu。
位数不足情况
上面是按照三个字节来举例说明的,如果字节数不足三个,那么该如何处理?
两个字节:两个字节共16个二进制位,依旧按照规则进行分组。此时总共16个二进制位,每6个一组,则第三组缺少2位,用0补齐,得到三个Base64编码,第四组完全没有数据则用 “=” 补上。因此,上图中“BC”转换之后为“QKM=”;
一个字节:一个字节共8个二进制位,依旧按照规则进行分组。此时共8个二进制位,每6个一组,则第二组缺少4位,用0补齐,得到两个Base64编码,而后面两组没有对应数据,都用“=”补上。因此,上图中“A”转换之后为“QQ==”;
3. base64实现
|
代码介绍:
由于base64编码是将编码前的3*8位数据,分解成4个6位的数据,所以经过base64编码后的字符串长度是4的倍数。
但往往进行编码的数据长度并不是3的倍数,这就造成了“编码”后的位数不为4的倍数,比如 “Brisk” 共5×8=40位,以6位为一组可以分为7组,这样“编码”后就有7个字符,但base64编码后的字符长度应该是4的倍数,显然这里就出问题了,那么怎么办呢?前面的不可以抛弃掉,所以就只有“追加”了,所以Brisk经过base64编码后的长度应该是8个字符,而第8个编码后的字符是’=’,再比如对单个字符a进行base64编码,由于它的长度不是3的倍数,以3个字节为一组它只能分一组,再以6位为一位它只能分两组,所以经过“编码”后它的长度是2,但base64编码后的个数应该是4的倍数,所以它的长度应该是4,所以在后面补上两个‘=’,由于一个数求余3后有三个不同的结果,0、1、2,所以在对一个数据进行base64进行编码后它的长度为:
(1)当进行编码的数据长度是3的倍数时,len=strlen(str_in)/3*4;
(2)当进行编码的数据长度不是3的倍数时,len=(strlen(str_in)/3+1)*4;
上面的代码实现分为了两个部分:
第一部分:输入数据长度是3的倍数,按照base64的原理对数据进行移位操作,转换承对应的base64编码
第二部分:如果不是3的倍数,也就意味(len % 3 == 1)或者(len % 3 == 2),则需要在后面补充对应的 “=”
base64逆向识别—IDA
1. 简介
将前面写好的代码打包成apk后,解压,拿到里面的so文件,利用IDA进行分析:
1、将so文件拖入到IDA中
2、找到我们定义好的导出函数 base64_enc。
可以看到从java层到so层,函数的命名会发生一定变化,在so层叫做 “Java_com_roysue_base64_MainActivity_base64_1enc”(Java_包名_函数名)
3、找到了具体的base64的实现流程:base64_enc(s, v4, (char *)v8);
- s:传入的字符串data
- v4:字符串长度
- (char*)v8:保存输出内容的变量
用了“Z10base64_encPciS(a1, a2, a3);”函数
2. base64_enc
char *__fastcall base64_enc(char *data, int len, char *out) |
1、查看代码,可以看到byte_16FC4参数,对应内容是一个标准的base64编码表
到这块就可以大胆猜测一下,当前算法为base64了,因为base64的参数因子是该编码的一个特征。
哪怕有的base64编码做了魔改,其实本质也大都是修改参数因子的内容!
2、对比上面写的C代码,是将switch转换成了一个多层的if判断
剩下的就不具体分析了,略略略~
frida so入门
上面的so代码是没有做符号抽取的,所以可以看到对应的函数名字。要是做了符号抽取,函数名是不可被直接识别的。
所以,当判断一个so层的函数可能实现了特定功能的时候,可以调用frida来实现主动调用
function base64_enc(data) { |
1、查找函数基址
var base = Module.findBaseAddress('libnative-lib.so'); |
通过Module.findBaseAddress获取so文件的基址,再查找到函数的偏移地址。
然后还需要判断当前指令集是thumb的还是arm的。判断步骤如下:
(1)Option->Gereral->设置字节大小为4
(2)查看函数中的地址,如果全部为4字节的话,为arm指令集;如果含有2个字节,则为thumb指令集,thumb指令集在查找函数基址时要+1
2、最终执行结果如下: