[리디북스] 자신이 소유한 책 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. ll says:

    Yes24나 알라딘같은곳은 DRM제거 못하나요?
    Yes24 주로 쓰는데 불편해요 ㅠㅠ

  2. qq says:

    그리고 이렇게 공개되었으니 조만간 막히겠군요.
    고맙습니다.

  3. says:

    안녕하세요? 그대로 따라했는데 진행은 되는것 되는거 같은데 라이브러리 폴더내에서 DRM이 해제된 파일을 찾을 수가 없네요. 혹시 어디에 생성되는지 알 수 있을까요?

    • Cai says:

      DRM 해제되는 파일들이 저장되는 폴더 지정은 두번째 인자를 통해 하실 수 있습니다. (글의 예제에서는 /tmp 폴더에 생성 됩니다.)

  4. roads says:

    100라인에 syntax오류가 나오는 이유는 무었일까요??

  5. 키미태 says:

    아.. 리디북스 DRM 해제하는법이 있다니.. 근데 프로그래밍 잘 모르는 윈도우 유저에겐 몇번을 읽어도 어렵네요 ㅠ

  6. ㅇㅇㅇ says:

    크레마 복호화도 가능할까요? 크레마는 꼼꼼하던데 ㅋㅋ

  7. Choryu Park says:

    Amazon은 Adobe Digital Editions가 아닌 자체 DRM을 사용합니다. Google Play Books 가 Adobe Digital Editions를 사용하구요. 둘 다 DeDRM 이라는 툴을 통해 복호화가 가능합니다.

    하지만 국내 모든 서점은 다 자체 DRM을 사용하고 있는데, Yes24, 알라딘, 교보문고 등은 한국이퍼브의 솔루션으로 서점이 구축되어 있어 (제가 알기론, 최소한 Yes24는) Markany X-Safe 를 사용하는걸로 알고 있고, 리디북스는 따로 자체 구현한 DRM을 사용하고 있습니다.

    • Choryu Park says:

      앗, Markany X-Sync 입니다

    • Cai says:

      앗, 그렇네요. 워낙 대충 찾아본지라, 제가 혼동했나봅니다 ^^; E-book DRM 현황에 대해서 많이 알고 계시네요! 오류 지적 감사드립니다.

    • Choryu Park says:

      으아아아, 교보는 한국이퍼브 계열이 아니네요. Fasoo DRM을 사용합니다. 반디앤루니스가 한국이퍼브 계열이구요. 잘못된 정보를 전해드려 죄송합니다 ㅠㅠ.

  8. Choryu Park says:

    여담인데, 최소한 리디북스 안드로이드 앱은 MITM 공격 방어가 되어 있는지, 가짜 루트 인증서를 안드로이드에게 신뢰 시키고 패킷 검사를 해보려고 하니 요청에 에러를 띄우더군요. 지식이 부족해 더 해보지는 못 했지만요.

    • Choryu Park says:

      아, 그리고 디바이스 고유 ID는 리디북스 웹 사이트에서 기기 관리 탭의 소스에 그대로 나와 있습니다. 안드로이드는 UUID가 평문이던데 윈도는 암호화 되어 있어서 뭔지 몰랐는데 이제서야 알게 됐네요.

    • Choryu Park says:

      음… 지금 안드로이드 앱을 보니 데스크탑에 적용된 일명 SimpleCrypt 부분은 실장되어 있지 않는듯 하네요. 암호화 해제 루틴은 이 글과 동일하나 UUID가 평문이고 .dat 파일이 그냥 AES 암호화만 적용된걸로 보입니다. 아마도…

  9. BHL says:

    죄송합니다.
    ENC_DEVICE_ID를 보려면 윈도우뷰어에서는 어디를 봐야하는지 부탁좀 드리겠습니다.
    찾을수가 없네요 ㅠ

    • Cai says:

      안녕하세요. 윈도우는 제가 테스트해보지 않아서 잘 모르겠습니다. 글에 쓰여진대로 더 이상 추가 분석이나 지원은 할 계획이 없어서 도와드리기 어려운 점 양해부탁드립니다 ㅠㅠ. 윈도우에서도 QSettings를 사용하고 있다면, 아마 레지스트리에 있을 확률이 큽니다.

      • mikeKim says:

        윈도우 레지스트리 HKEY_CURRENT_USER/Software/RIDI/Ridibooks/device 에 DEVICE_ID가 있습니다. LIBRARY_PATH 는 C:\Users\{$USERID}\AppData\Local\RIDI\Ridibooks\tensun\library 입니다. 그런데 잘 안되네요.

    • Choryu Park says:

      레지스트리를 검색하시면 나옵니다.

      • kai says:

        윈도우 레지스트리에서 Computer\HKEY_CURRENT_USER\Software\RIDI\Ridibooks에 있는 readervalue가 device_id인가요?

  10. Sangyong Shin says:

    감사합니다. 덕분에 파이썬 첫 경험 해봤어요~
    assert 구문은 이해가 안가 주석처리하고 돌렸는데 다행히 잘 되었습니다.

  11. 문의드립니다. says:

    작성자님 라이브러리 경로, 디바이스 아이디 찾아서 설정해 놨고 실행도 되는 것 같습니다.
    근데 다른 건 안건드리고 돌리기만 하면 라이브러리 내의 이북 파일들이 자동으로 drm 해제 되는 건가요? 그리고 풀린 파일은 경로가 어떻게 되나요?

  12. Knusper says:

    안녕하세요. 좋은 자료 감사합니다.
    제가 프로그래밍을 잘 몰라서 그러는데, ENC_DEVICE_ID를 어떻게 확인하는지 자세히 여쭤봐도 될까요?
    맥에서 Ridibooks.plist 열어보았는데 복잡한 글들이 마구 섞여있어서 정확히 어떤걸 복사해서 넣어야 할지 잘 모르겠습니다.

    감사합니다.

  13. Azur says:

    감사합니다.

  14. sky says:

    음…. 100번째 라인
    print ‘{} ‘.format(program) 에서 뒷부분 ‘ 에서 syntax 오류가뜬다고 하는데 왜그러는 걸까요 ~

    좋은 정보 감사드립니다~

    • sky says:

      100번째 라인 print ‘{} ‘.format(program) 에서 syntax 오류가뜬다고 하는데 왜그러는 걸까요 ~
      화살표는 뒤에 ‘ 를 가르키고 있긴한데….

    • sky says:

      어라 잘리네요 ㅠㅠ

      위에 100번째 라인 문장입니다~

  15. 멍충이 says:

    파이썬 공부중인 학생인데요. 파이썬 관련 질문드립니다.
    위 코드를 파이썬3에서 실행해보려고 살짝 수정해서 해보려는데 잘 안되서요 ㅠㅠ
    base64.b64decode(ENC_DEVICE_ID)가 파이썬2랑으 다르게 3에선 bytes로 나와서 chr로 바꿔서 넘겼더니 이부분은 어떻게 넘어간거 같은데

    57번라인 out = cipher.decrypt(data)에서 에러가 나네요 ㅠㅠ
    이 위에서 key_file도 chr로 바꿔서 넘겼는데 data의 값이 다르게 나오는거 같은데 어떻게 해야될지 모르겠어서요…

    파이썬 공부중이라 해결법 이라던가 왜 이렇게 차이가 나는지 알고 싶어서 글남깁니다.

Leave a Reply

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