# 逆向OpenSSL实现的加密算法相关笔记 ## 如何通过反汇编方法确定无符号程序EVP接口实现的加解密算法 需求:给一个静态链接了OpenSSL库的无符号程序,其加解密算法使用EVP接口实现,确定该算法名称。 EVP函数为OpenSSL加解密函数提供了高级接口。无论底层算法或模式如何都能提供一致的接口,支持的功能包括使用对称非对称算法进行加解密、签名/验证、密钥派生、安全哈希函数、消息认证码、外部加密引擎等。 以AES-256-CBC加密为例,通过EVP实现的代码如下所示(由deepseek生成): ```c #include #include #include #include void handle_errors() { ERR_print_errors_fp(stderr); abort(); } int aes_encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key, unsigned char *iv, unsigned char *ciphertext) { EVP_CIPHER_CTX *ctx; int len; int ciphertext_len; // 创建并初始化上下文 if(!(ctx = EVP_CIPHER_CTX_new())) handle_errors(); // 初始化加密操作 // 这里使用 AES-256-CBC,可以根据需要改为 EVP_aes_128_cbc() 或 EVP_aes_192_cbc() if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv)) handle_errors(); // 提供要加密的消息,获取加密输出 if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) handle_errors(); ciphertext_len = len; // 完成加密过程 if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handle_errors(); ciphertext_len += len; // 清理上下文 EVP_CIPHER_CTX_free(ctx); return ciphertext_len; } int main() { // 初始化 OpenSSL 库 OpenSSL_add_all_algorithms(); ERR_load_crypto_strings(); /* 设置密钥和 IV。 注意:在实际应用中,密钥应该安全地生成和存储, IV 应该是随机的,每次加密都不同 */ // AES-256 需要 32 字节密钥 unsigned char key[] = "01234567890123456789012345678901"; // IV 总是 16 字节(AES 块大小) unsigned char iv[] = "0123456789012345"; // 要加密的消息 unsigned char plaintext[] = "This is a secret message to be encrypted"; int plaintext_len = strlen((char *)plaintext); // 缓冲区用于存储密文 // 确保缓冲区足够大:明文长度 + 块大小(用于填充) unsigned char ciphertext[plaintext_len + EVP_MAX_BLOCK_LENGTH]; int ciphertext_len; // 执行加密 ciphertext_len = aes_encrypt(plaintext, plaintext_len, key, iv, ciphertext); // 打印结果 printf("Plaintext:\n%s\n", plaintext); printf("Ciphertext (%d bytes):\n", ciphertext_len); for(int i = 0; i < ciphertext_len; i++) printf("%02x", ciphertext[i]); printf("\n"); // 清理 OpenSSL 资源 EVP_cleanup(); ERR_free_strings(); return 0; } ``` 将openssl库静态链接到demo内: ```bash $ gcc aes_enc.c -o aes_enc -I"/usr/include/openssl/" -L"/lib/x86_64-linux-gnu" -l:libssl.a -l:libcrypto.a -l:libzstd.so.1 -l:/libz.so.1 $ ldd aes_enc linux-vdso.so.1 (0x00007fffe9e05000) libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007f192e500000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f192e4e3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f192e2f0000) /lib64/ld-linux-x86-64.so.2 (0x00007f192ea8c000) ``` 这个程序是带符号的,可以看到`EVP_aes_256_cbc`实现如下: ```c char *EVP_aes_256_cbc() { char *result; // rax result = &aes_256_cbc; if ( (OPENSSL_ia32cap_P & 0x200000000000000LL) != 0 ) result = (char *)&aesni_256_cbc; return result; } ``` 而 `aes_256_cbc` 反汇编数据格式如下: ```disasm .data.rel.ro:000000000044E9E0 aes_256_cbc dd 427 ; DATA XREF: EVP_aes_256_cbc+16↑o .data.rel.ro:000000000044E9E4 dd 10h .data.rel.ro:000000000044E9E8 dd 20h .data.rel.ro:000000000044E9EC dd 10h .data.rel.ro:000000000044E9F0 dq 2 .data.rel.ro:000000000044E9F8 dq 1 .data.rel.ro:000000000044EA00 dq offset aes_init_key .data.rel.ro:000000000044EA08 dq offset aes_cbc_cipher .data.rel.ro:000000000044EA10 dq 0 .data.rel.ro:000000000044EA18 dq offset qword_108 .data.rel.ro:000000000044EA20 dq 0 .data.rel.ro:000000000044EA28 dq 0 ``` 该格式其实是结构体`evp_cipher_st`(https://github.com/openssl/openssl/blob/648366ad010b3b22c1f298d39934d72702b3fd55/include/crypto/evp.h#L300)其第一个字段`nid`便是该算法名称对应的id。映射文件在文件obj_mac.num(https://github.com/openssl/openssl/blob/648366ad010b3b22c1f298d39934d72702b3fd55/crypto/objects/obj_mac.num#L427 ) ``` aes_256_cbc 427 ``` 因此确定无符号程序中EVP实现的加解密算法名称流程如下: - 定位EVP加解密函数位置,在没有符号的情况下定位这些函数也不难,`EVP_EncryptInit_ex`、`EVP_EncryptUpdate`中有大量 `OPENSSL_assert`提供的字符串可供定位函数名,如由`evp_cipher_init_internal`实现的`EVP_EncryptInit_ex`(https://github.com/openssl/openssl/blob/30adecd7258b1c657466f1ecf0c1d29491aac0b4/crypto/evp/evp_enc.c#L414)可直接搜索该assert条件的字符串定位。 - 定位到`EVP_EncryptInit_ex`后,查找该函数参数交叉引用,找到类似`EVP_aes_256_cbc`函数后定位`evp_cipher_st`的`nid`之后查表即可。 PS: 这里的`aesni_cbc_encrypt`是 OpenSSL 中利用 Intel AES-NI (AES New Instructions) 硬件指令集实现的高性能 AES-CBC 加密/解密函数。使用 CPU 内置的 AES 指令集提供比纯软件实现高得多的性能(通常快 4-10 倍) ## DES为什么没用到S盒 逆向的时候看算法结构像DES,但流程中没有和S盒匹配的数据。之后查看openssl源码时发现,openssl将S盒替换和P盒置换合并成了一个表`DES_SPtrans` ```c //openssl/openssl/crypto/des/spr.h static const DES_LONG DES_SPtrans[8][64] = { /* S1 */ { 0x02080800L, 0x00080000L, 0x02000002L, 0x02080802L, 0x02000000L, 0x00080802L, 0x00080002L, 0x02000002L, /* ... (共 64 个条目) ... */ }, /* S2 */ { 0x40108010L, 0x00000000L, 0x00108000L, 0x40100000L, /* ... (共 64 个条目) ... */ }, /* ... S3 到 S8 ... */ }; ``` `DES_SPtrans` 是 OpenSSL 对 DES **S-Box 和 P-Box 的合并优化**,通过预计算查表提高性能。它存储了 **8 个 S-Box 的 64 个可能输入对应的 32 位输出**,使得 F-function 计算仅需 **8 次查表 + 异或**。这种优化在 **软件实现** 中非常高效,但在 **硬件实现** 中可能不如位操作直接(因为查表需要较大存储)。