배포용 문서의 ViewText 암호화 해제 방법

6,781 views
Skip to first unread message

Changwoo Ryu

unread,
Jan 4, 2014, 6:37:33 AM1/4/14
to hwp-...@googlegroups.com
안녕하세요.

드디어 그 동안 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의 섹션과 마찬가지로 취급하면 됩니다.

----------

여기까지입니다. 구현에 필요한 것만 설명했고, 기타 설명은 답으로 덧붙이겠
습니다.


Changwoo Ryu

unread,
Jan 4, 2014, 7:15:20 AM1/4/14
to hwp-...@googlegroups.com
이 메일에서는 제가 어떻게 알아냈는지 설명합니다. 역분석이 아닌 방법으로
충분히 알아낼 수 있었다는 점을 설명하기 위해서입니다.


2014-01-04 (토), 20:37 +0900, Changwoo Ryu:

> ViewText는 BodyText와 마찬가지로 아래 Section0, Section1, ... 식으로 섹
> 션 스트림이 들어 있습니다. 각 섹션 스트림은 HWPTAG_DISTRIBUTE_DOC_DATA
[...]

스펙에 언급된 HWPTAG_DISTRIBUTE_DOC_DATA가 들어갈 곳이 필요했는데,
ViewText 앞에 레코드 헤더처럶 보이는 데이터가 있으니 쉽게 알아낼 수 있습
니다.

> 이 256바이트도 나름 암호화되어 있는데 간단히 알 수 있는 방식입니다. 256
> 바이트에서 첫 4바이트 정수를 시드로 이용해서, MSVC의 srand()/rand() 결과
> 를 이용해 나머지 252바이트를 XOR해 놓았습니다. MSVC의 srand()/rand()는
> 전통적인 linear congruential generator로 파라미터도 잘 알려져 있습니다.
> 다음과 같습니다.
[...]

먼저 배포용 문서로 저장할 때 암호가 같으면 데이터가 변하지 않고, 암호가
다를 경우에만 뒤의 데이터가 바뀐다는 점에 주목했습니다. 즉 암호에 따라
결정되는 키가 있는데 그 키도 문서 안에 들어 있는 것이지요. 즉 한컴에서
만든 프로그램에 고정된 키가 들어 있는 방식이 아니었다는 겁니다. 여기서
잘 하면 풀 수 있겠다는 생각을 했습니다.

그리고 첫 4바이트가 난수 시드가 된다는 사실은 이 4바이트가 유닉스 time_t
타임스탬프였기 때문에 쉽게 알 수 있었습니다. 매우 익숙한 숫자인데다가 배
포용 문서로 저장할 때마다 증가하는 걸 확인할 수 있었거든요. 타임스탬프를
키로 사용하는 걸 생각하니 흔히 srand(time(NULL)) 처럼 난수 시드 지정하는
걸 떠올랐습니다. 여러가지 구현 중에서 시도해 보다가 MSVC에서 쓰는 알고리
즘이 정확히 일치했고요.

> 252바이트의 암호를 푸는데 첫번째 rand() 결과의 하위 8비트를 XOR할 키로
> 취급하고, 두번째 rand() 결과의 하위 4비트 값에 1을 더한 값을 XOR할 개수
> 로 취급합니다. 그렇게 n바이트를 풀고 다시 난수값 두개를 구하는 걸 반복합
> 니다. 이렇게 252바이트를 풀면 되는데, 개수는 256바이트 처음부터 카운트하
> 는 것에 주의해야 합니다. 코드로 표현하면 다음과 같습니다.
[...]

256바이트 데이터를 잘 보면 같은 바이트가 반복되는 경우가 많다는 것 때문
에 이것도 알 수 있었습니다. 원본 데이터의 대부분이 0으로 채워져 있는데
일정 개수만큼 XOR한 것이구나라고 쉽게 추측할 수 있었고요. rand()를
1,3,4,5,...번째 할 때마다 결과가 데이터에 등장하길래 짝수 번째 rand()의
결과가 다른 용도로 사용되고 있다는 것도 예상할 수 있었습니다.

> 자 이렇게 HWPTAG_DISTRIBUTE_DOC_DATA를 풀었습니다. 이 안에 배포용 설정할
> 때 입력한 암호의 SHA-1 체크섬이 들어 있습니다. 특이하게도 바이너리로 안
> 들어 있고 UCS16 LE 문자열 40자로 (총 80바이트) 들어 있는 게 특이합니다.
> 들어 있는 위치는 위에서 난수 시드로 사용했던 첫번째 바이트의 하위 4비트
> 값만큼 오프셋에 들어 있습니다.
[...]

그 다음부터는 어렵지 않습니다. 40자의 0-9, A-F로 이루어진 유니코드 문자
열이 등장하는 걸 보고 SHA-1 체크섬이라는 걸 추측할 수 있었고, 암호의
SHA-1 체크섬과 일치했습니다.

> 이 UCS2 LE 형식의 SHA-1 문자열 중에 앞 16바이트, 즉 128비트가 암호화된
> 본문을 푸는 키입니다. 암호화 알고리즘은 AES-128 ECB 모드입니다. 풀린 데
> 이터는 BodyText의 섹션과 마찬가지로 취급하면 됩니다.

어느 부분을 키로 사용하는지는 이 체크섬 데이터를 조작해 보면서 한컴오피
스에서 열어보는 것으로 확인이 되었습니다. 또 ECB 모드였기 때문에 문서 내
용을 바꿨을 때 16바이트 단위로 바뀌는 것도 확인할 수 있었습니다.

즉 128비트 키에 128비트 블럭 암호화라는 뜻인데, 그러면 대세인 AES-128을
맨 처음에 시도하는 게 너무 당연했고 단번에 일치했습니다.



Allen Jeon

unread,
Jan 5, 2014, 9:44:41 PM1/5/14
to hwp-...@googlegroups.com
와우~ 멋집니다. 탁월한 통찰력으로 훌륭한 결과를 찾아내셨네요.

전체적으로 SHA1, XOR, AES 세가지가 잘 조합된 것이라고 볼 수 있을텐데
지적하신 것처럼 SHA1 해쉬값의 헥사 문자열 일부를 암호화키로 쓰는 점과
암호화키 보관을 위하여 XOR하는 방법 등은 약간 어설픈 것같습니다.

배포용 HWP 문서를 받아놓고 아무 것도 할 수 없는 사람들에게
크게 도움되는 고마운 정보입니다. 응원합니다.


2014년 1월 4일 토요일 오후 8시 37분 33초 UTC+9, Changwoo Ryu 님의 말:

Changwoo Ryu

unread,
Jan 7, 2014, 2:12:55 AM1/7/14
to hwp-...@googlegroups.com
이제 네이버 오피스에서 볼 수 있나요? :>

혹시 암호화도 이렇게 문자열 일부를 키로 쓰는 게 아닌가 좀 걱정스럽습니다.
그렇다면 128비트 키를 써도 32비트 키인 것과 마찬가지라서 brute force로 풀 수 있으니까요.

2014년 1월 6일 오전 11:44, Allen Jeon <alle...@gmail.com>님의 말:
> --
> Google 그룹스 'hwp-foss' 그룹에 가입했으므로 본 메일이 전송되었습니다.
> 이 그룹에서 탈퇴하고 더 이상 이메일을 받지 않으려면 hwp-foss+u...@googlegroups.com에 이메일을
> 보내세요.
> 더 많은 옵션을 보려면 https://groups.google.com/groups/opt_out을(를) 방문하세요.

Allen Jeon

unread,
Jan 10, 2014, 5:11:12 AM1/10/14
to hwp-...@googlegroups.com
네이버 오피스의 서비스 주체는 네이버라서 저희가 할 수 있다고 바로 적용하지는 못하고요. 기능개선에 대한 사용자 요구 정도나 다른 작업과의 우선순위, 일정 계획 등 모든 부분 네이버와 협의를 거쳐 진행합니다. 고객사와의 정보를 공개하는 것은 적절치 않기에 여기까지만 말씀드립니다.

저희 내부적으로는 지난해 여름 배포용문서에 대한 포맷분석작업을 마쳤고요. 저희가 꽤 많은 시행착오를 거쳐 알아낸 반면, Changwoo Ryu님은 탁월한 논리 전개로 매우 빠른 시간에 알아내신 것같아 크게 감탄했습니다. 정말로 대단하십니다. 구로에 오실 때 연락주세요. 밥사겠습니다.

끝으로, 읽어야 할 필요성이 있는 배포용문서와 달리 다른 암호화 문서는 분석해야할 명확한 사용자 필요성이 없기 때문에 분석하지도 않았고 그래서 알고 있는 바가 없고 분석할 계획도 없습니다. 우려하신 것처럼 되어 있지 않기를 바랍니다. ^^



2014년 1월 7일 화요일 오후 4시 12분 55초 UTC+9, Changwoo Ryu 님의 말:

류형수

unread,
Jan 27, 2014, 12:59:23 PM1/27/14
to hwp-...@googlegroups.com
개인 적인 호기심으로 3주째 한글 포맷에 덤벼들고 있는 사람입니다. 자바 파서와 렌더러를 만들고 있구요... 배포 문서 부분은 생각하지도 않고 있었는데 귀중한 정보를 나눠주심에 감사드립니다. 하지만, 배포문서 이전에 손볼 게 넘 많아서 한참 후에나 참고할 것 같습니다. :)


2014년 1월 4일 토요일 오후 8시 37분 33초 UTC+9, Changwoo Ryu 님의 말:
안녕하세요.

Changwoo Ryu

unread,
Oct 11, 2014, 3:47:22 PM10/11/14
to hwp-...@googlegroups.com
배포용 문서의 스펙이 공개되었습니다.

http://www.hancom.com/forMatQna.boardIntro.do

내용은 제가 발견한 것과 다르지 않은데, SHA-1 암호 해시 다음에 있는 WORD가 배포용 문서의 옵션이라는 정보가 있군요.


2014년 1월 4일 토요일 오후 8시 37분 33초 UTC+9, Changwoo Ryu 님의 말:
안녕하세요.

유성

unread,
Oct 15, 2014, 4:07:44 PM10/15/14
to hwp-...@googlegroups.com
On Sat, Oct 11, 2014 at 12:47:22PM -0700, Changwoo Ryu wrote:
같이 배포된 원래 스펙문서도 여러가지 업데이트 된 듯 합니다.

얼핏보니 각 필드별로 어느 버젼에서 추가되었는지도 나와있고,

누락되었던 HWPTAG_PARA_LINE_SEG의 내용도 공개되었네요. 시간이 나면 좀 더

살펴봐야겠습니다.
> --
> Google 그룹스 'hwp-foss' 그룹에 가입했으므로 본 메일이 전송되었습니다.
> 이 그룹에서 탈퇴하고 더 이상 이메일을 받지 않으려면 hwp-foss+u...@googlegroups.com에 이메일을 보내세요.
> 더 많은 옵션을 보려면 https://groups.google.com/d/optout을(를) 방문하세요.

Changwoo Ryu

unread,
Jan 14, 2015, 11:12:47 PM1/14/15
to hwp-...@googlegroups.com
한컴에서 문서도 공개되었고 하니 지금쯤 많은 분들이 구현한 것 같습니다.

헷갈리는 부분이 있는데, SHA1 체크섬의 UTF-8 스트링이 아니라 UTF-16LE 인코딩 형태의 문자열 중에서 앞
16바이트(128비트)가 AES128 암호화 키라는 겁니다.

체크섬이 E390612A7D316A91D1BC5E6545EC73890D221082 라고 하면 앞 8자 "E390612A"의
UTF-16LE 인코딩 형태가 키가 됩니다. HEX로는 다음과 같습니다.

$ echo E390612A|iconv -f utf8 -t utf16le| od -t x1
0000000 45 00 33 00 39 00 30 00 36 00 31 00 32 00 41 00



2014년 1월 4일 오후 8:37, Changwoo Ryu <cw...@debian.org>님이 작성:
Reply all
Reply to author
Forward
0 new messages