基础概念

加密与解密的关系

  1. 加密:是将原始的、可读的信息(明文)转换为另一种形式(密文)的过程,这样就可以防止未授权的访问。加密算法和密钥用于这一转换过程。加密的目的是确保信息的机密性,即使密文在传输或存储过程中被截获,没有正确的密钥也无法理解其内容。
  2. 解密:是加密的逆过程,它将密文转换回原始的明文。只有拥有正确密钥的人才能解密信息并获取原始内容。

加密和解密的关系可以概括为以下几点:

  • 互为逆过程:加密和解密使用相同类型的算法,但执行相反的操作。加密算法设计时必须确保可以逆向操作,以便解密。
  • 密钥的使用:加密和解密通常依赖于密钥。在加密过程中使用密钥生成密文,在解密过程中使用相同的密钥从密文恢复明文。密钥的安全管理是加密系统能否有效工作的关键。
  • 安全性:加密系统的设计目标是确保即使攻击者知道加密算法,也无法在没有密钥的情况下解密信息。因此,加密算法通常是公开的,而密钥则必须保密。
  • 应用场景:加密和解密广泛应用于多种场景,如网络安全中的数据传输、数据存储、数字签名等,以确保信息的保密性、完整性和可用性。

在现代密码学中,加密和解密算法多种多样,包括对称加密(如AES)、非对称加密(如RSA)、散列函数等,它们在不同的应用场景中提供了不同级别的安全性。

加密的简单分类

加密技术可以分为几个基本类别,每个类别都有其特定的用途和特点。以下是一些简单的分类:

  1. 对称加密(Symmetric Encryption)

    • 使用相同的密钥进行加密和解密。
    • 速度快,适用于大量数据的加密。

    • 典型的算法包括AES(Advanced Encryption Standard)、DES(Data Encryption Standard)、3DES(Triple Data Encryption Algorithm)等。

  2. 非对称加密(Asymmetric Encryption)

    • 使用一对密钥:公钥和私钥。公钥用于加密,私钥用于解密。
    • 安全性高,但速度较慢,适用于小量数据的加密。

    • 典型的算法包括RSA、ECC(Elliptic Curve Cryptography)、Diffie-Hellman等。

  3. 哈希函数(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

# 编码
def encode_base64(data):
return base64.b64encode(data)

# 解码
def decode_base64(encoded_data):
return base64.b64decode(encoded_data)

# 示例
original_data = b'Hello, World!'
encoded_data = encode_base64(original_data)
decoded_data = decode_base64(encoded_data)

print(f"Original data: {original_data}")
print(f"Encoded data: {encoded_data}")
print(f"Decoded data: {decoded_data}")

注意,Base64编码并不是一种加密方法,它不提供数据的安全性,只是将数据转换成一种适合在不同系统间传输的格式

2. 示例说明

假设要对字符串 “Man” 进行base64编码,整个过程如下所示:

img

第一步:“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。

位数不足情况

上面是按照三个字节来举例说明的,如果字节数不足三个,那么该如何处理?

img

两个字节:两个字节共16个二进制位,依旧按照规则进行分组。此时总共16个二进制位,每6个一组,则第三组缺少2位,用0补齐,得到三个Base64编码,第四组完全没有数据则用 “=” 补上。因此,上图中“BC”转换之后为“QKM=”;

一个字节:一个字节共8个二进制位,依旧按照规则进行分组。此时共8个二进制位,每6个一组,则第二组缺少4位,用0补齐,得到两个Base64编码,而后面两组没有对应数据,都用“=”补上。因此,上图中“A”转换之后为“QQ==”;

3. base64实现

image.png

#include <jni.h>
#include <string>

//定义base64的种子
static const char base64en[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};

//base64加密函数
void base64_enc(char *data, int len, char *out){
int index = 0; //定义out的索引
char last_c = 0;
char c = 0;
for (int i = 0; i < len; ++i) {
c = data[i];
switch (i % 3) {
case 0:
out[index++] = base64en[(c >> 2) & 0x3f ];
break;
case 1:
out[index++] = base64en[((last_c & 0x3) << 4) | ((c >> 4) & 0xf )];
break;
case 2:
out[index++] = base64en[( ( last_c & 0xf ) << 2 ) | ((c >> 6 ) & 0x3)];
out[index++] = base64en[c & 0x3f];
break;

}
last_c = c;
}
if(len % 3 == 1){
out[index++] = base64en[(c & 0x3) << 4];
out[index++] = '=';
out[index++] = '=';
} else if (len % 3 == 2){
out[index++] = base64en[( c & 0xf ) << 2];
out[index++] = '=';
}
}


extern "C"
JNIEXPORT jstring JNICALL
//可以看到有三个参数:
//①JNI *env表示java环境;
//②jobject thiz这块对应函数名,如果是static静态方法,这块参数将为jclass claz。非static方法则为jobject thiz
//③传入真正的参数
Java_com_roysue_base64_MainActivity_base64_1enc(JNIEnv *env, jobject thiz, jstring data) {
// TODO: implement base64_enc()
char *c_data = const_cast<char *>(env->GetStringUTFChars(data, 0)); //将java层的data参数传入到C中,并进行类型强转
int len = strlen(c_data); //定义c_data的长度
char out[100] = {0};
base64_enc(c_data,len,out); //调用base64_enc进行base64编码
env->ReleaseStringUTFChars(data,c_data); //进行内存的释放,将前面定义的变量进行释放
return env->NewStringUTF(out); //得到加密后的值

}

代码介绍:

由于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进行分析:

image.png

image.png

1、将so文件拖入到IDA中

image.png

2、找到我们定义好的导出函数 base64_enc。

可以看到从java层到so层,函数的命名会发生一定变化,在so层叫做 “Java_com_roysue_base64_MainActivity_base64_1enc”(Java_包名_函数名)

image.png

image.png

3、找到了具体的base64的实现流程:base64_enc(s, v4, (char *)v8);

  • s:传入的字符串data
  • v4:字符串长度
  • (char*)v8:保存输出内容的变量

img

用了“Z10base64_encPciS(a1, a2, a3);”函数

image.png

2. base64_enc

char *__fastcall base64_enc(char *data, int len, char *out)
{
int v3; // r2
int v4; // r2
int v5; // r2
char *result; // r0
int v7; // [sp+0h] [bp-1Ch]
int i; // [sp+4h] [bp-18h]
unsigned __int8 v9; // [sp+Ah] [bp-12h]
unsigned __int8 v10; // [sp+Bh] [bp-11h]
int v11; // [sp+Ch] [bp-10h]

v11 = 0;
v10 = 0;
v9 = 0;
for ( i = 0; i < len; ++i )
{
v9 = data[i];
v7 = i % 3;
if ( i % 3 )
{
if ( v7 == 1 )
{
v4 = v11++;
out[v4] = byte_16FC4[(16 * (v10 & 3)) | (v9 >> 4)];
}
else if ( v7 == 2 )
{
out[v11] = byte_16FC4[(4 * (v10 & 0xF)) | (v9 >> 6)];
v5 = v11 + 1;
v11 += 2;
out[v5] = byte_16FC4[v9 & 0x3F];
}
}
else
{
v3 = v11++;
out[v3] = byte_16FC4[v9 >> 2];
}
v10 = v9;
}
if ( len % 3 == 1 )
{
out[v11] = byte_16FC4[16 * (v9 & 3)];
out[v11 + 1] = 61;
result = out;
out[v11 + 2] = 61;
}
else
{
result = (char *)(len % 3);
if ( len % 3 == 2 )
{
out[v11] = byte_16FC4[4 * (v9 & 0xF)];
result = out;
out[v11 + 1] = 61;
}
}
return result;
}

1、查看代码,可以看到byte_16FC4参数,对应内容是一个标准的base64编码表

image.png

image.png

到这块就可以大胆猜测一下,当前算法为base64了,因为base64的参数因子是该编码的一个特征。

哪怕有的base64编码做了魔改,其实本质也大都是修改参数因子的内容!

2、对比上面写的C代码,是将switch转换成了一个多层的if判断

image.png

剩下的就不具体分析了,略略略~

frida so入门

上面的so代码是没有做符号抽取的,所以可以看到对应的函数名字。要是做了符号抽取,函数名是不可被直接识别的。

所以,当判断一个so层的函数可能实现了特定功能的时候,可以调用frida来实现主动调用

function base64_enc(data) {
var base = Module.findBaseAddress('libnative-lib.so');
var func_addr = base.add(0x8E85) ; //0x8E84是函数的偏移地址,+1是为了跳过Thumb指令
var func = new NativeFunction(func_addr,'pointer',['pointer','int','pointer']) //声明函数,第一个参数是函数地址,第二个参数是函数返回值类型,第三个参数是函数参数类型
var arg1 = Memory.alloc(100) //分配内存,用于存放输入的字符串
ptr(arg1).writeUtf8String(data) //向内存中写入输入的字符串
var arg3 = Memory.alloc(100) //分配内存,用于存放输出的base64字符串
func(arg1,data.length,arg3) //调用函数,第一个参数是输入的字符串地址,第二个参数是输入的字符串长度,第三个参数是输出的base64字符串地址
var result = ptr(arg3).readCString() //读取输出的base64字符串
return result; //返回base64字符串

}

1、查找函数基址

var base = Module.findBaseAddress('libnative-lib.so');
var func_addr = base + 0x8E84 + 1; //0x8E84是函数的偏移地址,+1是为了跳过Thumb指令

通过Module.findBaseAddress获取so文件的基址,再查找到函数的偏移地址。

然后还需要判断当前指令集是thumb的还是arm的。判断步骤如下:

(1)Option->Gereral->设置字节大小为4

image.png

(2)查看函数中的地址,如果全部为4字节的话,为arm指令集;如果含有2个字节,则为thumb指令集,thumb指令集在查找函数基址时要+1

image.png

2、最终执行结果如下:

image.png