Messenger Forensic
디지털 포렌식에서 메신저 아티팩트 분석은 사용자가 특정 메신저에서 어떤 정보를 주고받았는지 확인할 수 있기 때문에 중요한 분석 과정으로 여겨진다.
본글은 Kakao사의 KakaoTalk 메신저를 대상으로 대화 삭제 정책을 확인하고, 이를 토대로 효율적인 복구 정책을 정의한다. 이후에 복구 정책을 토대로 실제 아티팩트 분석을 진행해 정보 추출 범위를 확인한다.
Scenario
본 실습은 Rooting이 진행된 Android의 S7(SM-G930S) 기기에서 진행하며, 메신저 환경은 카카오톡 메신저에서 제공하는 대화 내용 삭제 방법의 모든 경우의 수를 적용해 재현한다.
먼저 카카오톡 메신저에서 제공하는 대화 내용 삭제는 세가지 방법이 존재한다. 첫 번째 방법은 '모든 대화 상대에게서 삭제 (5분 이내)', 두 번째 방법은 '이 기기에서 삭제 (5분 이내)', 세 번째 방법은 '삭제 (5분 이후)'이다. 추가적으로, 세 가지 각각의 삭제 방법마다 '본인의 대화를 본인이 삭제', '본인의 대화를 상대가 삭제', '상대의 대화를 본인이 삭제', '상대의 대화를 상대가 삭제'로 총 네 가지 경우의 수가 추가로 존재한다. 때문에, 본 실습은 카카오톡에서 제공하는 세 가지 삭제 방법과 각 방법마다 삭제 가능한 경우의 수를 고려하여 총 열두 가지 경우의 수의 삭제 시나리오를 진행해 메신저 사용환경을 재현한다.
아래는 구체적인 시나리오 정보를 정리한 표이다.
Text | Send & Receive Time | Delete Option | Delete Time |
Send 1-1 | 12:35 | 모든 대화 상대에게서 삭제 (5분 이내) (본인) | 12:39 |
Receive 1-1 | 12:35 | 모든 대화 상대에게서 삭제 (5분 이내) (상대) | 12:39 |
Send 1-2 | 12:35 | 삭제 (5분 이내) (상대) | 12:39 |
Receive 1-2 | 12:35 | 삭제 (5분 이내) (본인) | 12:39 |
Send 2-1 | 12:36 | 이 기기에서 삭제 (5분 이내) (본인) | 12:40 |
Receive 2-1 | 12:36 | 이 기기에서 삭제 (5분 이내) (상대) | 12:40 |
Send 2-2 | 12:36 | 삭제 (5분 이내) (상대) | 12:40 |
Receive 2-2 | 12:36 | 삭제 (5분 이내) (본인) | 12:40 |
Send 3-1 | 12:37 | 삭제 (5분 이후) (본인) | 12:43 |
Receive 3-1 | 12:37 | 삭제 (5분 이후) (상대) | 12:43 |
Send 3-2 | 12:37 | 삭제 (5분 이후) (상대) | 12:43 |
Receive 3-2 | 12:37 | 삭제 (5분 이후) (본인) | 12:43 |
Send 4-1 | 12:44 | X | X |
Receive 4-1 | 12:44 | X | X |
아래는 시나리오 중 '대화 내용 삭제 이벤트 이전 (12:38)', '모든 시나리오 수행 이후 (12:45)' 시점의 카카오톡 화면을 캡처한 모습이다.

KakaoTalk DB Decryption
KakaoTalk 메신저는 각 사용자마다 고유 값인 ‘user_id’를 부여해, 각 기기의 SQLite를 암호화(PKCS #12, AES-256-CBC)한다. 구체적인 암호화 알고리즘은 리버스 엔지니어링을 통해 파악이 가능하고, 암호화 알고리즘으로 복호화 알고리즘을 역추적할 수 있다. 본 글은 KakaoTalk 메신저를 디지털 포렌식의 관점에서 언급하고, 리버스 엔지니어링에 관한 내용은 다루지 않겠다.
KakaoTalk 메신저는 모바일 디바이스에서 SQLite를 암호화할 때, 대화를 관리하는 Table(chat_logs)과 대화방을 관리하는 Table(chat_rooms)에서 특정 Column의 정보(message, attachment)만 암호화한다.
KakaoTalk의 SQLite 복호화는 Github에 공개된 코드가 존재하고 실행 방법은 다음과 같다. 먼저 'guess_user_id.py' 코드를 실행해 'KakaoTalk.db' 파일에서 'user_id'를 추출한다.

이후에 'kakaodecrypt.py' 코드를 'user_id' 인자로 실행해 SQLite의 암호화된 Cell을 복호화한다.

아래는 KakaoTalk의 SQLite 복호화 Github 링크이다.
https://github.com/jiru/kakaodecrypt/tree/master
GitHub - jiru/kakaodecrypt: Decrypt chat history from the local database of Kakaotalk’s Android app
Decrypt chat history from the local database of Kakaotalk’s Android app - jiru/kakaodecrypt
github.com
KakaoTalk DB Analysis
KakaoTalk의 SQLite는 디지털 포렌식의 관점에서 두 가지 Table을 핵심적으로 분석해야 한다. 첫 번째 Table은 대화 정보를 관리하는 'chat_logs' Table이고, 두 번째 Table은 대화방 정보를 관리하는 'chat_rooms' Table이다.
아래는 'chat_logs' Table을 DB Browser로 열어본 모습과 중요한 Column의 정보를 정리한 표이다.


Name | Info | Deletion Policy (zerofill) |
id | 대화 고유 ID | X |
type | 대화 데이터 타입 (0: 삭제) (1: 텍스트) (2: 사진) (3: 영상) (16385: 모든 대화 상대에게서 삭제) | X |
chat_id | 대화방 고유 ID | X |
user_id | 사용자 고유 ID | X |
message | 대화 내용 | O |
attachment | 대화 내용 데이터 (사진, 영상: URL) | O |
created_at | 대화 전송 시간 | X |
deleted_at | 대화 삭제 시간 | - |
prev_id | 이전 대화 ID | X |
v | 대화 메타 데이터 (대화 작성자 정보: WRITE(본인), MSG: (상대방)) (대화 생성 시간) | X |
아래는 'chat_rooms' Table을 DB Browser로 열어본 모습과 중요한 Column의 Schema 정보를 정리한 표이다.

Name | Info |
id | 대화방 고유 ID |
type | 대화방 타입 |
members | 대화방 사용자 ID |
last_message | 대화방의 마지막 대화 |
last_updated_at | 대화방의 마지막 대화 생성 시간 |
KakaoTalk Deletion Policy
KakaoTalk의 삭제된 대화 내용 복구 과정을 정의하기 위해 먼저 대화 내용 삭제 정책을 확인한다. 대화 내용 삭제 정책은 두 가지가 존재하며, 각각 '모든 대화 상대에게서 삭제 (5분 이내)'인 경우와 '이 기기에서 삭제 (5분 이내)', '삭제 (5분 이후)'인 경우로 나뉜다.
모든 대화 상대에게서 삭제 (5분 이내) 정책: 삭제 대상 Record의 Type 값을 '16385'으로 변경하고, 새로운 Record를 생성한다. 새롭게 생성한 Record의 message Cell에 삭제 대상 Record의 id값을 기록해 해당 Record를 포인팅 하고, 'hidden:true'를 기록해 삭제 대상의 Record를 볼 수 없게 한다. 추가적으로 삭제 대상 Record를 제외하고, 새롭게 생성된 Record만 deleted_at Cell에 삭제 시간을 기록한다.

이 기기에서 삭제 (5분 이내), 삭제 (5분 이후) 정책: 삭제 대상 Record의 message, attachment Cell을 zerofill 하고, deleted_at Cell에 삭제 이벤트 시간을 기록한다.
KakaoTalk Recovery Policy
본 문단의 대화 삭제 복구 분석은 삭제 이벤트 이후의 이미지 파일을 기점으로 하고, KakaoTalk 대화 삭제 정책을 기반으로 대화 복구 흐름도를 정의한다. 각 순서 번호의 설명은 아래와 같다.
1. SQLite, WAL 파일 추출.
- SQLite, WAL 파일 추출. WAL 파일의 경우 Inode Number 확보.
2. WAL 파일의 File Version 확보.
- WAL 파일의 Inode Number로 Inode Entry 접근 후 File Version 확보.
3. 수정 혹은 삭제된 모든 WAL 파일 추출.
- Ext4의 journal에서 WAL 파일의 File Version 검색, 수정 혹은 삭제된 모든 Inode Entry 확보. 해당 Inode Entry로 비할당 영역에 존재하는 모든 WAL 파일 추출.
4. SQLite의 복원이 필요한 Leaf Page 번호 확보.
- SQLite에서 복원이 필요한 Table의 Schema 검색, Schema Table 접근 후 해당 Table이 관리하는 Root Page Number 확보. 해당 Root Page Number로 모든 Leaf Page Number 확보.
5. WAL 파일에서 복원에 필요한 Backup Page 추출.
- WAL 파일에서 복원에 필요한 Page Number 검색, 검색된 Frame 중 Page Header의 Number of Record 값이 복원하려는 여러 Record 중 가장 마지막 Record 번호와 동일한 Backup Page 추출.
6. SQLite의 Page 복원.
- 추출된 Backup Page를 SQLite의 복원해야 하는 Page 영역에 덮어씀(복원).
KakaoTalk DB Recovery
본 문단은 이전에 제시한 효율적인 복구 정책을 기반으로, 실제 분석을 진행해 삭제된 대화를 복원한다.
1. SQLite, WAL 파일 추출.
- SQLite 파일과 WAL 파일을 디스크 이미지 파일에서 추출한다.
- SQLite 파일의 경우 shm 파일과 wal 파일을 추출해 SQLite 파일의 트랜잭션을 커밋한다.
- WAL 파일의 경우 Inode Number를 확보한다.
아래는 실제 SQLite 파일과 wal 파일의 모습이다.
File Name | Inode Number |
KakaoTalk.db | 525,702 |
KakaoTalk.db-wal | 525,818 |


2. WAL 파일의 File Version 확보.
- WAL 파일의 Inode Number로 Inode Entry 접근해 File Version 확보한다.
아래는 실제 wal 파일의 Inode Entry이다. File Size가 존재하고, Deletion Time이 존재하지 않는 모습으로, 이는 wal 파일이 파일 시스템의 할당영역에 존재하기 때문에 올바른 결과이다. 특징이라면 Valid Extent Entry가 4개 존재하고, File Version은 '0xFF AF C1 78'이다.

3. 수정 혹은 삭제된 모든 WAL 파일 추출.
- Ext4의 journal에서 WAL 파일의 File Version을 검색해 수정 혹은 삭제된 모든 Inode Entry 확보하고, 비할당 영역에 존재하는 모든 WAL 파일을 추출한다.
아래는 실제 Ext4의 journal에서 검색한 wal 파일의 Inode Entry이다. 총 150개의 Inode Entry가 검색되었고, 첫 번째 Inode Entry부터 150번 Inode Entry까지 전부 확인한 결과 파일의 삭제 이벤트 (File Size == 0, Deletion Time != 0), 데이터 축소 (Vaild Extent Entry가 줄어듦) 이벤트, 데이터 할당 영역 변경 (Extent Entry의 Data Block 변경) 이벤트는 존재하지 않았다. 150번의 Inode Entry 모두 Vaild Entent Entry가 증가하거나, Extent Entry의 Data Block Length가 증가하였다. 즉, Ext4 파일 시스템의 할당 영역에서 관리하는 현재의 WAL 파일이 디스크의 유일한 WAL 파일임을 확인했다.

4. SQLite의 복원이 필요한 모든 Leaf Page 번호 확보.
- SQLite에서 복원하려는 Record를 포함하는 Table을 결정하고, 해당 Table의 Schema 정보를 검색해 Root Page Number를 확보한다.
- 이후에 Root Page에서 모든 Leaf Page를 확보하고, 복원하려는 유효한 Record 값이 가장 많이 존재하는 가장 마지막 Record 번호를 계산한다.
아래는 shm, wal 파일 커밋이 완료된 SQLite 파일을 복호화하고, DB Browser로 열어본모습이다. SQLite 파일에서 복구가 필요한 Table의 Schema 정보를 검색해, 해당 Table이 관리하는 Root Page Number를 확보한다. 이후에 Root Page Number 부터 각 Page를 순회하며 실제 데이터를 저장하는 Leaf Page Number를 모두 확보한다.

아래는 SQLite 파일에서 복원이 필요한 Table의 Leaf Page Number의 모습이다. 총 2개의 Page를 확보하고, 25번 Page는 1~10번 대화를, 26번 Page는 11~17번 대화를 관리하는 Page임을 확인할 수 있다.
25번 Page에서 복원이 필요한 Record, 다시 말해 1~10번 대화 중 복원이 필요한 Record는 5, 6, 9, 10번 Record가 되기 때문에 복원하려는 여러 Record 중 가장 마지막 Record 번호는 10번 Record가 된다. 26번 Page에서 복원이 필요한 Record, 다시 말해 11~17번 대화 중 복원이 필요한 Record는 3번 Record가 되기 때문에 복원하려는 여러 Record 중 가장 마지막 Record 번호는 3번 Record가 된다.


5. WAL 파일에서 복원에 필요한 Backup Page 추출.
- WAL 파일에서 복원에 필요한 Page 번호를 검색해 Backup Page를 저장하는 Frame을 확보한다.
- 확보한 Frame 중 Page Header의 Number of Record 값이 복원하려는 여러 Record 중 가장 마지막 Record 번호와 동일한 Backup Page 추출.
해당 과정을 진행하기 전에 알아야 할 정보가 두 가지 있다. 첫 번째는 Frame Header의 0x00~03 Field는 Backup Page Number 정보를 저장한다는 점이고, 두 번째는 Hex Editor 특성상 첫 번째 Page인 시작 Page를 0으로 표시하기 때문에 Hex Editor에 보이는 'X'번째 Page는 실제로 'X + 1'번째 Page라는 점이다.
WAL 파일에서 '0x00 00 00 1A'(26)를 검색해 25번 Page를 Backup 한 Frame을 검색하고, 검색된 결과가 Frame Header의 0x00~03 Field에 존재하는지 확인한다. WAL 파일에서 총 7개의 '0x00 00 00 1A'이 검색되었다. 그중 검색된 결과가 Frame Header의 0x00~03 Field에 존재하고, Backup Page의 Number of Record 값이 복원하려는 여러 Record 중 가장 마지막 Record 번호인 10(0x0A)을 갖는 Backup Page를 추출한다. 아래는 해당 조건을 만족하는 Frame의 모습이다.
사진으로는 message Cell('==')을 포함하는 Record를 8개밖에 담지 못했지만, 실제로 Backup Page 영역에 총 9개의 message Cell('==')이 발견되었다. 이는 해당 Backup Page가 10개의 Record를 모두 포함하고 있음을 의미한다. (첫 번째 Record 제외)



해당 Backup Page를 SQLite의 25번 Page에 복원하기 위해 Backup Page의 시작 주소부터 Page Size(0x1000)만큼을 복사한다.

WAL 파일에서 '0x00 00 00 1B'(27)를 검색해 26번 Page를 Backup 한 Frame을 검색하고, 검색된 결과가 Frame Header의 0x00~03 Field에 존재하는지 확인한다. WAL 파일에서 총 58개의 '0x00 00 00 1B'이 검색되었다. 그중 검색된 결과가 Frame Header의 0x00~03 Field에 존재하고, Backup Page의 Number of Record 값이 복원하려는 여러 Record 중 가장 마지막 Record 번호인 3(0x03)을 갖는 Backup Page를 추출한다. 아래는 해당 조건을 만족하는 Frame의 모습이다.
아래는 Frame를 캡처한 모습으로 Backup Page에 message Cell('==')을 포함하는 Record가 3개 모두 존재함을 확인할 수 있다.


해당 Backup Page를 SQLite의 26번 Page에 복원하기 위해 Backup Page의 시작 주소부터 Page Size(0x1000)만큼을 복사한다.

6. SQLite의 Page 복원.
- 추출(복사)한 Backup Page를 SQLite의 복원해야 하는 Page 영역에 덮어쓴다.
SQLite의 25번 Page에 25번 Backup Page를 덮어쓴다.

SQLite의 26번 Page에 26번 Backup Page를 덮어쓴다.

아래는 Page 복원이 완료된 SQLite 파일을 DB Browser로 열어본 결과로, 삭제된 대화가 정상적으로 복원되었음을 확인할 수 있다.

아래는 Page 복원이 완료된 SQLite 파일을 복호화하고, DB Browser로 열어본 결과이다. 삭제된 대화가 올바른 내용으로 복원되었음을 확인할 수 있다.

KakaoTalk DB Recovery Result
결과적으로 KakaoTalk의 '모든 대화 상대에게서 삭제 (5분 이내)'는 Secure Delete가 적용되지 않기 때문에 SQLite 파일 자체에서 삭제된 대화 내용을 복원할 수 있다. 하지만 '이 기기에서 삭제 (5분 이내)', '삭제 (5분 이후)'의 경우 Secure Delete가 적용되어 SQLite 파일 자체에서 삭제된 대화를 복원할 수 없고, WAL 파일을 이용해 복원할 수 있다. 이 경우 WAL 파일의 Check Point가 발생하고, 복원에 필요한 WAL 파일이 파일 시스템의 비할당영역에서 zerofill 된다면 복구할 수 없게 된다.
아래는 KakaoTalk에서 제공하는 삭제 방법에 따른 Secure Delete 여부, SQLite 복구 가능 여부, WAL 파일 복구 가능 여부를 정리한 표이다.
삭제 타입 | Secure Delete | SQLite | WAL |
모든 대화 상대에게서 삭제 (5분 이내) | X | O | O |
이 기기에서 삭제 (5분 이내) | O | X | O |
삭제 (5분 이후) | O | X | O |
Reference
https://www.dbpia.co.kr/journal/detail?nodeId=T15015734
https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE08736972