naroSEC
article thumbnail

들어가기 앞서

Digital Forensics Challenge(이하 DFC)  대회는 한국에서 열리는 몇 없는 디지털 포렌식 대회로 2018년부터 지금까지 매년 대회가 진행되고 있다. 올해에도 2023.05.01 ~ 2023.09.30까지 대회가 진행되었으며, 예전에는 디스크, 네트워크 포렌식 등의 문제가 주를 이루었다면, 근래 들어서는 모바일 문제도 조금씩 출제되고 있는 추세이다.
이번 포스팅에서는 DFC 2023 대회에서 출제되었던 301번 모바일 포렌식 관련 문제에 대한 Write Up을 작성하고자 한다.

Digital Forensics Challenge

Digital Forensics Challenge Digital Forensics Challenge hosted by Korea Institute of Information Security & Cryptology (KIISC) aims to expand knowledge of digital forensics and contribute to the field.

dfchallenge.org


문제 개요(시나리오)

대회에서 출제되었던 모바일 포렌식 관련 문제에 대한 개요는 아래와 같다.


문제 시나리오 개요
경찰은 피의자가 기밀 정보를 개인 스마트폰에 숨겨 유출하려 했다는 정보를 입수했습니다. 수색 및 압수 과정에서 기밀 정보의 흔적은 발견되지 않았습니다. 경찰은 피의자에게 스마트폰에 기밀 정보를 어디에 숨겼는지 물었지만, 모른다고 답했습니다. 용의자는 안드로이드 앱 개발에 관심이 많았던 것으로 알려져 있습니다. 용의자가 숨긴 기밀 정보를 찾아주세요.

문제
# UTC+9 표준 시간대를 기준으로 모든 문제를 풀어주세요.
1. 기밀 정보가 숨겨져 있는 APK 파일의 서명 정보는 무엇입니까? (60점) (MD5, SHA1, SHA256 모두 획득해야 합니다. 각 20점)
2. 기밀 정보 복호화 알고리즘은 무엇인가요? (90점)
3. 암호화된 기밀 정보의 해독된 평문은 무엇인가요? (150점)

문제 풀이(과정)

시나리오 개요에도 나와있듯이 용의자는 기업의 기밀 정보를 개인 스마트폰에 숨겨 유출하려는 혐의를 받고 있다. 그러나 용의자의 스마트폰을 수색한 결과 기밀 정보 유출 정황에 대한 흔적은 발견되지 않았다. 다만, 용의자는 안드로이드 모바일 애플리케이션 개발에 관심이 많았다고 문제에서 힌트를 주고 있다. 이러한 정보를 통해 용의자는 모바일 앱에 기밀 정보를 숨겼을 것이라고 추정해볼 수 있으며, 앱을 분석하여 숨긴 기밀 정보가 무엇인지 찾아야 되는 유형의 문제일 것으로 유추해 볼 수 있다. 그래서인지 문제에서는 용의자가 기밀 정보를 숨겼을 것이라고 추정되는 50여개의 APK 파일을 제공한다.
 

[ 풀이 1. 용의자가 기밀 정보를 숨겼을 것으로 추정되는 APK 파일 찾기 ]

문제에서 제공되는 50여개의 APK 파일들은 문제를 위해 제작된 테스트 앱이 아닌 실제 상용 앱의 설치 파일이다. 따라서, 용의자는 기밀 정보를 숨기기 위해 본인이 직접 앱을 만든 것이 아닌, 특정 상용 앱의 코드를 변조(리패키징)하여 코드 내에 기밀 정보를 숨긴 것으로 추측해 볼 수 있다. 이러한 전제를 기반으로, 제공된 APK 파일의 해시(MD5) 값을 각각 추출한 다음에 이를 Google Play Store에서 제공하는 해시 값과 비교하여 해시 값이 다른 APK 파일을 찾는다. 해시 값이 다르다는 것은 파일이 변조된 것이며, 용의자가 코드를 수정한 것이라고 볼 수 있다. 이때, 주의할 점은 해시 값 비교 시 반드시 동일 버전이랑 비교해야 한다.
이러한 과정을 거치고 나면, 단 한 개의 앱이 해시 값이 다른 것을 알 수 있는데 바로 [그림 1] "yes24" 앱의 설치 파일이다.
※ APK 해시 값 비교 과정은 어렵지 않기 때문에 별도의 풀이 과정은 기술하지 않겠다.

[그림 1] 해시 값이 다른 APK 파일(yes24)

 

[ 풀이 2. 원본 앱과 비교하여 변조된 코드 찾기 ]

1. 변조된 APK 파일(yes24)을 모바일 디바이스에 설치 후 실행해보면 우리가 아는 실제 "yes24" 앱과 UI나 기능적인 면에서 전혀 차이가 없는 것을 볼 수 있다.

[그림 2] 변조된 yes24 앱 실행

2. 겉으로는 실제 앱과 차이가 없기 때문에, 앱 내에서 용의자가 어떠한 기능을 숨겼는지 앱에서는 확인하기가 힘들다. 따라서, 앱을 디컴파일하여 코드를 직접 분석해야 한다. 다만, [그림 3]을 보면 알 수 있듯이 패키지 목록이 굉장히 많기 때문에, 코드를 하나하나 확인해가며 용의자가 어떠한 코드를 추가, 수정, 삭제 했는지 파악하는 것은 불가능하며 굉장히 비효율적이다. 또한, 해당 앱의 개발자가 이를 직접 보더라도 찾기 어려울 것이다. 따라서, 이러한 상황에서는 원본 앱과 비교하여 바뀐점(추가, 수정, 삭제)을 찾아야 한다.

[그림 3] 앱 패키지 목록

3. 코드를 비교해주는 Merge 프로그램은 많지만 모바일 앱 코드(Dex 파일)를 비교해주는 프로그램은 별로 없다. 그리고 있더라도 모바일 앱 컴파일 특성(모드, SDK 버전) 상 코드가 변경되지 않아도 레지스터리 변수들은 수시로 바뀔 수 있는데 Merge 프로그램에서는 이를 변경된 것으로 파악하여 알려준다. 때문에, 딱 분석가가 원하는 변경된 부분만을 한 눈에 파악하기란 힘들다. 그래서 필자의 경우 별도로 프로그램을 하나 만들었다. 해당 프로그램의 경우 Dex 파일의 디컴파일 산출물인 smali 코드를 비교하여 추가, 변경, 삭제된 클래스 및 함수명을 사용자에게 알려준다. 여기서는 해당 프로그램을 이용하여 풀이를 진행해 보도록 하겠다.

GitHub - naroSEC/SmaliCompare: Used to compare changed Smali codes.

Used to compare changed Smali codes. Contribute to naroSEC/SmaliCompare development by creating an account on GitHub.

github.com

4. 먼저 변조된 APK 파일과 비교 시 사용할 원본 APK 파일이 필요한데 현재 "Google Play Store"에 등록된 yes24 앱의 버전은 [그림 4]와 같이  "2.9.13"이다. 그리고 변조된 yes24의 APK 파일은 "2.9.9" 버전으로 버전이 상이하기 때문에, 별도의 모바일 앱 공유 사이트에서 다운로드 받아야 한다.

[그림 4] Google Play Store에 등록된 yes24 앱 버전

대표적으로 신뢰할 수 있는 앱 배포 및 공유 사이트로는 "APK Combo"와 "APK Pure"이 있는데 필자의 경우 APK Pure 사이트( https://apkpure.com/kr/ )에서 다운로드 받았다.

[그림 5] 대다수 앱 공유 사이트에서는 앱의 버전 별 설치 파일을 제공한다,

5. "SmaliCompare" 프로그램은 원본 smali 코드와 변조된 smali 코드 비교를 통해서 코드 변경 이력을 찾아주기 때문에, 먼저 Dex 파일을 smali 코드로 변환(디컴파일)하는 작업을 해줘야 한다. 일반적으로 Dex 파일을 smali 코드로 변환할 때, apkTool 또는 APK Easy Tool을 많이들 사용하는데 여기서는 "APK Easy Tool"을 사용해서 디컴파일을 해보겠다. APK Easy Tool은 아래 링크에서 Portable 버전을 다운로드 받으면 된다.

Box

app.box.com

6. APK Easy Tool은 apkTool과 달리 [그림 4]와 같이 GUI를 제공한다. 사용 방법은 간단하며, ① "Browse" 버튼을 클릭한 다음 디컴파일 대상 APK 파일을 선택하고 ② "Decompile" 버튼을 클릭해주면 된다.
※ 디컴파일 시 원본 APK와 변조된 APK 파일 두 개 모두 디컴파일 해주면 된다.

[그림 6] APK Easy Tool 실행 및 사용 방법

7. 디컴파일 과정이 완료되면, [그림 5]와 같이 "Decompiled APK directory" 버튼을 클릭하면 디컴파일 산출물을 확인할 수 있다. 

[그림 7] 디컴파일 산출물

8. 모든 디컴파일 과정이 끝났다면, "SmaliCompare" 프로그램을 실행시켜준다. 그러면 [그림 8]과 같이 "1번 디렉터리 선택", "2번 디렉터리 선택" 버튼이 있는데 [그림 9]와 같이 1번 디렉터리에는 원본 yes24 앱의 smali 코드가 위치한 "smali" 디렉터리를 선택해주고, 2번 디렉터리에는 변조된 yes24 앱의 smali 코드가 위치한 "smali" 디렉터리를 선택해주면 된다.

[그림 8] SmaliCompare 실행
[그림 9] 비교할 smali 코드가 위치한 디렉터리 선택

9. 디렉터리 선택이 다 되었다면, [그림 10]과 같이 시작 버튼을 누르고 잠시 기다리면 된다. 여기서 주의할 점은 비교해야 할 smali 파일이 많으면 많을 수록 시간이 오래 소요되기 때문에, 빠른 진행을 원할 경우 사전에 검사할 smali 파일을 추려내는 것을 추천한다.

[그림 10] SmaliCompare 비교 실행

비교가 완료되면, 프로그램이 위치한 디렉터리 내에 "REPORTING.TXT" 결과 파일이 생성된다. 그리고 해당 파일을 확인해보면 [그림 11] 처럼 원본 앱과 비교 했을 때, 변조된 APK 파일에는 "\c\c\d\u" 패키지 경로에 b.smali라는 새로운 파일이 생성되었고, \com\yes24\commerce\ActSetting.smali 파일에는 "dd()", "onTouchEvent()" 함수가 새로 추가된 것을 확인할 수 있다.
※ 참고로 확장자가 "smali"인 파일 중에서 "$" 특수문자가 붙지 않은 파일들은 Java에서 각각 클래스이다.

[그림 11] SmaliCompare 실행 결과

[ 풀이 3. 추가 및 변경된 smali 코드 분석 ]

1. 먼저 Java 디컴파일 도구로 새로 추가된 [그림 12]의 "\c\c\d\u\b.smali" 코드부터 살펴보면, "AES-CBC" 암호화 관련 로직인 것을 확인 할 수 있고. 추가로, [그림 12]의 ①을 보면 Cipher 객체의 초기화 함수인 init()의 첫 번째 인자 값이 "2"인 것을 통해 해당 로직이 "AED-CBC" 모드로 암호화된 데이터를 복호화하는 로직이라는 것을 알 수 있다.
※ 참고로, init() 함수의 첫 번째 인자 값이 "1"인 경우 암호화 한다는 의미이고, "2"인 경우 복호화 한다는 의미이다.

[그림 12] c/c/d/u/b 클래스 소스코드

2. 다음으로 새로 추가된 "com/yes24/commerce/ActSetting.smali" 클래스의 "dd()", "onTouchEvent()" 함수를 간략히 살펴보면, "dd()" 함수는 앞서 살펴본 "\c\c\d\u\b" 클래스의 AES-CBC 복호화를 수행하는 "dbk()" 함수를 호출하고 있고, "onTouchEvent()"는 디스플레이의 특정 위치를 10번 클릭(터치) 할 경우 "dd()"함수를 호출한다.

[그림 13]com/yes24/commerce/ActSetting 클래스의 dd(), onTouchEevent() 함수

3. 두 함수에 대한 개요를 간략히 알아봤으니, 이제는 onTouchEvent() 함수부터 자세히 살펴보고 정확히 어떠한 역할을 하는지 분석해보도록 하겠다. [그림 14]는 onTouchEvent() 함수의 소스코드를 smali 코드와 Java 코드를 비교한 사진으로, onTocuhEvent()의 경우 화면 클릭(터치) 액션이 발생할 경우 자동으로 호출되는 리스너 함수이다. 먼저 [그림 14]의 Java 코드에서 ①을 보면 해당 함수의 인자를 통해 화면의 터치 여부를 조건문으로 확인하고 있고 화면이 한 번 터치될 경우 [그림 15]의 ActSetting() 클래스의 생성자 값인 "this.dn"에 1을 더한 값을 "v" 변수에 저장한다. ② 그리고 "v" 변수 값이 "this.cu"와 같은지 조건문을 통해 확인하고 있는데 이때, "this.cu" 값은 [그림 15]에서 볼 수 있듯이 정수 20으로 즉, 특정 화면이 총 20번 터치가 되었는지 확인하고 있다. ③ 화면이 20번 터치가 되었다면, "Toast.makeText (앱에서 사용자에게 특정 메시지(Alert)를 출력할 때 사용된다.) " 함수를 호출하게 된다. 다만, 여기서 주의 깊게 봐야할 것이 있는데 현재 Java 코드에서는 "Toast.makeText()" 함수 호출 시 ""(공백)만 출력하는 것으로 보이는데 smali 코드를 보면 Toast.mekeText() 함수의 두 번째 인자로 dd() 함수 호출 결과가 전달된 것을 볼 수 있다. 분명 smali 코드에는 함수 호출부가 있으나, Java 코드에서는 디컴파일 시 해당 부분이 출력되지 않는 경우가 종종 있다. 따라서, 너무 Java 디컴파일러를 맹신하게 되면 이러한 중요한 부분들을 놓치게 될 수 있다.

[그림 14] com/yes24/commerce/ActSetting 클래스의 onTouchEvent() 함수의 smali 코드와 Java코드
[그림 15] com/yes24/commerce/ActSetting 클래스의 생성자

4. 다음으로 "dd()" 함수를 살펴보겠다. 먼저 [그림 16]의 ①을 보면 "dd()" 함수가 호출될 때, String 객체의 인자가 하나 전달된다. 그리고 해당 인자는 ②의 "dbk()" 함수의 두 번째 인자로 전달되며, 해당 결과 값이 "rv" 변수에 저장된다. ③ 마지막으로 "rv" 변수는 "dd()" 함수의 결과로 반환된다. 위에서도 잠깐 언급했지만, "dbk()" 함수는 "AES-CBC"로 암호화된 데이터를 다시 평문으로 복호하는 기능을 수행하는 "\c\c\d\u\b" 클래스의 "AES-CBC" 복호화 함수이다. 추가로 "dbk()" 함수에 대한 자세한 분석은 아직 안했지만, 해당 함수로 전달되는 매개 변수 이름을 통해 첫 번재 매개 변수가 암호 복호화 시 사용되는 "Key" 값이고 두 번째 매개 변수가 "암호문"이라는 것을 유추해 볼 수 있다.

[그림 16]  com/yes24/commerce/ActSetting 클래스의 dd() 함수 소스코드

5. 이제 마지막인 "\c\c\d\u\b" 클래스 함수들을 분석해보겠다. 먼저 [그림 17]의 ①을 보면 [그림 16]의 "dd()" 함수에서 호출되었던 "dbk()" 함수를 확인할 수 있다. 코드를 분석해보면 함수 호출 시 전달된 첫 번째 인자를 "getBytes()" 함수를 사용하여 문자열을 바이트 배열로 변환하고 두 번째 인자는 Base64로 디코딩 한 다음 "b.dd()" 함수의 인자 값으로 전달한다. 그리고 "b.dd()" 함수 코드에서는  ② 함수가 호출되면 새로운 "secretKeeySpec" 클래스 객체를 생성하는데 "secretKeeySpec" 클래스는 암호화 및 복호화 시 사용할 "암호 키""암호 알고리즘"을 설정할 때 사용되는 클래스이다. 추가로 객체 생성 시 전달되는 첫 번째 인자가 "암호 키"이고 두 번재 인자가 "암호 알고리즘"이 된다. 따라서, 이를 통해 "b.dd()" 함수의 "k" 인자가 "암호 키" 값이라는 것을 알 수 있으며, [그림 18]과 같이 전달되는 인자를 역으로 추적해보면 키 값이 "ActSettingCreate"라는 것도 확인 할 수 있다. ③에서는 실질적으로 암호화 및 복호화 작업을 수행하는 "Cipher" 클래스 객체를 생성하며, 이때 인자로 전달되는 "AES/CBC/PKCS5Padding" 문자열은 AES 암호화 알고리즘을 사용하고 모드는 CBC이며, 패딩 스킴으로 PKCS5을 사용하겠다는 뜻이다. 마지막으로 에서는 "doFinal()" 함수를 이용해 최종적으로 암호화 또는 복호화를 수행하게 된다. 여기서는 위 [그림 12]에서 설명했듯이 "Cipher" 클래스의 "init()" 함수 첫 번째 인자 값이 2이기 때문에 복호화를 수행하며, 인자로 전달된 암호문을 복호화 한 다음 해당 문자열을 반환하게 된다. 따라서, 매개 변수 "t"는 암호문이 된다.

[그림 17] \c\c\d\u\b 클래스의 소스코드
[그림 18] b.dd() 함수에서 사용된 "암호 키" 값 추적

6. 아래의 [표 1]은 지금까지 코드 분석한 내용을 간략히 정리한 것으로, 현재까지 파악된 내용은 특정 화면을 총 20번 터치하면 암호화 된 데이터를 복호화하는 함수가 호출되고 그 결과가 Toast.makeText() 함수를 통해 앱에 출력된다는 것이다. 다만, 코드 분석을 하면서 암호화 시 사용된 "암호 키"는 확인 할 수 있었지만, 정작 가장 중요한 암호문은 발견하지 못했다. 이에 대해서는 다음 풀이 챕터에서 자세히 살펴보도록 하겠다.

클래스 경로(위치)함수 이름기능 설명
com/yes24/commerce/ActSettingonTouchEvent(MotionEvent event)특정 화면의 위치에 클릭(터치)이 20번 발생 시 Toast.makeText() 함수가 호출되며, 이때, Toast.makeText() 함수의 인자로 ActSetting.dd() 함수의 결과 값이 전달된다.
com/yes24/commerce/ActSettingdd(String it)onTouchEvent() 함수에 의해 호출되는 함수로써,
AES 복호화 기능을 수행하는 b.dbk() 함수를 호출하며 이때, 인자로 전달 받은 "it(암호문)"과 "this.ky(암호 키)"를 b.dbk() 함수의 인자로 전달한다.
c/c/d/u/bdbk(String k, String t)전달받은 암호키와 암호문을 바이트 배열 및 Base64 디코딩 후 b.dd() 함수의 인자로 전달한다.
c/c/d/u/bdd(byte[] k, byte[] t)실질적으로 전달 받은 암호문을 복호화하는 기능을 수행하며, 복호화한 문자열을 다시 반환해준다.

[표 1] 분석한 함수 요약 정리

 

[ 풀이 4. 코드 분석을 통해 확인한 Toast.makeText() 함수를 출력하는 특정 화면 찾기 ]

1. 원본 앱과 비교해서 변조된 앱에 새로 추가되거나 변경된 코드를 대략적으로 살펴봤다. 이를 통해 "특정 화면"에서 사용자가 터치(클릭)를 총 20번 할 경우 Toast.makeText() 함수가 출력되는 것을 알 수 있었다. 이제 이 "특정 화면"이 어디인지 찾아야하는데 일반적으로 아래의 3가지 접근 방법을 통해서 찾을 수 있다.

  • onTouchEvent() 함수는 ActSetting 클래스에 정의되어 있으며, 해당 클래스는 Activity로 즉, 하나의 화면으로 구성된다. 따라서, ActSetting이라는 이름을 통해 설정과 관련된 화면 일 것으로 추측해 볼 수 있다.
  • ActSetting 클래스는 Activity이다. 그렇다는 것은 분명 코드 어딘가에서 Layout View를 바인딩하고 있을 것이기 때문에, 해당 코드를 찾아서 Layout 이름을 확인하고 View를 직접 확인해보면 된다.
  • AndroidManifest.xml 파일에서 정의된 ActSetting 클래스에 대한 Activity 정보를 확인하고 외부에서 호출이 가능하면 직접 호출을 통해 실행하여 어떠한 화면인지 확인한다.

2. 여기서는 가장 쉽고 빠르게 접근할 수 있는 3번 째 방법을 사용하여 "특정 화면"을 찾아보겠다. 먼저 변조된 앱의 디컴파일 산출물에서 "AndroidManifest.xml" 파일을 열어보면 [그림 19]의 ②와 같이 "ActSetting" 클래스에 대한 Activity 속성이 정의된 것을 볼 수 있다. 그리고 ①을 보면 "android:exported" 속성 값이 "true"로 이는 외부에서 해당 액티비티로 직접 참조가 가능하다는 의미이다.

[그림 19] 변조된 앱의 AndroidManifest.xml 파일 중 일부

3. ADB(Android Debug Bridge)를 통해 외부에서 "ActSetting" 액티비티를 직접 호출해보겠다. [그림 20]과 같이 명령 프롬프트 창에서 아래의 adb 명령어를 입력해주면 된다.

adb shell am start -n com.yes24.commerce/com.yes24.commerce.ActSetting
[그림 20] ADB를 이용한 액티비티 직접 호출

※ ADB를 이용하여 액티비티를 직접 호출하는 자세한 방법은 아래 포스팅을 참고하자.

ADB를 이용하여 액티비티를 강제로 실행시키는 방법

개요 안드로이드 앱 진단 시 루팅, 무결성, 디버깅 등과 같은 보안 위협에 탐지될 때, 별도의 우회 과정 없이 ADB를 이용하여 액티비티를 강제 실행함으로써, 탐지 프로세스가 무력화되는 경우가

naro-security.tistory.com

4. 명령어가 실행되면 [그림 21]과 같이 앱이 실행되어 adb를 통해 호출한 "ActSetting" 액티비티로 이동된다. 그리고 이를 통해서 "ActSetting" 액티비티는 "설정" 화면이라는 것을 알 수 있다.

[그림 21] ADB 명령어 실행 전/후 비교

5. 이제 "특정 화면"을 찾았으니, [그림 22]의 ①처럼 설정 화면의 액션바를 20번 클릭하면  ② Toast 메세지가 출력되는 것을 볼 수 있다. 그러나, 해당 Toast 메세지에는 아무것도 출력되지 않고 있다. 이는 위에서도 언급했듯이 Toast 메세지에는 "dd()" 함수의 반환 값 즉, 암호문의 복호 값을 출력하게 되어 있는데  [그림 23]의 코드를 보면 "dd()" 함수 호출 시 전달되는 인자(암호문) 값이 빈 값("")이기 때문에, 반환 값도 빈 값으로 출력되는 것이다. 따라서, 이제 가장 중요한 용의자가 숨긴 "암호문"을 찾는 일만 남았다.

[그림 22] 설정 화면의 액션바 클릭
[그림 23] dd() 함수가 호출될 때, ""(빈 값)이 인자로 전달되는 것을 볼 수 있다.

[ 풀이 6. 용의자가 숨긴 것으로 추정되는 암호문 찾기 ]

1. 이제 용의자가 어딘가에 숨긴 것으로 추정되는 암호문을 찾아야 한다. 다만, 지금까지 변경된 이력이 존재하는 클래스 및 함수들을 살펴 봤지만 암호문으로 추정되는 문자는 발견하지 못했다. 따라서, 암호문은 지금까지 분석한 클래스 및 함수 외의 코드에 삽입되어 있을 가능성이 높으며, 이러한 상황에서 별다른 힌트 없이 용의자가 숨긴 암호문을 무턱대고 찾는다는 것은 사막에서 바늘 찾기와 같다. 하지만, smali 코드의 특징을 조금만 이용한다면 생각보다 쉽게 찾을 수 있는 방법이 존재한다. 먼저 [그림 24]의 smali 코드를 보면 코드 상에 존재하는 문자열(String)은 별 다른 인코딩 또는 변환 없이 그대로 노출되고 있다. 그리고 [그림 17]의 "b.dbk()" 함수에서는 인자로 받은 암호문을 "Base64"로 디코딩을 수행한다. 따라서, 암호문은 Base64 인코딩 되어 smali 코드 어디간에 삽입되어 있을 것이며, 정규 표현식으로 이러한 특징들을 가진 문자열을 파싱한다면 암호문을 쉽게 찾을 수 있을 것이다.

[그림 24] smali 코드 예시

2. 암호문 찾기에 사용될 정규 표현식의 규칙은 아래와 같다.

  • smali 코드에 존재하는 모든 문자열(String)은 ""(더블 쿼터)로 묶인다.
  • Base64로 인코딩 된 문자열은 알파벳 대·소문자, 숫자, 특수문자('/', '=')로 구성된다. 이때, 특수문자는 문자열에 포함이 안 될 수도 있다.
  • 문자 길이는 최소 40자

위 규칙 중에서 문자 길이를 최소 40자로 정했는데 그 이유는 [그림 25]를 보면 "NARO Security"라는 문자열을 "AES-CBC" 모드로 암호화한 다음에 그 결과를 다시 Base64로 인코디한 산출물의 길이가 최소 40자이기 때문이다. 따라서, 용의자가 숨기려고한 기밀 정보가 적어도 "NARO Security" 문자열 보다는 길 것으로 추정되기 때문에 최소 길이를 40자로 정했다.

[그림 25] AES 암호화 및 Base64 인코딩

3. 이제 변조된 앱의 모든 smali 코드에서 위 정규 표현식의 규칙대로 문자열을 파싱하면 된다. 별도의 스크립트를 작성해서 정규 표현식으로 smali 코드를 하나씩 가져와 문자열을 찾는 방법도 있지만 여기서는 조금 더 쉬운 방법으로 접근하고자 한다. "JEB Decompiler"는 앱을 디컴파일 시 자동으로 모든 smali 코드를 합치게 되는데 이때, [그림 26]과 같이 "smali 코드 뷰 패널"에서 "CTRL + A" 단축키를 누르면 합쳐진 smali 코드를 별도로 저장(추출)할 수 있게 된다.

[그림 26] Merge된 smali 코드 추출

4. 추출한 smali 코드를 정규 표현식 검색을 지원하는 Text Editor로 불러와준다. 필자는 "NotePad++"를 사용했다. "NotePad++" 기준으로 "CTRL + F" 버튼을 눌러 찾기 기능을 불러와준다. 그 후 [그림 27]과 같이 ① 정규 표현식을 작성하고 ② 되돌이 검색을 체크해준다. ③ 그리고 정규 표현식 검색을 위해 "정규 표현식"을 체크하고 ④ 현재 문서에서 모두 찾기 버튼을 클릭해준다.

."[A-Za-z0-9]{40,}={0,2}/{0,}+"$
[ 정규 표현식 해석 ]
." => 문자의 맨 처음은 "(더블 쿼터)로 시작한다.
[A-Za-z0-9] => 문자는 영문 대소문자, 숫자가 올 수 있다.
{40,} => 문자의 최소 길이는 40자이다.
={0,2} => 검색 문자열 중 특수문자 '='은 최소 0번에서 최대 2번까지 올 수 있다.
/{0,} => 검색 문자열 중 특수문자 '/'은 최소 0번 올 수 있다.
+"$ => 문자의 맨 끝은 "(더블 쿼터)로 끝난다.
[그림 27] NotePad++에서 정규 표현식 검색하는 방법

5. 검색 결과 정규 표현식 조건에 일치하는 문자열은 총 18개이며, 이 중  Base64의 특징을 가지는 문자열은 "u7AdMnRkEXIXlNFgqlvJuyIrtjEWRY88M00A3GdkSZtN5jbQsVAqS8J9iEkIx3GK"으로, 해당 문자열이 암호문일 것으로 추정된다.

[그림 28] 정규 표현식 검색 결과

6. [그림 28]에서 찾은 문자열은 "d/a" 패키지 경로의 a() 함수에 위치하고 있다.

[그림 29] d/a 패키지 내에 a() 함수에 암호문이 위치하고 있다.

문제 풀이(암호문 복호화 하기 with Frida)

1. 이제 암호문을 찾았으니 AES 알고리즘 복호화를 통해 평문을 추출하면 된다. 복호화 로직은 [그림 17]과 [그림 18]의 과정에서 찾은 "암호 키"와 초기화 백터인 "IV(Initialization Vector)" 값을 통해 스크립트를 작성하면 된다. 다만, 해당 포스팅에서는 스크립트를 작성하는 방식이 아닌 "Frida"를 이용해서 평문을 추출해보겠다. 먼저 "Frida"를 사용하기 위해서는 "루팅 디바이스"가 필요한데 없을 경우 Nox와 같은 "에뮬레이터"를 사용해도 무방하다. 루딩 디바이스로 변조된 앱을 실행하면 [그림 30]과 같이 "루팅/탈옥 기기에서는 실행하실 수 없습니다"라는 문구가 출력되며, 루팅 디바이스를 탐지하고 있는 것을 볼 수 있다. 따라서, Frida 스크립트를 작성해서 평문 추출 시 루팅 우회 과정도 추가해줘야 한다.

[그림 30] 루팅 디바이스 탐지

2. [그림 31]은 루팅 우회 코드와 dd() 함수 후킹 코드가 포함된 Frida 스크립트이다. 먼저 ①을 보면 해당 앱에서는 루팅 탐지 시 Runtime 클래스의 exec() 함수를 통해서 "su" 명령어가 사용 가능하다면 루팅 디바이스로 탐지하게 된다. 따라서, 해당 함수가 호출될 때 전달되는 인자 값을 검사하여 "su" 일 때 임의의 값으로 변조하게 되면 루팅 탐지를 우회할 수 있게 된다. ②에서는 dd() 함수가 호출될 때 전달되는 인자 값이 빈 값("")이었지만, Frida의 재작성 기능을 통해 해당 인자 값을 [그림 29]에서 찾은 암호문으로 대체해주고 함수 반환 결과를 콘솔에 출력하도록 작성해줬다. 이제 Frida 스크립트를 실행해주면 앱 설정 화면의 액션 바 20번 클릭 시 Toast 메시지 및 콘솔 창에 암호문을 복호화 한 결과가 출력될 것이다.

[그림 31] 루팅 우회 및 dd() 함수 후킹 코
Java.perform(function() {
    const Runtime = Java.use('java.lang.Runtime')
    const exec = Runtime.exec.overload('java.lang.String')
    const ActSetting = Java.use('com.yes24.commerce.ActSetting')
    const dd = ActSetting.dd.overload('java.lang.String')

    exec.implementation = function(cmd) {
        if (cmd === 'su') {
            const fakeCmd = 'fakeValue'
            return exec.call(this, fakeCmd)
        }
        return exec.call(this, cmd)
    }
    dd.implementation = function(cryptogram) {
        cryptogram = "u7AdMnRkEXIXlNFgqlvJuyIrtjEWRY88M00A3GdkSZtN5jbQsVAqS8J9iEkIx3GK"
        const decryptionText = dd.call(this, plainText)
        console.log(`[*] 암호문 복호화 결과 => ${decryptionText}`)
        return dd.call(this, plainText)
    }
})

3. 작성한 스크립트를 Frida를 통해 실행한 다음 설정 화면 클릭 시 [그림 32]와 같이 복호문이 출력된다. 따라서, 용의자가 숨긴 암호문의 의미는 "정보(기밀 정보)는 내 개인 클라우드에 저장되어 있다."이다.

information was stored in my private cloud
[그림 32] Frida 스크립트 실행 및 복호문 출력

시나리오 정리

지금까지 분석한 결과를 토대로 간략히 문제 시나리오를 정리해보자면, 용의자는 "yes24" 앱의 코드 변조(리패키징)를 통해 "특정 액션 이벤트(onTouchEvent)"와 "AES 복호화 로직"을 기능을 추가했고 "information was stored in my private cloud" 라는 메시지를 "AES-CBC" 모드로 암호화하여 코드에 삽입했다. 그리고 추후에 누군가(경쟁사일 것으로 추정)에게 해당 앱을 전달하여 기밀 정보가 위치한 저장 매체(여기서는 개인 클라우드)의 위치를 알려주려고 했던 것으로 추정해볼 수 있다.


마무리

지금까지 DFC 대회에 출제되었던 모바일 포렌식 관련 문제를 살펴봤다. 필자의 경우 Google, Hacker101, HacktivityCon 등 다양한 주최사에서 제공하는 여러 모바일 관련 CTF 문제를 풀어보았지만, 이번 DFC 문제처럼 실제 상용 앱을 대상으로 문제가 출제된 케이스는 처음 접해 봤기 때문에 나름 신선했다. 문제 풀이 측면에서 보자면 AES 복호화나 암호문 숨기기 등 고전적인 방식이긴 하나 포렌식 관점에서는 실제 있을 법한 시나리오이기도 하고, 또 어떻게 보면 모바일 애플리케이션을 이용한 스테가노그래피라고 볼 수 있겠다. 그리고 Google, HacktivityCon 등이 주최하는 CTF 문제를 보면 너무 문제 다운 성향이 강하다. 즉, 어렵게 만드는 것에 초점을 맞추다 보니까 실제 실에서는 써먹기 힘든 기술들이 대거 등장한다. 그런면에서 봤을 때는 이번 DFC 문제의 경우 현업에서도 활용할 수 있는 부분들이 다수 존재해 모바일 공부를 하시는 분들은 한 번씩은 풀어볼 것을 추천해볼만한 문제이다.

profile

naroSEC

@naroSEC

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...