안녕하세요.
드디어 그 동안 HWP 5.x 구현에 걸림돌이었던 배포용 문서의 암호화를 푸는
방법을 알아냈습니다. 정보를 공개해도 문제가 없나 확신이 들지 않아서 잠시
고민했지만, 여러가지 이유로 문제가 없다고 판단하고 공개합니다.
먼저 저는 역분석을 통해 알아내지 않았습니다. 그러니 저작권법에 언급된 역
분석의 제한을 (3자 제공 금지, 동일한 프로그램 금지 따위) 받지 않습니다.
사실 역분석을 할 만한 지식이나 경험도 부족하고요. 짧은 시간 동안 한컴오
피스 체험판에서 문서를 여러번 저장하면서 파일을 들여다 보고 이것 저것을
시도해 보는 것 만으로도 알아낼 수 있었습니다. 내용을 보면 아시겠지만 장
담하건대 HWP 스펙을 약간 이해하고 어느 정도 경험이 있는 프로그래머라면
시간 문제일 뿐 충분히 알아낼 수 있습니다.
이 암호화가 DRM(저작권법의 "기술적 보호조치")으로 취급되고 이 정보의 공
개가 DRM을 해제하는 것으로 생각되면 문제가 될 수 있을 겁니다. 하지만 과
거에 배포용 문서의 문제에 대해 김호동님이 행안부에 문제를 제기했었을 때
한컴에서는 분명히 고의적으로 누락한 것이 아니다라고 답을 했었습니다. 한
컴 스스로 비밀 정보로 생각하지 않으니 DRM이 될 수 없다고 볼 수 있을 겁니
다.
아울러, 한글과컴퓨터(사)에 확인 결과 배포용 문서에 사용하는
“ViewText" 포맷은 기 공개된 "BodyText"와 동일한 내용으로 해당 업
체는 이를 고의적으로 누락한 것이 아니라 이미 공개된 것으로 간주
하고 있으며 현재 공개에서 누락된 부분에 대한 추가적인 보완도 추
진하고 있다고 합니다. 또한, 리눅스용 뷰어의 경우에는 리눅스의 국
내 도입 초기에 체험판 배부 등을 통해 제공하였으나 워낙 리눅스의
버전이나 종류가 다양해짐에 따라 일일히 제공하지는 못하고 있으며
향후 지속적인 관심을 가지고 노력하겠다는 대답을 받았습니다.
http://www.ubuntu.or.kr/viewtopic.php?p=81082#p81082
무엇보다도 저는 문서 내용을 읽고 싶을 뿐, 문서 내용을 편집/인쇄 가능하도
록 크랙하는 데는 관심이 없습니다. 문서 내용을 보기 위해 필요한 최소한의
작업이 크랙이 쉬워진다고 불법이 된다면 이상한 일일 겁니다.
그럼 시작합니다.
----------
ViewText는 BodyText와 마찬가지로 아래 Section0, Section1, ... 식으로 섹
션 스트림이 들어 있습니다. 각 섹션 스트림은 HWPTAG_DISTRIBUTE_DOC_DATA
레코드와 뒤의 암호화된 데이터로 구성되어 있습니다.
- SectionN
<HWPTAG_DISTRIBUTE_DOC_DATA record>
<encrypted data>
HWPTAG_DISTRIBUTE_DOC_DATA 레코드는 스펙에도 언급되어 있는데 256바이트라
는 것만 쓰여 있습니다. 바로 이 256바이트 안에 뒤의 암호화된 본문을 푸는
키가 들어 있습니다.
이 256바이트도 나름 암호화되어 있는데 간단히 알 수 있는 방식입니다. 256
바이트에서 첫 4바이트 정수를 시드로 이용해서, MSVC의 srand()/rand() 결과
를 이용해 나머지 252바이트를 XOR해 놓았습니다. MSVC의 srand()/rand()는
전통적인 linear congruential generator로 파라미터도 잘 알려져 있습니다.
다음과 같습니다.
srand(seed) {
random_seed = seed;
}
rand() {
random_seed = (random_seed * 214013 + 2531011) & 0xFFFFFFFF;
return (random_seed >> 16) & 0x7FFF;
}
참고:
http://en.wikipedia.org/wiki/Linear_congruential_generator#Parameters_in_common_use
252바이트의 암호를 푸는데 첫번째 rand() 결과의 하위 8비트를 XOR할 키로
취급하고, 두번째 rand() 결과의 하위 4비트 값에 1을 더한 값을 XOR할 개수
로 취급합니다. 그렇게 n바이트를 풀고 다시 난수값 두개를 구하는 걸 반복합
니다. 이렇게 252바이트를 풀면 되는데, 개수는 256바이트 처음부터 카운트하
는 것에 주의해야 합니다. 코드로 표현하면 다음과 같습니다.
srand(data[3] << 24 | data[2] << 16 | data[1] << 8 | data[0]);
for (i = 0, n = 0; i < 256; i++, n--) {
if (n == 0) {
key = rand() & 0xFF;
n = (rand() & 0xF) + 1;
}
if (i >= 4)
data[i] ^= key;
}
자 이렇게 HWPTAG_DISTRIBUTE_DOC_DATA를 풀었습니다. 이 안에 배포용 설정할
때 입력한 암호의 SHA-1 체크섬이 들어 있습니다. 특이하게도 바이너리로 안
들어 있고 UCS16 LE 문자열 40자로 (총 80바이트) 들어 있는 게 특이합니다.
들어 있는 위치는 위에서 난수 시드로 사용했던 첫번째 바이트의 하위 4비트
값만큼 오프셋에 들어 있습니다.
sha1ucsstr = &data[4 + (data[0] & 0xF)];
이 UCS2 LE 형식의 SHA-1 문자열 중에 앞 16바이트, 즉 128비트가 암호화된
본문을 푸는 키입니다. 암호화 알고리즘은 AES-128 ECB 모드입니다. 풀린 데
이터는 BodyText의 섹션과 마찬가지로 취급하면 됩니다.
----------
여기까지입니다. 구현에 필요한 것만 설명했고, 기타 설명은 답으로 덧붙이겠
습니다.