iOS逆向学习-006常用加密

2,809 阅读12分钟

本篇文章会重在如何调用api,原理不会深入,网上教程比较多。

下面是我的封装:

  • OC版本:GitHub地址
  • swift版本:GitHub地址,这里没有封装RSA(我承认是我懒,可以参照OC版本)

Base64

个人觉得,Base64并不是一种加密方式,而是一种编码方式,和ASCII码一样。

ASCII码用数值0-127表示128个字符,也就是2^7ASCII码的扩展能表达出2^8个字符,而Base64则用0-63表示2^6个字符,具体表如下:

image.png

我们可以发现这些字符都是比较好表达的,在ASCII表中有些符号不好展示出来,所以Base64更容易被我们复制黏贴。

除了表格中的字符,我们常常看到Base64字符串后面常常接着==表示的也是0,但是这个比较特殊,因为Base64编码的时候是以6个bit位编码的,而我们传输单位的字节是8bit的,Base64编码时常会发生不能整除的数据的情况,所以要最后补上0,而这些0会被编码成=,在解码的时候,=0会被忽略。

我们看下在iOS中的调用:

  • OC代码:
// Data转成Base64编码字符串
    NSString *string = [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    
// Base64编码转成Data
    [[NSData alloc] initWithBase64EncodedString:base64Str options:NSDataBase64DecodingIgnoreUnknownCharacters];
  • Swift代码:
// Data转成Base64编码字符串
    data.base64EncodedString()
    
// Base64编码转成Data
    let data = Data.init(base64Encoded: base64Str)

散列函数

散列函数又称哈希函数,具体原理可以看我前面写的探索Swift中Dictionary的底层实现及原理

苹果加密这块用的是CommonCrypto,所以在用加密前都要导入这个框架(iOS13推出了新的框架CryptoKit)

我们看下苹果支持多少种哈希函数

enum {
    kCCHmacAlgSHA1,
    kCCHmacAlgMD5,
    kCCHmacAlgSHA256,
    kCCHmacAlgSHA384,
    kCCHmacAlgSHA512,
    kCCHmacAlgSHA224
};
typedef uint32_t CCHmacAlgorithm;

Hmac的枚举值种可以看到,支持SHA1MD5SHA256SHA384SHA512SHA224,其他枚举值写的太零散了,不过CommonCrypto支持这6种。

不同哈希函数结果的字节长度也不一样:

#define CC_MD5_DIGEST_LENGTH    16          /* digest length in bytes */
#define CC_SHA1_DIGEST_LENGTH   20          /* digest length in bytes */
#define CC_SHA224_DIGEST_LENGTH     28          /* digest length in bytes */
#define CC_SHA256_DIGEST_LENGTH     32          /* digest length in bytes */
#define CC_SHA384_DIGEST_LENGTH     48          /* digest length in bytes */
#define CC_SHA512_DIGEST_LENGTH     64          /* digest length in bytes */

我们看下swift实现:

func hashData(data: Data) -> Data {
        
        var digest = Data(count: Int(hashDigestLength))
        digest.withUnsafeMutableBytes { (digestPtr: UnsafeMutablePointer<UInt8>) in
            // 这里碰到一个坑,Data的长度不一样,内存结构也会不一样(测下来15),本来data.withUnsafeBytes { $0 }只能用NSData(data: data).bytes了
            let dataPtr = NSData(data: data).bytes
            switch type {
            case .MD5:
                CC_MD5(dataPtr, numericCast(data.count), digestPtr)
            case .SHA1:
                CC_SHA1(dataPtr, numericCast(data.count), digestPtr)
            case .SHA224:
                CC_SHA224(dataPtr, numericCast(data.count), digestPtr)
            case .SHA256:
                CC_SHA256(dataPtr, numericCast(data.count), digestPtr)
            case .SHA384:
                CC_SHA384(dataPtr, numericCast(data.count), digestPtr)
            case .SHA512:
                CC_SHA512(dataPtr, numericCast(data.count), digestPtr)
            }
        }
        return digest
    }

OC版本可以在我封装的代码里面看。

加盐

虽然说哈希函数是一个不可逆的过程,理论上从结果上无法推断出原文是什么,但是网上还是有人才提供了一个数据库,里面存放了常见的字符串与对应的hash值,例如初始密码123456,就能从hash值查表的方式找到原文

MD5破解网站地址

那怎么办呢,我们可以在我们的字符串的后面加一串自定义的字符串,这样可以添加被破解的复杂度,这样的行为我们称之为加盐。但是这样比较low,所以系统提供了Hmac方式的哈希函数:

func hmacHashData(data: Data, hmacKey: Data) -> Data {
        var digest = Data(count: Int(hashDigestLength))
        digest.withUnsafeMutableBytes {
            CCHmac(CCHmacAlgorithm(hmacAlgorithm), NSData(data: hmacKey).bytes, hmacKey.count, NSData(data: data).bytes, data.count, $0)
        }
        return digest
    }

计算大文件的hash值

计算文件的hash值,我们可以先把文件读到内存中变成数据,然后调用api得到hash值,但是有一个问题,如果文件非常大的话,那么会占用非常大的内存,说不定导致APP内存溢出了,那怎么办呢?

框架给我们提供分布读取data进行hash的api:

CC_MD5_CTX hashCtx;
CC_MD5_Init(&hashCtx);
while (YES) {
    @autoreleasepool {
        NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
        CC_MD5_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
        if (data.length == 0) {
            break;
        }
    }
}
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CC_MD5_Final(buffer, &hashCtx);
result = [NSData dataWithBytes:buffer length:CC_MD5_DIGEST_LENGTH];

在循环内部放一个自动释放池@autoreleasepool,这样就能及时释放内存了。

对称加密

对称加密在CommonCrypto就一个核心的api,把这个api搞懂了,那么就能把对称加密掌握了,我们看下这个api:

CCCryptorStatus CCCrypt(
    CCOperation op,         /* kCCEncrypt, etc. */
    CCAlgorithm alg,        /* kCCAlgorithmAES128, etc. */
    CCOptions options,      /* kCCOptionPKCS7Padding, etc. */
    const void *key,
    size_t keyLength,
    const void *iv,         /* optional initialization vector */
    const void *dataIn,     /* optional per op and alg */
    size_t dataInLength,
    void *dataOut,          /* data RETURNED here */
    size_t dataOutAvailable,
    size_t *dataOutMoved)
    API_AVAILABLE(macos(10.4), ios(2.0));

我们分析下各个参数

CCOperation

这是一个枚举值,很简单:

enum {
    kCCEncrypt = 0,
    kCCDecrypt,
};
typedef uint32_t CCOperation;

一个是加密一个是解密

CCAlgorithm

这个是CommonCrypto支持对称加密种类的枚举值

enum {
    kCCAlgorithmAES128 = 0, /* Deprecated, name phased out due to ambiguity with key size */
    kCCAlgorithmAES = 0,
    kCCAlgorithmDES,
    kCCAlgorithm3DES,
    kCCAlgorithmCAST,
    kCCAlgorithmRC4,
    kCCAlgorithmRC2,
    kCCAlgorithmBlowfish
};
typedef uint32_t CCAlgorithm;

第一个kCCAlgorithmAES128第一个被废弃了,用了也等于kCCAlgorithmAES

CCOptions

这个是对称加密的一些可选项:

enum {
    /* options for block ciphers */
    kCCOptionPKCS7Padding   = 0x0001,
    kCCOptionECBMode        = 0x0002
    /* stream ciphers currently have no options */
};
typedef uint32_t CCOptions;

虽然只有两个枚举,但应该是设计成可选枚举的,所以我们用这个枚举值的时候用位运算符。

填充模式

我们先看第一个kCCOptionPKCS7Padding,这是一个填充模式,不使用枚举值的时候是ZeroPadding,和NoPadding一个意思。

说填充模式前,我们先认识下块(Block)的概念,对称加密算法对数据加密都是一段数据一段数据加密,每一段数据都被称之为块Block,不同加密算法的块(数据)长度是不一样的,我们看下对应的枚举值:

enum {
    /* AES */
    kCCBlockSizeAES128        = 16,
    /* DES */
    kCCBlockSizeDES           = 8,
    /* 3DES */
    kCCBlockSize3DES          = 8,
    /* CAST */
    kCCBlockSizeCAST          = 8,
    kCCBlockSizeRC2           = 8,
    kCCBlockSizeBlowfish      = 8,
};

说完这个我们回到填充模式

  • ZeroPadding:数据长度不对齐(数据长度不能被块Block大小整除)时使用0填充,否则不填充。
  • PKCS7Padding:数据长度不对齐时使用数字n填充至对齐,数据长度已经对齐时后面填充一块块Block长度的n数字数据,n为你所补充的字节长度。
  • PKCS5Padding:PKCS7Padding的子集,块大小固定为8字节,也就是n固定为8。(iOS中并没有这个选项 = =!)

加密方式

我们看到第二个枚举值是kCCOptionECBMode,如果不选,默认是ECBMode

  • ECB(Electronic Code Book):电子密码本模式。每一块数据,独立加密。最基本的加密模式,也就是通常理解的加密,相同的明文将永远加密成相同的密文,无初始向量,容易受到密码本重放攻击,一般情况下很少用。

这里的每一块指的就是前面说的Block,虽然上面介绍说很少使用,但是在应用中,因为比较简单,没有向量参数,平时反而用的多。 = =

  • CBC(Cipher Block Chaining):密码分组链接模式。使用一个密钥和一个初始化向量[IV]对数据执行加密。明文被加密前要与前面的密文进行异或运算后再加密,因此只要选择不同的初始向量,相同的密文加密后会形成不同的密文,这是目前应用最广泛的模式。CBC加密后的密文是上下文相关的,但明文的错误不会传递到后续分组,但如果一个分组丢失,后面的分组将全部作废(同步错误)。CBC可以有效的保证密文的完整性,如果一个数据块在传递是丢失或改变,后面的数据将无法正常解密。

ECB相比,CBC更加安全,因为CBC的每一块数据加密都和前一块数据有关,所以在CBC模式下,如果你只拿到中间一段加密数据,即使有秘钥,也解不开数据。但我个人觉得,在APP下抓包请求,一般都能拿到完整的加密数据,所以CBC模式的这个特性,在这个场景下好像并没有多少优势,所以平时用ECB会多一点。

keykeyLength

key需要传入的是秘钥data的指针,这个没什么好说的,最关键的还是keyLength,每种加密方式需要的秘密长度是不一样的,具体的在框架中用枚举已经体现出来了:

*/
enum {
    kCCKeySizeAES128          = 16,
    kCCKeySizeAES192          = 24,
    kCCKeySizeAES256          = 32,
    kCCKeySizeDES             = 8,
    kCCKeySize3DES            = 24,
    kCCKeySizeMinCAST         = 5,
    kCCKeySizeMaxCAST         = 16,
    kCCKeySizeMinRC4          = 1,
    kCCKeySizeMaxRC4          = 512,
    kCCKeySizeMinRC2          = 1,
    kCCKeySizeMaxRC2          = 128,
    kCCKeySizeMinBlowfish     = 8,
    kCCKeySizeMaxBlowfish     = 56,
};

其中AES可以有3种不同的长度,分别对应了16,24,32个字节。

AESDES3DES的秘钥长度都是固定的,而CASTRC4RC2Blowfish的秘钥长度都是浮动的,这里给出了最大值及最小值,确保自己的秘钥长度在这区间之内

iv

关于iv前面提到过了,这个是独属于CBC加密模式的,所以如果前面的option内用的ECB模式,iv这个参数毫无作用。iv参数传的也是数据data指针,但是没有让你传长度,这个是因为iv的数据长度是和你加密的块Block长度是一致的,一旦你的加密模式确定了,那么iv的数据长度也是确定的,具体长度值看:

enum {
    /* AES */
    kCCBlockSizeAES128        = 16,
    /* DES */
    kCCBlockSizeDES           = 8,
    /* 3DES */
    kCCBlockSize3DES          = 8,
    /* CAST */
    kCCBlockSizeCAST          = 8,
    kCCBlockSizeRC2           = 8,
    kCCBlockSizeBlowfish      = 8,
};

如果你传的iv数据长度和加密类型不一致,那么短的会补0,长的会被截断。

dataIndataInLength

这两个比较简单,传的就是你需要加密或解密数据的数据指针和数据长度

dataOutdataOutAvailabledataOutMoved

这3个参数是用来获取加密或者解密后的数据的。

我们需要申请一块内存空间传递给函数,函数会在这块空间内做加密处理,并且函数处理完的数据也会放在空间内。

那么dataOut就是你申请的内存空间的指针,最好堆空间,因为并不能确定加解密数据有多大。

dataOutAvailable就是你申请内存空间的长度了,那么申请多大的空间合适呢?上面在填充模式的时候也说过了,在PKCS7Padding模式下,如果字节已经对齐,那么会在最后填充一块块Block大小的填充数据,而且这种情况也是在这么多填充模式下数据最长的情况,所以我们申请空间的时候就以加密数据datalength加上blockSize就行了:

// setup output buffer
    size_t bufferSize = [data length] + blockSize;
    void *buffer = malloc(bufferSize);
};

dataOutMoved这个值就是获取结果的数据长度,是一个size_t *类型,所以你传一个size_t变量的指针过去,等加密或解密操作结束正确后,变量就会拿到结果data真正的长度

CCCryptorStatus

CCCryptorStatus是函数方法CCCrypt的返回值,如果是0,那么说明你调用CCCrypt成功加密或者解密了,如果不是0,而是其他值,可以参考:

enum {
    kCCSuccess          = 0,
    kCCParamError       = -4300,
    kCCBufferTooSmall   = -4301,
    kCCMemoryFailure    = -4302,
    kCCAlignmentError   = -4303,
    kCCDecodeError      = -4304,
    kCCUnimplemented    = -4305,
    kCCOverflow         = -4306,
    kCCRNGFailure       = -4307,
    kCCUnspecifiedError = -4308,
    kCCCallSequenceError= -4309,
    kCCKeySizeError     = -4310,
    kCCInvalidKey       = -4311,
};

有关函数方法CCCrypt已经讲完了,具体使用看GitHub上的封装

非对称加密RSA

RSA是常见的非对称加密之一,也是我们日常APP开发遇到最多的非对称加密

关于RSA的介绍就不多说了,关于概念和原理网上有很多的,这里说下RSAiOS下的细节。

RSAiOS中不导入三方框架的情况下,貌似只能依靠钥匙串的框架Security实现,具体使用看GitHub

有很多都是固定用法没什么好讲的,说下几个注意点

  • 系统iOS 10开始支持pem文件的内容的解析,所以我封装框架最低系统支持也是iOS 10,主要是其中的一个方法可以通过秘钥的data来生成ref对象:
SecKeyRef _Nullable SecKeyCreateWithData(CFDataRef keyData, CFDictionaryRef attributes, CFErrorRef *error)
__OSX_AVAILABLE(10.12) __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0);
  • RSA默认的填充方式是PKCS1,里面填充的值是随机值,所以每次加密的结果都不一样,如果你换成None不填充的话,那么不会填充数值,所以每次加密结果都一样。框架里还支持一种OAEP的填充方式,不是重点就不说了,感兴趣的自己搜下

  • 关于数据长度的,RSA加密数据的长度是有限制的,具体的可以调用:

size_t blockSize = SecKeyGetBlockSize(keyRef);

这个值是你能加密数据长度的最大值,但是由于填充数据的存在,你必须还要减掉一定的长度,PKCS1需要减掉11,OAEP需要减掉42。有些人封装的时候,如果超过最大的长度,直接会抛出错误,而我封装的会帮你分段加密后拼在一起,但还是不建议让加密数据过长

  • 关于签名:
typedef CF_OPTIONS(uint32_t, SecPadding)
{
    kSecPaddingNone      = 0,
    kSecPaddingPKCS1     = 1,
    kSecPaddingOAEP      = 2, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0),

    /* For SecKeyRawSign/SecKeyRawVerify only,
     ECDSA signature is raw byte format {r,s}, big endian.
     First half is r, second half is s */
    kSecPaddingSigRaw  = 0x4000,

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is an MD2
       hash; standard ASN.1 padding will be done, as well as PKCS1 padding
       of the underlying RSA operation. */
    kSecPaddingPKCS1MD2  = 0x8000, // __OSX_DEPRECATED(10.0, 10.12, "MD2 is deprecated") __IOS_DEPRECATED(2.0, 5.0, "MD2 is deprecated") __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE,

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is an MD5
       hash; standard ASN.1 padding will be done, as well as PKCS1 padding
       of the underlying RSA operation. */
    kSecPaddingPKCS1MD5  = 0x8001, // __OSX_DEPRECATED(10.0, 10.12, "MD5 is deprecated") __IOS_DEPRECATED(2.0, 5.0, "MD5 is deprecated") __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE,

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA1
       hash; standard ASN.1 padding will be done, as well as PKCS1 padding
       of the underlying RSA operation. */
    kSecPaddingPKCS1SHA1 = 0x8002,
    
    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA224
     hash; standard ASN.1 padding will be done, as well as PKCS1 padding
     of the underlying RSA operation. */
    kSecPaddingPKCS1SHA224 = 0x8003, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA256
     hash; standard ASN.1 padding will be done, as well as PKCS1 padding
     of the underlying RSA operation. */
    kSecPaddingPKCS1SHA256 = 0x8004, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA384
     hash; standard ASN.1 padding will be done, as well as PKCS1 padding
     of the underlying RSA operation. */
    kSecPaddingPKCS1SHA384 = 0x8005, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),

    /* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA512
     hash; standard ASN.1 padding will be done, as well as PKCS1 padding
     of the underlying RSA operation. */
    kSecPaddingPKCS1SHA512 = 0x8006, // __OSX_UNAVAILABLE __IOS_AVAILABLE(2.0),
};

这边枚举值的前3个用于填充模式,后面的用于签名与签名验证。md2md5已经被废弃了,剩下了SHA1SHA224SHA256SHA384SHA512。具体签名看代码。