국민대학교 Forensic 동아리 FaS 에서 활동하며 발표를 맡았던
TigerConnect App 관련 내용을 정리할 것이다.
사실 이 앱같은 경우 어디에서 데이터를 복호화해야하는지를 모두 찾았으나, 어떻게 해결하는 지 막막해 중도 포기하고
KDFS 2022 ( 디지털 포렌식 챌린지 ) 로 넘어갔다.
조금 시기가 지났지만 그래도 끝맺는게 맞는 것 같아 다시 파일을 찾아 돌아왔다.
정리를 시작해보자.
-> 추출 기간이 달라 다른 부분이 있음. 다시 수정해야함.
1. 내가 찾았던 부분

1) TigerConnext 란?
- 암호화, PIN 잠금 및 관리자 제어 기능으로 메시징
- 전화, 이미지 비디오 및 음성녹음
- 한눈에 보이는 역할(roles)
- 역할 별 담당 직원을 빠르게 찾고 텍스트 전송
- 교대조별 일정관리
2) App Data 추출 - Com.TigerText

나는 루팅 안드로이드 폰으로 USB 추출을 진행했다.
우선 추출에는 데이터베이스 추출과 APP APK 추출이 있는데,
두가지 추출의 목적은
데이터베이스 - 유저 정보, 앱에 있는 데이터 / 대화 내용 등 을 추출할 때 필요
APP APK - 데이터베이스가 암호화되어있어 이를 해제하기 위한 복호가 필요할 때 코드를 뜯어보기 위해 추출
[ 애플리케이션 내부 데이터 구조 확인 방법 ]
- 데이터 추출
과연 내부에 어떤 데이터가 있는지 유용한 데이터 확인 작업이 필요하다.
데이터베이스가 암호화 되어있더라도 , 앱 apk 를 뜯지 않고 sharedprefs 에 있는 파일에 복호 힌트가 들어있는 경우가 있기 때문에 먼저 진행해주는 것이 좋다.
사용도구) HxD , SQLCipher, Dcode

- 앱.apk 추출
데이터를 추출해봐도 특별히 복호화 내용이 없다면, 본격적으로 apk 를 뜯어봐야한다.
사용도구) JADX
https://github.com/skylot/jadx/releases/tag/v1.4.3
3) 암호화 되어있는 데이터베이스 복호화
우선 TIGERCONNECT 는 데이터가 암호화되어있다.
그렇기 때문에 앱 데이터를 열기 위한 복호화가 필요하고, 앞에서 말했듯이 이를 복호화하기 위해서는
APP.apk 파일을 추출해야한다.
추출 한 후 우리가 사용해야하는 프로그램은 뭐다?
JADX

- 암호화된 데이터베이스 이름 검색 - "ttandroid.db"

- AES 암호화 방식
복호화 하기 위해 AES 암호화 방식을 알면 좋겠다 . 생각해서 찾아봤던

- 암호/복호 함수 찾기
JAVA 의 암호 모듈은 대부분 "AES/ECB/PKCS5Padding" 을 쓰기 때문에
이 부분을 찾는 것이 핵심!

여기서 authToken 을 찾는 것이 중요하겠다.
이를 위해 getAuthToken() 함수를 들어가보면

this.h 를 return -> getAuthToken() 함수 값을 구하는 것이 우선

최종적으로 구해야하는 값. (return 값)
[ restKey : restSecret ] 형태를 띄고 있음.
* 본격적으로 코드를 뜯어보자.

1) getRestKey 함수로부터 restKey 를 구할 수 있음.
- shared_prefs 파일에 "tigertext_default" 파일을 찾는다.

- 해당 파일을 사용해 파일 안에 ttkey01 값을 찾으면 이와 같이 나온다.
- 이를 restKey 로 사용하겠다는 뜻.
restkey = "SW3haySjzOxSmFnbxtlKrpao324QIG1U "
2) getRestSecret 함수로부터 restSecret 을 구할 수 있음.
1-1) 사진을 다시 보면,
getSecureString(defaltpref, "ttkey02", "") 부분을 통해 이 함수를 들어갈 수 있다.

string == null 이냐? yes : no
이부분에서 string 값이 shared_prefs 파일 폴더 안에 있는 default 값 중, "ttkey02" 에 해당하는 것을 알 수 있다.
그렇다면, 당연히 string 값은 null 값이 아니게 되고
no 이기 때문에 this.f48586b.decrypt(string); 값을 선택하게 된다.

이 값을 decrypt 함수에 넣어야 하기 때문에, decrypt 함수를 찾아 가보자.

여기에서
restKey 값은 위에서 살펴봤듯이, getRestKey 함수에서 가져온 "ttkey01" 값이다.
그 밑에 또 살펴볼 부분은 doFinal 인데,
doFinal 의 대상은 str 을 Base64.dexode 로 변한 값이다.
(여기서 str 은 위에서 string 값과 같음)
즉, "ttkey02" 값을 base64.decode 한 것임.
2. 도움을 받아 해결한 부분
이 이후 부분은 석사과정 선배님 자료 도움을 받아 제작했습니다.
윗 사진에서 이어서 설명해보면,
getRestKey 함수로부터 얻은 restKey 값을 b 함수에 적용

d(str) -> d 함수에 restkey 값을 넣는 것이다.

여기서 encryptionSalt 값은 .
"tigertext_defalt" 파일에서 "encryption_salt" 값 이다.

encryption_salt = "UEsnJdFTro+PweDr/QqQKV2gU+mDLQCAIhFYgyCSA0M="
bArr2 = Base64.decode(EncryptionSalt,2) 이므로
bArr2 = "50 4b 27 25 d1 53 ae 8f 8f c1 e0 eb fd 0a 90 29 5d a0 53 e9 83 2d 00 80 22 11 58 83 20 92 03 43"
test -----
encryption_salt_byte = '21 80 8d f1 6b f8 55 76 cf 3b e0 de 77 11 56 4d 8c 75 c6 f7 5b 47 11 bb ce 5e 83 d3 ec cc 8c 91'
encryption_salt = 'IYCN8Wv4VXbPO+DedxFWTYx1xvdbRxG7zl6D0+zMjJE='
ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
ttkey01 = '73 ca ba eb 6c af e1 d7 11 6d b3 6c 89 cb ef 46 09 04 bc 71 d0 2d ef 28'

salt_decode = base64.b64decode(encryption_salt)

모두 디코드 해야하는 것을 깨닫고.. 몇번의 실행 결과 완성 !
최종 코드 정리
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from base64 import b64encode,b64decode
encryption_salt = 'IYCN8Wv4VXbPO+DedxFWTYx1xvdbRxG7zl6D0+zMjJE='
salt_decode = base64.b64decode(encryption_salt)
ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
ttkey01_decode = base64.b64decode(ttkey01)
secretkey = pbkdf2_hmac('sha1', password=ttkey01_decode, salt=salt_decode, iterations=1000, dklen=32)
print('secretkey : ', secretkey.hex())
BLOCK_SIZE = 16
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
def decrypt(key,enc):
cipher = AES.new(key, AES.MODE_ECB)
plain_text = cipher.dexrypt(enc)
return unpad(plain_text)
...
ttkey02 = 'i+LssarIzuxjZ7fTiBsQm8IOIUsJcf8rbrKkhydbxkEA3ZB5Bh9tQOkmJtOF0yYWIBcXxOIZDwrD 9MeIrRwJSw=='
ttkey02_decode = base64.b64decode(ttkey02)
restsecret = decrypt(secretkey, ttkey02_decode)
print(restsecret.hex())
-> restkey , restsecret 모두 구했다.
참고문헌
Decrypt AES ECB using python

다시 처음으로 돌아가서, 우리가 궁극적으로 구해야 하는 이 return 값을 들여다보면.

colon 으로 두 값이 연결되어있음을 알 수 있다.
즉, restKey : restSecret
restKey = ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
restSecret = ''
두 값을 콜론으로 연결한

이 값이 데이터베이스의 암호 key 가 된다.

실제로 복호화 되는 모습을 확인할 수 있다.
국민대학교 Forensic 동아리 FaS 에서 활동하며 발표를 맡았던
TigerConnect App 관련 내용을 정리할 것이다.
사실 이 앱같은 경우 어디에서 데이터를 복호화해야하는지를 모두 찾았으나, 어떻게 해결하는 지 막막해 중도 포기하고
KDFS 2022 ( 디지털 포렌식 챌린지 ) 로 넘어갔다.
조금 시기가 지났지만 그래도 끝맺는게 맞는 것 같아 다시 파일을 찾아 돌아왔다.
정리를 시작해보자.
-> 추출 기간이 달라 다른 부분이 있음. 다시 수정해야함.
1. 내가 찾았던 부분

1) TigerConnext 란?
- 암호화, PIN 잠금 및 관리자 제어 기능으로 메시징
- 전화, 이미지 비디오 및 음성녹음
- 한눈에 보이는 역할(roles)
- 역할 별 담당 직원을 빠르게 찾고 텍스트 전송
- 교대조별 일정관리
2) App Data 추출 - Com.TigerText

나는 루팅 안드로이드 폰으로 USB 추출을 진행했다.
우선 추출에는 데이터베이스 추출과 APP APK 추출이 있는데,
두가지 추출의 목적은
데이터베이스 - 유저 정보, 앱에 있는 데이터 / 대화 내용 등 을 추출할 때 필요
APP APK - 데이터베이스가 암호화되어있어 이를 해제하기 위한 복호가 필요할 때 코드를 뜯어보기 위해 추출
[ 애플리케이션 내부 데이터 구조 확인 방법 ]
- 데이터 추출
과연 내부에 어떤 데이터가 있는지 유용한 데이터 확인 작업이 필요하다.
데이터베이스가 암호화 되어있더라도 , 앱 apk 를 뜯지 않고 sharedprefs 에 있는 파일에 복호 힌트가 들어있는 경우가 있기 때문에 먼저 진행해주는 것이 좋다.
사용도구) HxD , SQLCipher, Dcode

- 앱.apk 추출
데이터를 추출해봐도 특별히 복호화 내용이 없다면, 본격적으로 apk 를 뜯어봐야한다.
사용도구) JADX
https://github.com/skylot/jadx/releases/tag/v1.4.3
3) 암호화 되어있는 데이터베이스 복호화
우선 TIGERCONNECT 는 데이터가 암호화되어있다.
그렇기 때문에 앱 데이터를 열기 위한 복호화가 필요하고, 앞에서 말했듯이 이를 복호화하기 위해서는
APP.apk 파일을 추출해야한다.
추출 한 후 우리가 사용해야하는 프로그램은 뭐다?
JADX

- 암호화된 데이터베이스 이름 검색 - "ttandroid.db"

- AES 암호화 방식
복호화 하기 위해 AES 암호화 방식을 알면 좋겠다 . 생각해서 찾아봤던

- 암호/복호 함수 찾기
JAVA 의 암호 모듈은 대부분 "AES/ECB/PKCS5Padding" 을 쓰기 때문에
이 부분을 찾는 것이 핵심!

여기서 authToken 을 찾는 것이 중요하겠다.
이를 위해 getAuthToken() 함수를 들어가보면

this.h 를 return -> getAuthToken() 함수 값을 구하는 것이 우선

최종적으로 구해야하는 값. (return 값)
[ restKey : restSecret ] 형태를 띄고 있음.
* 본격적으로 코드를 뜯어보자.

1) getRestKey 함수로부터 restKey 를 구할 수 있음.
- shared_prefs 파일에 "tigertext_default" 파일을 찾는다.

- 해당 파일을 사용해 파일 안에 ttkey01 값을 찾으면 이와 같이 나온다.
- 이를 restKey 로 사용하겠다는 뜻.
restkey = "SW3haySjzOxSmFnbxtlKrpao324QIG1U "
2) getRestSecret 함수로부터 restSecret 을 구할 수 있음.
1-1) 사진을 다시 보면,
getSecureString(defaltpref, "ttkey02", "") 부분을 통해 이 함수를 들어갈 수 있다.

string == null 이냐? yes : no
이부분에서 string 값이 shared_prefs 파일 폴더 안에 있는 default 값 중, "ttkey02" 에 해당하는 것을 알 수 있다.
그렇다면, 당연히 string 값은 null 값이 아니게 되고
no 이기 때문에 this.f48586b.decrypt(string); 값을 선택하게 된다.

이 값을 decrypt 함수에 넣어야 하기 때문에, decrypt 함수를 찾아 가보자.

여기에서
restKey 값은 위에서 살펴봤듯이, getRestKey 함수에서 가져온 "ttkey01" 값이다.
그 밑에 또 살펴볼 부분은 doFinal 인데,
doFinal 의 대상은 str 을 Base64.dexode 로 변한 값이다.
(여기서 str 은 위에서 string 값과 같음)
즉, "ttkey02" 값을 base64.decode 한 것임.
2. 도움을 받아 해결한 부분
이 이후 부분은 석사과정 선배님 자료 도움을 받아 제작했습니다.
윗 사진에서 이어서 설명해보면,
getRestKey 함수로부터 얻은 restKey 값을 b 함수에 적용

d(str) -> d 함수에 restkey 값을 넣는 것이다.

여기서 encryptionSalt 값은 .
"tigertext_defalt" 파일에서 "encryption_salt" 값 이다.

encryption_salt = "UEsnJdFTro+PweDr/QqQKV2gU+mDLQCAIhFYgyCSA0M="
bArr2 = Base64.decode(EncryptionSalt,2) 이므로
bArr2 = "50 4b 27 25 d1 53 ae 8f 8f c1 e0 eb fd 0a 90 29 5d a0 53 e9 83 2d 00 80 22 11 58 83 20 92 03 43"
test -----
encryption_salt_byte = '21 80 8d f1 6b f8 55 76 cf 3b e0 de 77 11 56 4d 8c 75 c6 f7 5b 47 11 bb ce 5e 83 d3 ec cc 8c 91'
encryption_salt = 'IYCN8Wv4VXbPO+DedxFWTYx1xvdbRxG7zl6D0+zMjJE='
ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
ttkey01 = '73 ca ba eb 6c af e1 d7 11 6d b3 6c 89 cb ef 46 09 04 bc 71 d0 2d ef 28'

salt_decode = base64.b64decode(encryption_salt)

모두 디코드 해야하는 것을 깨닫고.. 몇번의 실행 결과 완성 !
최종 코드 정리
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from base64 import b64encode,b64decode
encryption_salt = 'IYCN8Wv4VXbPO+DedxFWTYx1xvdbRxG7zl6D0+zMjJE='
salt_decode = base64.b64decode(encryption_salt)
ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
ttkey01_decode = base64.b64decode(ttkey01)
secretkey = pbkdf2_hmac('sha1', password=ttkey01_decode, salt=salt_decode, iterations=1000, dklen=32)
print('secretkey : ', secretkey.hex())
BLOCK_SIZE = 16
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
def decrypt(key,enc):
cipher = AES.new(key, AES.MODE_ECB)
plain_text = cipher.dexrypt(enc)
return unpad(plain_text)
...
ttkey02 = 'i+LssarIzuxjZ7fTiBsQm8IOIUsJcf8rbrKkhydbxkEA3ZB5Bh9tQOkmJtOF0yYWIBcXxOIZDwrD 9MeIrRwJSw=='
ttkey02_decode = base64.b64decode(ttkey02)
restsecret = decrypt(secretkey, ttkey02_decode)
print(restsecret.hex())
-> restkey , restsecret 모두 구했다.
참고문헌
Decrypt AES ECB using python

다시 처음으로 돌아가서, 우리가 궁극적으로 구해야 하는 이 return 값을 들여다보면.

colon 으로 두 값이 연결되어있음을 알 수 있다.
즉, restKey : restSecret
restKey = ttkey01 = 'c8q662yv4dcRbbNsicvvRgkEvHHQLe8o'
restSecret = ''
두 값을 콜론으로 연결한

이 값이 데이터베이스의 암호 key 가 된다.

실제로 복호화 되는 모습을 확인할 수 있다.