[리디북스] 자신이 소유한 책 DRM 해제하기 (feat. 비밀번호 노출)

요즘 일로 너무 바쁜 나날을 보내고 있는데, 더이상 글을 안쓰면 1년을 넘길 것 같아서 뭐라도 투척하자는 마음으로 적는 글이다보니 기술적으로 깊게 짚고 넘어가기 보다는 간단하게 생존 신고 하는 용도의 포스팅을 하려 한다. 최근 1시간 정도 스트레스를 풀기 위해 작업한 사이드 프로젝트에 대해서 설명하고 그 과정에서 발견한 것들과 산출물(?)을 공유한다.

얼마전 개발자들의 모임 공간인 ‘이상한모임’ 슬랙의 한 채널에서 일어난 대화를 보고 문득 궁금증이 생겨 작업에 임했다.

[…] 리디북스와 아마존 킨들을 둘 다 사용하는데, 리디에서 산 책을 아마존 기기로 옮길 수가 없어서 불편해요 […]

부끄럽지만(…) 나는 어릴때부터 책 읽는 것을 무척이나 싫어했다 — 아 물론, 만화책과 추리소설은 꽤 좋아했다. 독서는 습관인지, 나는 여전히 책 읽는데에 관심이 없다. 그래서 많이들 가지고 있는 이북리더 (..김정은 e-book reader) 또한 사용해본적이 없고, 당연히 그에 따른 편함이나 불편함은 겪어본 경험이 없다. 그런데 위의 대화에 관심을 가지게 된 궁극적인 계기는 바로 그 불편함의 근원이 DRM이라는 것이었다.

그래서 잠깐 구글링을 통해 e-book에서 사용되는 포맷들과 각종 벤더들과 지원하는 리더들에 대해서 찾아봤다. 자세히 찾아본 것은 아닌지라 얼마나 정확한지는 모르겠으나, 보통 아마존을 통해 유통되고 있는 전자책들은 Adobe Digital Editions라는 것을 통해 DRM 설정이 되어있고, 같은 프로그램을 사용하여 본인이 소유한 컨텐츠라면 DRM을 해제하는 것이 가능해보였다. 다만, 국내 최대 전자책 유통사인 리디북스의 경우에는 자체적인 DRM 솔루션을 만들어 사용하고 있어 보였다. 그런 이유로 리디북스를 통해 구입한 컨텐츠를 자신의 다른 기기에 옮기는 것이 사실상 불가능했던 것이다.

그리하여, 리디북스의 리더를 분석해보기로 마음 먹는다.

 

리디북스 뷰어 구하기

리디북스 뷰어 다운로드

리디북스 뷰어 다운로드

리더 프로그램을 구하는것은 그리 어렵지 않았다. 친절하게도 비로그인 유저에게도 다운로드 링크를 제공하고, 모바일과 PC 버전도 모두 지원하기 때문에 오히려 어떤 녀석으로 할까 고르는게 더 어려웠다 (결정장애). 물론, 나중에 테스트를 하기 위해서는 계정을 하나 만들긴 해야했지만.. 그건 성공한 후의 일이고, 우선 프로젝트의 난이도를 파악하는게 먼저였다.

난 맥 사용자이므로 테스트도 수월하게 할 겸 맥용 프로그램을 받아서 분석을 시작했다.

 

리디북스뷰어 분석하기

앞서 말했듯이, 이번 글의 목표는 기술적인 분석보다는 같은 불편함을 가진 사람들이 좀 더 편하게 자신 소유의 컨텐츠를 관리할 수 있게 함이기에 자세한 내용은 적지 않겠다. 아래 DRM 해제 코드를 보면 어떤 식으로 DRM이 적용되고 관리되어 지는지 자명할 것이다.

우선 리디북스뷰어는 Qt 프레임워크를 사용하여 cross-platform을 목적으로 만들어져 있어서 운영체제에 상관 없이 코드베이스가 거의 비슷하다. 물론 이 말은 C++로 작성되어 있기에 분석하기에는 조금 껄끄럽다는 점이 있지만, 심볼과 함께라면 두려울 것이 없다! 리디북스에서 지원하는 컨텐츠 타입은 크게 세 가지이다: PDF, EPUB, 그리고 ZIP. 여기에서, ZIP 형태로 된 파일은 보통 만화책을 담을 때 사용된다.

분석을 진행하며 한 가지 흥미로웠던 점은 각 파일 형식마다 복호화 키를 만들어내는 방식과 사용하는 알고리즘이 조금씩 다르다는 점이었다. 아마 각 파일 형식을 맡은 개발자나 개발팀이 달랐던 듯 싶다. 또한, 아래에서 언급되는 설정 값들은 운영체제 마다 저장되는 공간이 다른데, 코드 내부적으로는 QSettings 클래스를 사용하므로 기본적으로 설정 파일이 저장되는 공간이 정해져있다.

공통

어떤 형식이든 DRM 복호화를 위해서는 기기 고유의 값인 device_id를 사용한다. 이 값은 설정 파일에 존재하는데, 보안상의 이유로 암호화된 후 base64 인코딩 되어 저장되어 있다 — device.device_id 항목. 이 값을 비롯한 몇 가지 보안에 민감한 다른 설정 값들도 암호화 되어 있는데, 이 부분은 SimpleCrypt라는 녀석을 통해 구현했다. SimpleCrypt는 아래 DRM 해제 코드에도 (python으로 재구현한 버전) 포함되어있는데, 사실 아주 간단한 xor 인코딩이다.

클래스 생성 시 64-bit 정수형의 key를 받은 뒤, key split이라는 작업을 통해 해당 키를 key parts라고 불리는 조각들 8개로 나눈다. 그리고, 이 key parts들을 사용하여 암호화 또는 복호화를 진행한다. 독자가 예상했을 수 있겠지만, 여기에서 사용되는 “key” 값은 고정된 값이다. 좀 더 명시하자면, 특정 유저나 기기에 고정된 값이 아니라 모든 리디북스 유저/기기에게 고정된 값이다. 이로 인해 발생하는 보안 위협은 아래 부록 섹션에서 설명하도록 한다.

복호화 된 device_id는 UUID 형태의 문자열이다. 사실 이 값이 생성되는 방법과 값은 운영체제와 기기 마다 다르기 때문에 큰 의미는 없다. 단지 로그인 시점에서 리디북스의 유저 계정과 해당 device_id를 연결하고, 이 device_id를 토대로 생성된 암호키를 이용하여 암/복호화에 사용한다 — 코드에선 dec_key라고 불린다.

다운 받아진 컨텐츠 파일들이 존재하는 책장 (library) 디렉토리로 가보면, 암호화 되어있는 PDF/EPUB/ZIP 파일 말고도 .dat 파일이 존재한다. 이 .dat 파일은 각 컨텐츠마다 다른 암호키 (content_key)가 암호화 되어 저장된 파일이다. 이 파일은 위에서 계산한 dec_key를 SimpleCrypt의 키로 사용하여 복호화 할 수 있다. 이렇게 복호화 해서 나온 데이터는 한 번 더 암호화가 되어있는데, device_id의 첫 16바이트를 키로 이용하는 AES-128-ECB 알고리즘으로 암호화 되어있다.

복호화의 연속을 거치고 나면 결과적으로 어떤 hex 문자열이 나오는데, 그 중 “일부” 16바이트가 각각의 컨텐츠를 복호화하는데 쓰이는 content_key가 된다. 즉, 이 부분은 컨텐츠마다 다르다. 아래에서 언급하겠지만, ZIP 형식에서는 .dat 키 파일과 상관없이 device_id 만으로부터 content_key를 도출하여 사용한다 (????).

PDF 형식

위에서 나온 content_key를 AES-128-CBC 알고리즘의 키로 사용하여 복호화 한다. 이 때 사용되는 IV (initialization vector)는 널바이트 16개로 구성된 문자열이다. 복호화 한 후, 첫 16바이트는 hash 같아보이는 데이터로 실제 컨텐츠 데이터가 아니다. 잘라내고 읽으면 원본 컨텐츠 복호화 완료.

EPUB 형식

위에서 나온 content_key를 AES-128-ECB 알고리즘의 키로 사용하여 복호화 한다. 다른 메타데이터는 없어보이며, 그대로 복호화 된 데이터를 읽으면 원본 컨텐츠 복호화 완료.

ZIP (만화책) 형식

위에서 나온 con… 전혀 content_key와는 관련없이 device_id 문자열의 부분을 16 바이트만큼 읽어서 키로 사용한다. 다만, ZIP 파일 자체가 암호화가 되어있는 형식이 아니라 압축을 풀면 나오는 각각의 파일을 AES-128-ECB로 복호화 해주면 된다. 그리고, 다시 파일들을 ZIP으로 묶어주면 원본 컨텐츠 복호화 완료.

 

리디북스 DRM 해제하기

분석한 내용을 따라 DRM을 해제하는 스크립트를 작성하는 것은 어렵지 않았다. 각 형식에 맞춰서 파싱한 후, 복호화 작업을 해주고, 원하는 곳에 저장하면 끝이다. 최대한 Python 기본 내장 라이브러리들로만 구성하여서 작성하려고 노력했으나, AES 코드를 붙여넣기엔 가독성이 떨어져서 pycrypto 패키지 의존성이 하나 있다. 이는 pip install pycrypto 를 실행하면 해결된다.

 

사용하기전에 바꿔야할 부분은 크게 두 곳이다.

  1. ENC_DEVICE_ID – base64로 되어있는 device_id 를 넣어야 하는데, 이건 macOS 기준 /Users/<username>/Library/Preferences/com.ridibooks.Ridibooks.plist 열어서 확인하면 된다.
  2. LIBRARY_PATH – 리디북스 앱 환경설정에 적혀있는 책장 경로이다. macOS 기준 디폴트 /Users/<username>/Library/Application Support/RIDI/Ridibooks/<ridi_username>/library/ 이다.

 

원래 5명 내외의 개발자들에게만 편의를 위해 제공한 툴이라, 추가적인 사용법에 대한 질문은 따로 받지 않도록 하겠다. 언젠가 시간적으로 여유가 생기거나 하면 보통 사용자들도 이용하기 편한 macOS/Windows 앱으로 만들지도 모르겠지만, 아마도 그런 날은 오지 않겠지 ㅠㅠ.. (누군가가 나서서 해주셔도 됩니다ㅎㅎ)

 

테스트 결과 및 마치며…

리디북스가 지원하는 세 가지 형식에 대하여 샘플링해서 테스트 해본 결과, 모두 이상 없이 DRM이 해제되어 복호화 되었고 다른 유틸리티를 통해 해당 파일들을 불러들여올 수 있었다.

이 프로젝트는 기본적으로 “내가 돈 주고 산 물건에 대해서는 내 소유다”라는 아주 기본적이고도 직관적인 목표를 가지고 진행됐다. 간단하게 코드만 올려놔야지 했던 생각으로 글을 시작해서 결과적으로 분석+코딩한 시간 보다 더 긴 시간 동안 글을 작성하게 되어 당혹스럽다…

악용될 소지가 없지 않기에, 사용에 따른 법적 문제나 도의적 책임은 사용자 본인에게 있음을 알리며 글을 마친다.

 

부록. 위험에 처한 내 비밀번호 ㅠㅠ

위에서 언급된 설정 파일을 살펴보면 device.device_id 말고도 다른 흥미로운 데이터들이 (암호화 되어) 저장되어 있다. 그 중 단연 돋보이는 것은 account.password와 account.id 이다. 시스템에 고스란히 저장되어 있는 내 비밀번호.. 암호화 되어있지만 어떻게 암호화 되어 있으며 안전할까?

대답은 ‘아니오’ 였다.

device_id를 복호화 하는데 사용된 같은 “글로벌 공통 키”가 시스템에 저장되는 비밀번호에도 그대로 사용되고 있었던 것이다. 물론, 사용자의 시스템을 먼저 장악해야하기 때문에 큰 위협이 아니라고 할 수 있겠지만, 비밀번호의 hash 값이 아닌 실제 비밀번호 데이터가 복호화 가능한 형태로 들어가 있는 것은 꽤나 보안적으로 치명적인 디자인 결함이다.

이런식으로 저장되어 있는데에는 이유가 있다. 바로 “자동 로그인” 기능을 지원하기 위한 것. 자동 로그인 시, 로그인 URL에 POST 리퀘스트를 보내서 세션쿠키를 받아오는데, 이 때 유저이름과 비밀번호가 전송되게 된다 — 다행히 TLS 위에서 전송되기 때문에 안전하다…(하지만 인증서 갈아치우기로 SSL도 DPI를 하는 회사 시나리오에서는…?? 굳바이 마이 패스워드).

리디북스가 고객의 개인정보, 특히 그 중에서도 매우 민감한 비밀번호 데이터의 보안에 신경 쓴다면 실제 비밀번호가 아닌 salted hash 또는 PBKDF2를 사용하여 사용자를 인증하기 바란다.

 

업데이트

  • 2018/05/18: 리디북스의 개발자 분으로부터 최근 비공개 커뮤니티 등에서 리디북스에서 유출된 콘텐츠가 유통되는 사례가 접수됨에 따라 악용될 수 있는 스크립트 부분은 내려달라는 정중한 요청을 받아 해당 코드 부분은 삭제 조치 하였습니다.

You may also like...

29 Responses

  1. Cai says:

    안녕하세요. 우선, 제 블로그에 방문해주셔서 감사합니다. 죄송스럽게도… 원글에 표기했듯이 툴 배포의 목적보다는 분석/가능성에 대한 시사점을 남기기 위한 요점의 글이었던 관계로 글 내용 이외의 추가적인 지원은 드리기 어렵습니다. 양해부탁드립니다.

Leave a Reply

Your email address will not be published. Required fields are marked *