Microsoft 오피스 패스워드 검증과 문서 암호 해제
오피스 문서 포맷에 걸린 패스워드 검증과 문서 암호 해제에 대하여 알아봅니다.
테스트 환경 : Windows 7 & Office 2013 (엑셀 및 워드)
# 패스워드 검증
먼저, 패스워드를 걸은 문서를 준비합니다.
오피스에서 패스워드를 설정할때 읽기 패스워드와 쓰기 패스워드를 설정해야 하는데 본 포스팅에서는 읽기 패스워드 검증만 다룹니다.
오피스 문서의 구조를 볼 수 있는 Structured Storage Viewer로 문서 파일을 열어보면, EncryptionInfo에서 암호화 정보를 XML로 확인할 수 있습니다. 제 문서의 경우는 아래와 같았습니다.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<encryption xmlns="http://schemas.microsoft.com/office/2006/encryption"
xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password"
xmlns:c="http://schemas.microsoft.com/office/2006/keyEncryptor/certificate">
<keyData
saltSize="16"
blockSize="16"
keyBits="256"
hashSize="64"
cipherAlgorithm="AES"
cipherChaining="ChainingModeCBC"
hashAlgorithm="SHA512"
saltValue="KtwuzFCcK6QP3ysDp0c9VA=="
/>
<dataIntegrity
encryptedHmacKey="huc0j3uogWrKgic0aUl1dcGY+ESN0MhIH59PX+uBVwFr5qlqthL/StMxY4Lmt+WmBB3I1eAM1G1kinPDnwshyg=="
encryptedHmacValue="uNQ9sZRZglMpRUFmsymsm7QCTC0IJy0PD6Pw+9ymrAAdJN+Lz1DuJlHyCuMr9L9appVn4Iy8QbDPG6WyTVoVyQ=="
/>
<keyEncryptors>
<keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">
<p:encryptedKey
spinCount="100000"
saltSize="16"
blockSize="16"
keyBits="256"
hashSize="64"
cipherAlgorithm="AES"
cipherChaining="ChainingModeCBC"
hashAlgorithm="SHA512"
saltValue="IdxU1HZJTfqRJ2lxI0JL2A=="
encryptedVerifierHashInput="+VL6xUT+JSrrgz9TZzM0uw=="
encryptedVerifierHashValue="2nEZi/gDl/2/K6Bp77YQfgHVmzucUdpK6vcv8vnPZcaZPrdtgx84AsbpqTLs8AMDNmB4QvCj7Iww+o0cmG+G7g=="
encryptedKeyValue="rRU8RZoSDlX9ZYGx+bJsCVh8WVeSmbAzttbNKzuT27U="
/>
</keyEncryptor>
</keyEncryptors>
</encryption>
아래 링크에 각 항목에 대한 설명이 있습니다.
읽기 패스워드를 검증하기 위해선 아래의 keyEncryptor 구조만 보면 됩니다.
대략 살펴보면 반복 해시 10만번, salt 16바이트, AES256-CBC, SHA512 정도가 보입니다.
해당 정보들은 오피스 버전마다 다릅니다. 버전이 올라갈수록 암호화 강도도 세집니다.
(오피스 2007의 경우 반복 해시 0번에 SHA을 사용합니다.)
패스워드를 검증하는데 필요한 항목은 saltValue, encryptedVerifierHashInput, encryptedVerifierHashValue 이 세가지 입니다. 디코드하면 각각 16바이트, 16바이트, 64바이트입니다.
salt는 사용자 입력 패스워드에 덧붙이는 랜덤 난수 생성 값으로 복호화 시 IV에 사용됩니다.
encryptedVerifierHashInput와 encryptedVerifierHashValue는 암호화 된 검증용 값들로 패스워드 해시로 유도한 대칭키로 복호화 한 후 패스워드가 올바른지 검증하는데 사용합니다.
패스워드를 검증하는 일련의 과정을 간략하게 도식화하면 아래와 같습니다.
[키 생성]
[키 검증]
첫번째 그림은 키 생성부입니다. 2개의 대칭키(복호화 키)를 뽑아내고 있습니다. 1 ~ 5)번까지의 과정입니다. 두번째 그림은 키 검증부입니다. 6 ~ 8)번까지의 과정입니다.
1) 16바이트 salt와 사용자 입력 패스워드를 sha512 해시(이하 해시)합니다.
2) 현재의 spin count를 입력하고 1)에서 생성한 해시를 붙입니다. (spin count (4바이트) + hash (64바이트))
3) 2)를 해시하고 spin count 뒤에 붙입니다. 이를 10만번 반복합니다. 최종적으로 패스워드 해시가 생성됩니다.
4) hash input 블록키를 패스워드 해시 뒤에 붙이고 해시합니다. 해당 해시는 encryptedVerifierHashInput의 대칭키가 됩니다.
5) hash value 블록키를 패스워드 해시 뒤에 붙이고 해시합니다. encryptedVerifierHashValue의 대칭키가 됩니다.
이제 얻어낸 키로 암호화 된 2개의 데이터를 복호화 해야 합니다.
AES 256비트와 CBC모드를 사용중이며 IV는 32바이트인데 salt (16바이트) + 패딩 0 (16바이트)로 되어 있습니다.
6) 4, 5)에서 얻어낸 각각의 대칭키를 넣고 복호화를 하여 16바이트 / 64바이트 데이터를 얻습니다.
7) 복호화 된 hash input 데이터를 해시한 후 복호화 된 hash value와 비교합니다.
8) 일치하면 올바른 패스워드입니다.
# 문서 암호 해제
복호화를 하기 전에 원본 문서 길이(복호화 길이)와 암호화된 문서 데이터를 알아야 합니다.
문서의 구조를 보면 EncryptedPackage 라는 항목이 있는데 원본 문서를 암호화 한 데이터가 들어가 있는 항목입니다.
첫번째 8바이트는 복호화 길이로 0x2071 = 8305바이트를 포함한다고 명시되어 있습니다.
암호화 된 데이터의 위치는 오프셋 0x8부터 끝까지입니다.
다시 xml구조를 봅니다. 문서를 복호화하는데 필요한 항목은 패스워드 검증할 때와 조금 다릅니다.
keyEncryptor 구조의 encryptedKeyValue와 keyData 구조의 saltValue만 있으면 됩니다.
encryptedKeyValue는 암호화 된 대칭키입니다. 이 키로 문서를 복호화합니다. saltValue는 IV를 생성할 때 사용하는 salt 입니다. 디코드를 하면 각각 32바이트, 16바이트입니다.
복호화 시 중요한 점은 문서 전체를 한번에 복호화 하는 것이 아닌 블록 단위 (4096바이트)로 합니다. 이 과정에서 IV는 매번 다른 값을 사용합니다.
1) key 블록키를 패스워드 해시 뒤에 붙이고 해시합니다. 해당 해시는 encryptedKeyValue의 대칭키가 됩니다. (블록키는 소스를 참조)
2) 1)에서 얻어낸 키로 복호화를 하여 32바이트 대칭키를 얻습니다.
3) IV연산은 16바이트 salt 에 4바이트 블록키를 더해서 생성합니다. 디폴트 블록키는 0이며 한 사이클당 1씩 증가합니다.
4) salt+블록키 = 20바이트를 해시합니다.
5) 해시된 데이터를 32바이트 컷합니다. (해시에 1 2 3 ... 64가 담겼다고 가정할 경우 32에서 컷) 해당 데이터를 IV로 사용합니다.
6) 대칭키와 IV, 그리고 암호화된 문서 블록(4096바이트)를 가지고 복호화합니다.
7) 3)부터 N번만큼 복호화를 반복합니다. 마지막 복호화의 길이는 원본문서길이 % 블록 사이즈입니다. 원본 길이가 8305이면 N은 3, 마지막 암/복호화 길이는 113입니다.
복호화된 데이터는 오피스에서 바로 오픈할 수 있습니다.
#include "stdafx.h"
#include <Windows.h>
#include <openssl/sha.h>
#include <openssl/aes.h>
#include "base64.h"
// 사용자 입력 패스워드 (unicode)
#define Password L"1234"
/* Input XML */
// From KeyData
#define SaltValue "PyRz/rrl5W3f8APOTTRqEg=="
// From KeyEncryptor
#define Salt "I9I1XvYqB2CZ/Xj/Ktt8tg=="
#define EncryptedVerifierHashInput "CoY5oPNs2r0eNXgnGxpVSA=="
#define EncryptedVerifierHashValue "oDHZqZdAG/ues/73uvAOL4yG2HlILGxIO2E0kfJ/QWFqO1E9JuX8jBvtR93o26DR0UimnRRI5NjarERGk4ajfA=="
#define EncryptedKeyValue "TblBTCEtWuGcM9qdLMMCkGNhJXiTNUcgg486ykUr7KA="
#define SPIN_COUNT 100000
#define MAX_FILE_SIZE 999999
void Decrypt2(
IN PBYTE pbySalt, // salt
IN PBYTE pbyKey, // 복호화용 대칭키
IN PBYTE pbyEncryptedContent, // 암호화 된 컨텐츠
OUT PBYTE pbyDecryptedContent, // 복호화 된 컨텐츠
IN int cbDecryptLength // 복호화 된 문서 길이
)
{
DWORD dwBlockSize = 4096; // 블록 길이
DWORD dwRead = 0; // 총 복호화 한 길이
int n = (cbDecryptLength + dwBlockSize - 1) / dwBlockSize; // 복호화 횟수
for (int i = 0; i < n; i++)
{
DWORD dwLen = 0;
// 한 블록당 복호화 할 길이를 구한다.
if (i < n - 1)
{
dwLen = dwBlockSize;
}
else
{
dwLen = cbDecryptLength % dwBlockSize;
}
// iv + blockkey
BYTE temp[20] = { 0, };
// 블록키는 1씩 증가한다.
BYTE byBlockKey[4] = { 0, };
memcpy(byBlockKey, &i, sizeof(int));
memcpy(temp, pbySalt, 16); // 16바이트를 salt로 채운다.
memcpy(temp + 16, byBlockKey, 4); // 블록키를 iv뒤에 4바이트 이어붙임
// 64바이트 iv hash생성
BYTE ivhash[64] = { 0, };
SHA512_CTX ctx;
SHA512_Init(&ctx);
SHA512_Update(&ctx, temp, 20);
SHA512_Final(ivhash, &ctx);
BYTE iv[32] = { 0, }; // iv는 32바이트이다.
AES_KEY akey;
memcpy(iv, ivhash, 32); // IV 해시에서 32바이트만큼 복사한다.
memset(&akey, 0, sizeof(AES_KEY));
// iv와 대칭키를 설정한다.
if (AES_set_decrypt_key(pbyKey, 256, &akey) < 0)
{
printf("error\n");
return;
}
AES_cbc_encrypt(pbyEncryptedContent + (i*dwBlockSize), (unsigned char*)pbyDecryptedContent+dwRead, dwLen, &akey, iv, AES_DECRYPT);
dwRead += dwLen;
}
}
void Decrypt(
IN PBYTE pbySalt, // salt
IN PBYTE pbyKey, // 복호화용 대칭키
IN PBYTE pbyEncryptedContent, // 암호화 된 컨텐츠
OUT PBYTE pbyDecryptedContent, // 복호화 된 컨텐츠
IN int cbDecryptLength // 복호화 된 검증값 길이
)
{
BYTE iv[32] = { 0, }; // iv는 32바이트이다.
AES_KEY akey;
memcpy(iv, pbySalt, 16); // 16바이트를 salt로 채운다.
memset(&iv[16], 0, 16); // 16바이트 이후를 0으로 채운다.
memset(&akey, 0, sizeof(AES_KEY));
// iv와 대칭키를 설정한다.
if (AES_set_decrypt_key(pbyKey, 256, &akey) < 0)
{
printf("error\n");
return;
}
AES_cbc_encrypt(pbyEncryptedContent, (unsigned char*)pbyDecryptedContent, cbDecryptLength, &akey, iv, AES_DECRYPT);
}
void GenPasswordHash(
OUT PBYTE pbyHash,
IN PWCHAR pszPassword,
IN PBYTE pbySalt)
{
// spin count를 입력해야하기 때문에 UINT형을 사용한다.
UINT32 uiBuffer[(SHA512_DIGEST_LENGTH / sizeof(UINT32)) + 1] = { 0, };
int len = wcslen(pszPassword) * 2; // 패스워드는 유니코드로 처리한다.
SHA512_CTX ctx;
SHA512_Init(&ctx);
SHA512_Update(&ctx, pbySalt, 16);
SHA512_Update(&ctx, pszPassword, len);
SHA512_Final((BYTE*)&uiBuffer[1], &ctx);
// spin_count = 100000
for (int i = 0; i < SPIN_COUNT; i++)
{
*uiBuffer = i; // spin count
SHA512_Init(&ctx);
SHA512_Update(&ctx, &uiBuffer, SHA512_DIGEST_LENGTH + 4 /*spin count buffer length */);
SHA512_Final((BYTE*)&uiBuffer[1], &ctx);
}
memcpy(pbyHash, (BYTE*)&uiBuffer[1], SHA512_DIGEST_LENGTH);
}
void GenAgileEncryptionKey(
IN PBYTE pbyHash, // password 해시
IN PBYTE pbyBlockKey, // 블록키
OUT PBYTE pbyEncKey // 생성된 대칭키
)
{
BYTE byHashWithKey[SHA512_DIGEST_LENGTH + 8] = { 0, };
// password 해시 복사
memcpy(byHashWithKey, pbyHash, SHA512_DIGEST_LENGTH);
// 해시 뒤에 블록키 붙임
memcpy(&byHashWithKey[SHA512_DIGEST_LENGTH], pbyBlockKey, 8);
SHA512_CTX ctx;
SHA512_Init(&ctx);
SHA512_Update(&ctx, byHashWithKey, SHA512_DIGEST_LENGTH + 8);
SHA512_Final(pbyEncKey, &ctx);
}
int _tmain(int argc, _TCHAR* argv[])
{
/*----------------------------
패스워드 검증
-----------------------------*/
// 블록 키 (고정값)
BYTE byHashInputBlockKey[] = { 0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79 };
BYTE byHashValueBlockKey[] = { 0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e };
// decode64 된 데이터
BYTE bySalt[16] = { 0, };
BYTE byEncryptedVerifierHashInput[16] = { 0, };
BYTE byEncryptedVerifierHashValue[64] = { 0, };
base64_decode(Salt, bySalt, 16);
base64_decode(EncryptedVerifierHashInput, byEncryptedVerifierHashInput, 16);
base64_decode(EncryptedVerifierHashValue, byEncryptedVerifierHashValue, 64);
// 암호화 데이터를 복호화하는 대칭키
BYTE byHashInputKey[SHA512_DIGEST_LENGTH] = { 0, };
BYTE byHashValueKey[SHA512_DIGEST_LENGTH] = { 0, };
// 패스워드 해시를 구함
BYTE byPwHash[64] = { 0, };
GenPasswordHash(byPwHash, Password, bySalt);
// 대칭키 생성 (with password hash)
GenAgileEncryptionKey(byPwHash, byHashInputBlockKey, byHashInputKey);
GenAgileEncryptionKey(byPwHash, byHashValueBlockKey, byHashValueKey);
// 복호화
BYTE byDecryptedVerifierHashInput[16] = { 0, };
BYTE byDecryptedVerifierHashValue[64] = { 0, };
Decrypt(bySalt, byHashInputKey, byEncryptedVerifierHashInput, byDecryptedVerifierHashInput, 16);
Decrypt(bySalt, byHashValueKey, byEncryptedVerifierHashValue, byDecryptedVerifierHashValue, 64);
// 복호화 된 검증용 해시 입력을 해시한다.
BYTE byFinalHash[SHA512_DIGEST_LENGTH] = { 0, };
SHA512_CTX ctx;
SHA512_Init(&ctx);
SHA512_Update(&ctx, byDecryptedVerifierHashInput, 16);
SHA512_Final(byFinalHash, &ctx);
// 해시 비교한다.
int nCmp = memcmp(byFinalHash, byDecryptedVerifierHashValue, SHA512_DIGEST_LENGTH);
if (0 == nCmp) printf("correct password!\n");
else printf("incorrect password!\n");
/*----------------------------
문서 복호화
-----------------------------*/
// 블록 키 (고정값)
BYTE byKeyBlockKey[] = { 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6 };
BYTE* byEncryptedFileBuffer = NULL;
BYTE* byDecryptedFileBuffer = NULL;
byEncryptedFileBuffer = (BYTE*)malloc(MAX_FILE_SIZE);
byDecryptedFileBuffer = (BYTE*)malloc(MAX_FILE_SIZE);
UINT64* pullDecryptLength = (UINT64*)malloc(sizeof(UINT64));
// EncryptPackage 파일 오픈
HANDLE hFile = CreateFile(L"D:\\EncryptedPackage2", GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
DWORD dwRead = 0;
ReadFile(hFile, byEncryptedFileBuffer, MAX_FILE_SIZE, &dwRead, NULL);
// 앞 8바이트에서 복호화 길이를 구한다.
memcpy(pullDecryptLength, byEncryptedFileBuffer, 8);
BYTE bySaltValue[16] = { 0, };
BYTE byEncryptedKeyValue[32] = { 0, };
base64_decode(SaltValue, bySaltValue, 16);
base64_decode(EncryptedKeyValue, byEncryptedKeyValue, 32);
// 대칭키 생성 (with password hash)
BYTE byKeyValueKey[SHA512_DIGEST_LENGTH] = { 0, };
GenAgileEncryptionKey(byPwHash, byKeyBlockKey, byKeyValueKey);
// 대칭키를 복호화한다.
BYTE byDecryptedKeyValue[32] = { 0, };
Decrypt(bySalt, byKeyValueKey, byEncryptedKeyValue, byDecryptedKeyValue, 32);
HANDLE hSaveFile = CreateFile(L"D:\\Decrypt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
// 문서를 복호화한다.
Decrypt2(bySaltValue, byDecryptedKeyValue, &byEncryptedFileBuffer[8], byDecryptedFileBuffer, *pullDecryptLength);
DWORD dwWritten = 0;
WriteFile(hSaveFile, byDecryptedFileBuffer, *pullDecryptLength, &dwWritten, NULL);
free(pullDecryptLength);
getchar();
return 0;
}