원래는 nox로 환경을 맞춰서 같이 해보려고 했지만,,,

 

애플의 보안 정책으로 nox를 사용할 수가 없어서,

제가 해온 것으로 대체하여 진행하겠습니다. ㅜ

 


분석을 하려면 제일 먼저 apk부터 구해야 합니다.

 

apk를 구하는 방법으로는 playstore에서 설치 후 추출하거나,

apkpure.com에서 검색을 해서 받는 방법이 있습니다.

 


playstore을 통해 설치한 apk를 추출하는 방법입니다.

 

제일 먼저 해야 할건 뭘까요?

바로 어플리케이션을 설치하는 것입니다.

 

그다음으로 해야 할 것들이 추출을 위하여

노트북과 안드로이드 장치를 adb로 연결하고,

추출할 패키지의 이름을 확인한 뒤

해당 패키지를 추출하면 끝납니다.

 


adb 로 안드로이드 장치에 연결하는 방법입니다.

에뮬레이터의 경우 각 에뮬레이터 별로 사용하는 adb 포트로 접속을 하고,
실제 기기의 경우에는 개발자 옵션 중 하나인 usb 디버깅을 허용해주면 됩니다.

 

[*] nox : 62001

 


패키지의 목록은 pm list packages -f로 확인할 수 있으며,

설치된 패키지가 많을 경우에는 찾기가 힘들기에,

적당히 grep으로 추려서 확인하시면 됩니다.

 


추출할 패키지의 이름을 확인했다면,

pull 옵션으로 해당 apk를 추출할 수 있습니다.

 

반대로 adb로 apk를 설치할 수도 있습니다.

adb의 옵션으로 install을 주면 apk를 설치할 수 있으며,

-r 옵션을 추가로 주는 경우에는 이미 존재하는 패키지를 재설치할 수 있습니다.

 

하지만 기존에 설치된 서명과 설치하고자 하는 서명이 다른 경우에는

다른 패키지로 인식하여 재설치할 수 없습니다.

그런 경우에는 기존에 충돌되는 패키지를 삭제하고, 설치를 진행해야 합니다.

 


지금까지 사용한 adb 명령어들입니다.

 


apkpure.com 에서 apk를 구하는 방법은 간단합니다.

apk 이름만 검색하면 쉽게 다운받을 수 있습니다. 참 쉽쥬 ~?

 


자 이제 apk 를 구했으니 본격적으로 분석을 시작해 봅니다.

apk의 디코드와 빌드는 apktool을 이용할 수 있습니다.

 

apktool의 옵션으로

d를 주면 디코드 되며,

b를 주면 빌드되어 dist 폴더에서 확인할 수 있습니다.

 


루팅 탐지 루틴을 분석하기 위하여

안드로이드 어플리케이션을 분석할 수 있는 방법을 2가지 정도 소개하겠습니다.

 

물론 정해진 분석 방법은 없으며,

맞는 방법이 아닐 수도 있습니다.

본인이 편한 방법으로 분석을 진행하면 됩니다.

 

방법은 크게 Top-down 방식과 Bottom-up 방식이 있습니다.

쉽게 이야기하자면 위에서부터 아래로, 아래에서부터 위로 분석하는 방식입니다.

 

예제를 들며 설명을 하면 이해하기 편할 것 같아서,

native root checker를 대상으로 분석하면서 설명하겠습니다.

 

우선 이 어플을 선택한 이유는

native 하게 루팅을 탐지해서

조금 뒤에서 루팅 탐지 방법을 설명할 때,

익숙하지 않은 달빅 바이트코드보다는

cpp 코드가 더 이해하기 쉬울 것 같아서 골랐습니다.

 


Top에서 내려가는 방식보다

Bottom에서 올라가는 것이 더 간단하기에 이것부터 설명하겠습니다.

 

Bottom-up 분석 방식은 끝에서부터 원하는 분석 부분까지 올라가는 방식입니다.

 


분석을 하기에 가장 먼저 찾아야 할 것은 시작점을 찾는 것입니다.

여러 방법 중에서 가장 쉽다고 생각하는 방법을 하나 소개하겠습니다.

다시 한번 말하지만 분석 방법에 있어 항상 정답은 없습니다.

 

어플리케이션을 실행시켜 보면 각 항목 별로 탐지 결과를 보여주는데,

이 항목들은 문자열로 처리를 했을 것이다라고 가정을 하고 시작합니다.

 

그래서 확인 차 TEST KEYS를 검색해 보니,

strings.xml 파일에서 확인을 할 수 있습니다.

 


strings.xml 은 다국적의 언어를 처리하기 쉽게 하기 위한 용도의 파일로,

사용자가 선택한 언어에 따라 문자열을 동일한 name으로 처리할 수 있습니다.

 


이전에 strings.xml 에서 찾은 name을 검색해 보면,

public.xml에서도 사용된 것을 확인할 수 있습니다.

 

strings.xml 이 변수명과 그 값을 저장한 파일이라면,

public.xml 은 변수명과 변수의 주소를 저장한 파일입니다.

 


마지막으로 이 주소를 검색해 보면,

달빅 코드 내부에서는 이 주소를 이용하는 것을 확인할 수 있습니다.

 

아직 분석은 안 했지만 뭔가 감이 오는 switch 문이 보입니다.

이런 식으로 문자열을 기준으로 하여,

분석 지점을 쉽게 파악할 수 있습니다.

 


하지만 몇 번 말했듯이 이 방법도 정답이 아닐 수도 있습니다.

이것처럼 잘못된 시작은 삽질의 시작이 될 수도 있습니다.

 

루팅 탐지의 결과를 알려주는 맨 하단부의 초록색 창이

문자열일 것이라고 생각하고 진행을 한다면,

그 문자열은 찾지 못할 것입니다.

왜냐하면 이 문자열은 사진(png)으로 처리되어 있기 때문입니다.

 

그래서 저 같은 경우에는 이렇게 몇 개 해보고 안되면,

Top-down 방식으로 다시 분석을 합니다.

 


그다음으로는 Top-down 분석법입니다.

 

위에서부터 아래로, 큰 범주에서 목표를 향해 분석하는 방식입니다.

 


마찬가지로 Top-down 분석을 시작하려면 분석을 할 시작점부터 찾아야 합니다.

 

apk의 경우 그 시작점은 메인 엑티비티로,

메인 엑티비티는 apk 파일 내의

안드로이드 메니페스트 파일에서 확인할 수 있습니다.

 


이 사진은 메인 엑티비티에 해당하는 엑티비티입니다.

 

그다음은 어디를 봐야 할까요?

그것을 알려면 안드로이드 라이프사이클에 대하여 알아야 합니다.

 


안드로이드 라이프 사이클이라고 부르는 관계도입니다.

정확히 말하면 엑티비티 라이프 사이클입니다.

안드로이드 어플리케이션에는 여러 엑티비티가 있고,

이들 중 하나의 엑티비티가 실행되고 소멸하기까지의 관계도를 도식화한 것이 엑티비티 라이프 사이클입니다.

 

그렇다면 이 엑티비티 라이프사이클은 왜 중요할까요?

안드로이드의 경우 PC 와는 다르게 제한된 환경으로,

정해진 양의 자원을 여러 어플리케이션에서 나눠서 사용해야 합니다.

따라서 각 어플리케이션 별로 사용하는 자원을 효율적으로 관리하기 위하여 나온 것이 라이프 사이클입니다.

어디서 어떻게 사용할지 알아야,

자원을 같이 사용할 수 있는 거죠.

 

처음에 엑티비티가 호출되면 시스템에서 가장 먼저 호출하는 메소드가

onCreate( ) 메소드입니다.


onCreate( ) 메소드는 엑티비티가 처음 생성될 당시 한 번만 호출되며,

데이터를 초기화하는 단계입니다.
파라미터로 넘어온 데이터를 바인딩하거나

이전에 저장된 instance의 값 등을 불러오는 역할을 합니다.

 

onStart( ) 메소드에서는

본격적으로 엑티비티와 사용자가 상호작용 할 수 있도록 준비하는 단계입니다.


예를 들면 UI를 관리하는 코드를 초기화하여,

엑티비티를 포그라운드로 보내 상호작용을 할 수 있도록 합니다.

 

onResume( ) 메소드에서는 엑티비티를 화면에 보여주면서,

사용자와 상호작용을 하는 단계입니다.


사용자와 상호작용을 하며 엑티비티가 실행되며,

별도의 이벤트가 있기 전까지 이 상태에 머무릅니다.

 

onPause( ) 메소드는 다른 엑티비티가 호출되어,

잠시 멈출 때 호출됩니다.


화면에서 뒤에 흐리게 되어 존재는 하지만,

상호작용을 할 수 없는 상태입니다.


예를 들면 게임에서 일시정지 버튼을 눌러,

게임 관련 엑티비티가 멈춘 상태입니다.

 

onStop( ) 메소드는 엑티비티가 화면에서 사라질 때 호출되며,

필수로 저장해야 하는 데이터 등을 저장합니다.


예를 들면 게임을 하고 있는데 전화가 온다면,

화면이 변경됨과 동시에

시스템에서 게임의 엑티비티에 onStop( ) 메소드를 콜백 합니다.

 

그 후 게임으로 다시 돌아가게 된다면,

onRestart( ) 메소드를 호출하여

엑티비티의 onStart( ) 메소드를 호출할 수 있게 준비합니다.

 

마지막으로 onDestroy( ) 메소드에서는 onCreate( ) 메소드에서 할당했던 변수 등을 해제하며,

엑티비티를 정리합니다.

 

짝을 지어 보자면

onCreate( ) 메소드와 onDestory( ) 메소드가 대치되며,

nStart( ) 메소드와 onStop( ) 메소드와 대치됩니다.

 

마지막으로 정리해보면

처음 엑티비티가 호출되면,

onCreate -> onStart -> onResume 메소드를 거치며 엑티비티가 실행되며,

사용자와 상호작용하다가

특별한 이벤트가 발생하면 onPause 메소드가 로 호출되어 일시 정지되며,

사용자의 중지 요청 등으로 onStop 메소드가 호출되며,

onDestroy 메소드를 거쳐 엑티비티가 종료합니다.

 

네 그러면 native root check 어플리케이션을 이 라이프 사이클에 맞춰서 분석해 보겠습니다.

오늘 제가 발표할 주제는 안드로이드 101입니다.

원래는 안드로이드 어플리케이션에서 어떻게 루팅을 탐지하는지에 대해서만 발표하려고 하였으나,
처음 안드로이드 공부할 때 많이 막막했던 것이 생각나기도 하고,
처음 하는 분들에게 조금이라도 도움을 드릴수 있고자 안드로이드에 대한 기본적인 내용을 추가하고,
루팅 탐지 부분의 비중을 낮춰서 안드로이드 101으로 발표 주제를 정했습니다.

 


목차는 크게 다음과 같습니다.

 - APK에 대한 기본적인 지식들

 - APK 분석 방법

 - 루팅 탐지 방법

 


아시다시피 요즘 날의 모바일 어플리케이션 생태계는 크게 안드로이드와 iOS로 나눌 수 있습니다.

그중에서 non-native code 인 달빅 바이트 코드를 사용하며,
상대적으로 디버깅하기 쉬운 안드로이드 어플리케이션에 대하여 이야기해보려고 합니다.

 


앞에서 non-native code에 대하여 언급했는데,
조금은 헷갈릴 수 있는 개념이라 간단하게 정리하고 넘어가겠습니다.
정확하게 까지는 아니더라도 대충 native 한 게 어떤 느낌인지 정도만 아셔도 괜찮을 것 같습니다.

 

우선은 managed code입니다.

 

이 managed code는 microsoft에서 2003년에 비주얼 스튜디오 2003을 출시하면서 발표한 개념입니다.
마이크로소프트 사의 C#이나 VB .NET 이 managed code에 포함되며,
그리고 많이들 사용하는 Python, Java 등도 여기에 포함됩니다.

 

소스코드가 실행되기까지의 과정에 대하여 간단하게 이야기해보면,
사용자가 작성한 소스코드는 IL 즉 중간 언어로 컴파일되는데,
이 IL 은 어셈블리어 정도로 생각하시면 될 것 같습니다.
그 후 IL 파일이 정상인지 확인한 후 runtime에 사용되는 메소드를 호출합니다.

 

이 지터에서 runtime 시에 사용되는 메모리나 예외 등을 처리해주기 때문에,
파이썬처럼 변수를 따로 할당하지 않아도 알아서 메모리를 할당해 주고 해제해줍니다.

 

다음으로는 unmanaged code입니다.

 

managed code에 반대되는 개념으로 예전부터 있던 전통적인 C 등이 여기에 포함됩니다.

 

소스코드는 각 환경에 맞게 바로 컴파일되어 기계어로 된 프로그램의 형식으로 실행되며,
C언어를 하면서 많이들 느꼈겠지만 필요한 메모리를 직접 할당(malloc) 및 해제를 해줘야 합니다.

 

참고로 알고리즘 할 때 많이 사용되는 C++ 은 managed code 가 될 수도, unmanaged code 가 될 수도 있습니다.

 

마지막으로 좀 모호하다고 생각되는 native code입니다.

 

보통은 native code를 unmanaged code의 동의어의 일종으로 생각해서,
크게 managed code와 native code로 나누기도 합니다.
이전 슬라이드에서 말한 것처럼 native code와 non-native code로 나눈 것처럼 말이죠.


하지만 native code는 unmanaged code 일 수도,
managed code의 산출물인 machine code 인 기계어도 될 수 있습니다.

 

이 부분에 대해서는 pwnwiz 님이 더 잘 아시니,
잘 모르겠다 싶은 것은 pwnwiz 님께 질문하면 잘 알려주실 겁니다.

 


apk 해킹? 이 맞는 말인지 모르겠지만,
apk 해킹에서 제가 가장 중요하다고 생각하는 것은

리패키징 방지 우회 및 루팅 탐지를 우회하는 것이라고 생각합니다.

 

왜냐하면 리패키징 방지와 루팅 탐지 2개에서 자유롭다면 분석의 난이도는 다르지만,
그래도 코드를 변조해서 무엇인가는 할 수 있기 때문입니다.

 


안드로이드 어플리케이션인 apk의 경우 iOS 어플리케이션인 ipa에 비하여 단순한 zip 구조로 구성되어 있으며,
별다른 절차(루팅) 없이도 개발자 도구 중 하나인 usb 디버깅 모드로 apk의 설치 및 삭제가 가능합니다.

 

그래서 사용자는 설치된 apk를 추출하거나 외부에서 apk를 구하여,
수정한 후 리패키징하여 다시 설치할 수 있습니다.


이렇게 되면 악의적인 사용자는 apk를 수정하여하고 싶은 것을 할 수 있게 됩니다.

 


apk의 구조를 조금 더 자세히 보자면,

소스코드는 dex 파일로 컴파일되며
여기에 apk에서 사용할 여러 리소스 파일들과 네이티브 코드들을 압축하여

apk를 생성합니다.

 

이 apk를 설치하면,

반대로 압축을 풀면서 설치가 되고 달빅이가 바이트코드를 해석하고 실행이 됩니다.

 


앞에서 말한 바와 같이 apk 해킹하는데 첫 번째로 중요하다고 생각하는 리패키징 방지입니다.

 

리패키징 과정은 apk 만드는 과정을 거꾸로 포함하고 있습니다.


정상적이라면 소스코드에서 컴파일을 한 후,
패키징 과정을 거친 후에 서명을 하고 배포를 합니다.


하지만 리패키징 과정은 apk를 언패킹 해서,
디컴파일을 거쳐 소스코드를 획득하고,
수정을 한 후에 다시 apk를 만듭니다.

 

따라서 이렇게 리패키징을 통해 소스코드를 수정할 가능성이 있기 때문에,
apk 가 리패키징되지 못하도록 방지해야 합니다.

 


이름은 밝힐 수 없는 한 게임이 있습니다.
물론 이 게임에서도 여러 보안 방식들로 자사의 어플리케이션을 보호하고 있습니다.


하지만 이렇게도 할 수 있다는 것을 보면서,
리패키징 방지의 중요성을 한번 더 생각해 봤으면 좋겠습니다.

 

우선 이러한 짓을 한 개요는 다음과 같습니다.
게임을 플레이하는 게 귀찮아서

“한 가지 색상으로만 나온다면 가만히 있어도 편하게 게임을 끝낼 수 있지 않을까?"

라는 생각으로,
파란색 캔디만 생성되도록 바꿔 보았습니다.
하지만 당연히 끝날 거라 생각했던 게임은 끝나지가 않았습니다,,

 


그다음으로 두 번째로 중요하다고 생각하는 루팅 탐지입니다.

 

여기서 루팅이란 정확히 뭘 말하는 것일까요?
루팅이란 최고 권한인 root를 획득하는 것을 말하며,
안드로이드 운영체제에서는 최고 권한인 superuser 권한을 획득하는 것을 말합니다.


이 권한으로는 일반적으로 할 수 없는 것들을 할 수 있게 해 줍니다.

예를 들면 접근이 불가능한 시스템 폴더 같은 곳에 접근해서 삭제 불가능한 좀비 어플을 삭제하거나,
어플리케이션이 사용하는 메모리에 접근해서 수정하는 것이 가능합니다.

 


초등학교, 중학교 때 게임을 많이 해보신 분들은 한 번은 써봤을 러키패처와 치트엔진입니다.


일반적으로 안드로이드에서는 각 어플리케이션별로 uid를 생성하고 각 uid 별로 관리되지만,
이 어플리케이션들은 루팅을 이용하여 다른 프로세스에서 사용하는 메모리를 수정할 수 있게 해 줍니다.

 

따라서 메모리 무결성 등이 중요한 금융이나 게임 어플리케이션의 경우 루팅이 되어 있는지를 확인해야 합니다.

 


앞에서 소개한 루팅을 탐지하는 것과 리패키징을 방지하는 것 외에도 여러 보호 기법들이 있습니다.


이 중에서 제가 가장 싫어하는 보호 기법을 하나 보여 드리면서 넘어가도록 하겠습니다.

 


이 어플리케이션은 제가 분석하면서 제일 싫었던 어플리케이션 이였습니다.

 

중간에 보면 메소드들이 a.a.a으로 되어 있는데 다시는 만나고 싶지 않은 친구입니다.

 

그러면 오늘의 진짜 주제였던 안드로이드에서 루팅 탐지 방법에 대하여 소개하겠습니다.

log 파일에서 새로이 추가되는 내용을 확인하기 위하여, tail 명령어를 이용합니다.

tail -f example.log

위의 명령어를 실행하면, example.log 파일을 추적하면서 새로이 추가되는 부분을 출력하여 줍니다.

 

하지만 tail 명령어는 기본적으로 실행 시점을 기준으로 바로 직전의 10줄을 출력합니다.

tail -f -n0 example.log

이를 출력하길 원하지 않는 경우에는, tail 명령어의 옵션 중 하나인 -n0 을 추가하면 직전 10줄은 출력하지 않고 실행 이후에 추가되는 부분만을 출력합니다.

 

그리고 특정 로그만을 출력하게 하기 위하여, pipe 라인으로 grep 명령어를 같이 사용합니다.

tail -f -n0 example.log | grep 'alert'

위의 명령어를 실행하면, 새로이 추가되는 부분들 중에서 alert 라는 키워드를 포함하고 있는 줄만을 출력하여 줍니다.

 

그리고 출력되는 양이 늘어나게 되면, 출력되는 중간중간 잘려서 출력이 되곤 합니다.

tail -f -n0 example.log | grep --line-buffered 'alert'

그럴 경우에는 grep 명령어의 옵션 중 하나인, --line-buffered 를 이용하면 한줄 단위로 출력할 수 있습니다.

 

마지막으로 이렇게 출력한 내용을 파일로 만들기 위해서는, > 명령어를 이용하면 됩니다.

tail -f -n0 example.log | grep --line-buffered 'alert' > alert.log

위의 명령어를 실행하면, example.log 파일에서 새로이 추가되는 줄 중에서 alert 키워드를 포함하고 있는 줄만을 alert.log 파일로 저장하게 됩니다.

 

> 명령어의 경우, 파일을 새로이 생성하여 저장을 합니다.

>> 명령어의 경우, 기존의 파일에 추가하여 저장을 합니다.

readme.txt

readme.txt 파일을 읽어보면, file 을 복호화하는 것이 목표이며 그 암호화된 파일은 EXE 인 것 같습니다.

 

unpack UPX

암호화하는 프로그램으로 추정되는 run.exe 의 정보를 확인해 보면, upx 로 압축되어 있습니다.

따라서 이를 먼저 풀어줍니다.

 

anti Hexray

run.exe 프로그램을 보면 중간중간 분석을 방해하기 위해서 특정한 목적없이 무의미한 명령어를 반복하는 부분이 있는데, 이로 인하여 디컴파일 도구에서 제대로 분석하지 못하도록 방해하고 있습니다. 따라서 이것들을 먼저 제거해 줍니다.

 

바이트 코드를 확인해 보면 60 61 90 50 58 53 5B 가 반복이 되는데, 이를 010 editor 같은 도구를 이용하여 90 * 7 으로 치환합니다.

 

90 치환 시작 주소 90 치환 끝 주소 offset 실제 offset
0x406 0x129CD 0x125C7 0x125C4
0x129E9 0x24FB0 0x125C7 0x125C4
0x24FD7 0x3759E 0x125C7 0x125C4
0x375AD 0x49B74 0x125C7 0x125C4

이제 90으로 채워져서 NOP 으로 채워져 있지만, 디컴파일에 시간을 잡아먹기 때문에 이 NOP 공간을 점프하는 명령어를 넣습니다.

JMP 할 공간의 offset 은 0x125C7 로 동일하며, 명령어에 사용되는 5바이트를 빼주고 90 의 끝에서 다음 주소를 가리켜야 하기 때문에 1을 더해줍니다.

따라서 E9 C3 25 01 00 을 90 공간의 맨 앞부분에 넣어줍니다.

 

그러면 위와 같이 중간에 NOP 코드를 생략하는 것을 확인할 수 있습니다. 근데 위의 코드도 자세히 보면 레지스터 값을 넣고, 그대로 빼는 의미없는 함수입니다.

 

실제로 이 함수를 여러번 호출하는데, 이를 통해 디컴파일을 방해하려는 목적인 것 같습니다.

 

복호화 루틴

sub_41355e0 함수를 보면, 이 함수에서 복호화가 진행되는 것을 알 수 있습니다.

 

복호화에 사용할 키를 0x44d370 에 입력받습니다.

 

입력으로 들어온 Key 의 길이를 구합니다.

 

그 후 file 이라는 파일을 열고,

 

파일의 크기를 구하고, 파일 포인터를 처음으로 돌려 놓습니다.

 

그 후 파일의 내용을 받아서, dword [ebp - 0x8] + 0x5415b8 영역에 올립니다.

 

그 후 key 와 파일의 내용을 1바이트 씩 가져와서 xor 연산을 수행한 후, 그 값에 0xff 를 xor 즉 ~ 연산을 수행합니다.

 

그 후 그 내용을 file 에 다시 씁니다.

 

복호화 하기

파일의 마지막 부분을 보면, 평소 0으로 채워져 있어야 할 영역을 확인할 수 있습니다.

특정 문자열이 13개씩 반복되는 것으로 보아, 키는 13자리인 것을 추측할 수 있습니다.

또한 exe 라면 dos stub 에 위치한 This program cannot be run in DOS mode 를 이용하여 키를 구해보겠습니다.

 

위에서 찾은 키를 이용하여, 암호화된 파일을 복호화하겠습니다.

 

복호화 key 는 letsplaychess 이며, 그 키를 이용하여 file 을 복호화하였습니다.

 

복호화가 된 파일을 확인해 보면, upx 로 압축된 pe32 파일임을 알 수 있습니다.

 

Flag

복호화한 EXE 에서 플래그를 찾아보겠습니다.

 

먼저 UPX로 압축이 되어 있기 때문에, 이를 먼저 풀어줍니다.

 

그 후 출력하는 부분을 확인해 보면, Key -> Colle System 라는 문자열을 출력합니다.

이 부분외에는 출력하는 부분이 없는 것으로 보아, Colle System 이 플래그인 것 같습니다.

 

인증

'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Easy_ELF  (0) 2019.10.05
Reversing.kr :: Easy_Unpack  (0) 2019.03.01
Reversing.kr :: Easy_Keygen  (0) 2019.02.26
Reversing.kr :: Easy_Crack  (0) 2019.02.24

문제 소개

상용 안드로이드 어플리케이션을 개발했지만, 불행히도 개발은 멈췄고 출시를 하지 못하게 되었습니다. 하지만 유효한 키를 찾아서 프리미엄 기능을 열어달라고 합니다.

 

APK 분석

apk 를 제공해 주므로, apktool 을 이용하여 디컴파일을 진행합니다.

.
├── THC
│   ├── AndroidManifest.xml
│   ├── apktool.yml
│   ├── original
│   ├── res
│   └── smali
└── THC.apk

apk 를 디컴파일해보면, 여러 구성 요소들이 보입니다.

 

AndroidManifest.xml 파일을 확인해 보면, com.thc.bestpig.serial.MainActivity 가 메인 엑티비티 인 것을 알 수 있습니다.

 

MainActiviy->onCreate() 부터 살펴 봅니다. onCreate() 에서 하는 게 많지만, 핵심은 마지막 부분에 위치합니다.

Id가 0x7f070023 View 를 가져와서 버튼인지 확인을 하고, MainActivity$3의 <init> 함수를 호출하고 버튼에 Click Listener 을 설정합니다.

 

<public type="id" name="buttonActivate" id="0x7f070023" />

Id 가 0x7f070023 인 것을 확인해 보면, buttonActivate 라는 이름을 확인할 수 있습니다.

 

더 확인해보면, 텍스트로 "Activate Software" 를 가진 버튼임을 확인할 수 있습니다.

 

다시 MainActivity$3 으로 돌아와서, 버튼을 click 하게 되면 위의 method 가 호출됩니다.

입력한 값을 가져와서 문자열로 바꾸고, 이 문자열을 MainActivity의 checkPassword() 메소드의 인자로 하여 호출합니다.

 

checkPassword 메소드에서는 이 문자열을 다시 validateSerial 메소드의 인자로 하여 호출을 하고, 결과값이 1이 아니면 인증 실패 루틴으로 이동하게 됩니다. 따라서 validateSerial 메소드에서 1을 반환할 조건을 분석해 봅니다.

 

validateSerial 메소드에서는 v0 의 값을 return 하게 되는데, 이 값이 1이 되는 경우는 아래에 더 존재하는 모든 조건을 충족하는 경우만 있습니다. 따라서 이 조건들을 정리해 보면 아래와 같습니다.

 

.line 18    len(flag) == 0x13
.line 20    flag[0x04] == flag[0x09] == flag[0x0e] == 0x2d
.line 22    flag[0x05] == flag[0x06] + 0x01
.line 25    flag[0x05] == flag[0x12]
.line 28    flag[0x01] == (flag[0x12] % 0x04) * 0x16
.line 30    flag[0x0a] == flag[0x03] * flag[0x0f] / flag[0x11] - 0x01
.line 32    flag[0x01] == flag[0x0a]
.line 34    flag[0x0d] == flag[0x0a] + 0x05
.line 36    flag[0x0a] == flag[0x05] - 0x09
.line 38    0x5a0 == (flag[0x00] % flag[0x07]) * flag[0x0b]
.line 40    flag[0x02] - flag[0x08] + flag[0x0c] == flag[0x0a] - 0x09
.line 42    (flag[0x03] + flag[0x0c]) / 0x02 == flag[0x10]
.line 44    flag[0x00] - flag[0x02] + flag[0x03] == flag[0x0c] + 0x0f
.line 46    flag[0x03] == flag[0x0d]
.line 48    flag[0x10] == flag[0x00]
.line 50    flag[0x07] + 0x01 == flag[0x02]
.line 52    flag[0x0f] + 0x01 == flag[0x0b]
.line 54    flag[0x0b] + 0x03 == flag[0x11]
.line 56    flag[0x07] + 0x14 == flag[0x06]

이 조건들로 시리얼을 직접 구할 수도 있지만, SMT solver 중 하나인 Z3 를 이용하여 solver 를 만듭니다.

 

Serial 구하기

위에서 찾은 식을 조건에 추가하고, 조건을 충족하는 결과 값을 출력합니다.

 

위에서 구한 serial 값을 입력하면, 활성화에 성공한 것을 확인할 수 있습니다.

'0x00 Reversing > 0x02 CTF' 카테고리의 다른 글

InCTF 2019 :: cliche_crackme  (0) 2019.10.05

_start 함수

_start 함수를 보면 main 함수를 호출한다.

 

main 함수

main 함수를 보면 eax 값이 1이 아니면 Wrong 이라는 문자열로 보아 sub_804851 의 return 값이 1이 되어야 한다.

 

sub_8048451 함수

함수의 return 값이 1이 되어야 하는데, 위에서 return 을 할 수 있는 루틴은 맨 왼쪽으로 진행되는 루틴 이외에는 존재하지 않는다. 따라서 해당 루틴으로 갈 수 있는 조건들을 확인해 본다.

 

data_804a021 == 0x31
data_804a024 == 0x7c
data_804a025 == 0x00
data_804a022 == 0x7c
data_804a020 == 0x78
data_804a023 == 0xdd

위에서 부터 위의 조건들을 모두 성립해야 return 1을 한다.

data_804a020 == 0x78
data_804a021 == 0x31
data_804a022 == 0x7c
data_804a023 == 0xdd
data_804a024 == 0x7c
data_804a025 == 0x00

이를 순서대로 정리해 보면 아래와 같이 된다.

하지만 중간에 위치한 xor 도 있기 때문에 이를 적용해 본다.

data_804a020 ^= 0x34
data_804a020 == 0x78
data_804a021 == 0x31
data_804a022 ^= 0x32
data_804a022 == 0x7c
data_804a023 ^= -0x78
data_804a023 == 0xdd
data_804a024 == 0x7c
data_804a025 == 0x00

이를 최종적으로 정리해 보면 아래와 같은 조건들을 만족해야 한다.

data_804a020 == 0x78 ^ 0x34        // L
data_804a021 == 0x31                    // 1
data_804a022 == 0x7c ^ 0x32        // N
data_804a023 == 0xdd ^ -0x78        // U
data_804a024 == 0x7c                    // X
data_804a025 == 0x00                    // End of String

그러면 data_804a020 부터 있는 문자열은 어디서 오는지 확인을 해본다.

 

data_804a020 데이터

아까 main 함수에서 sub_8048451 함수를 호출하기 전에 sub_8048434 함수를 호출한다.

이를 확인해 보면, scanf 함수를 호출하는데 입력받은 값을 data_804a020 에 넣는다.

따라서 data_804a020 는 사용자가 입력한 값이며, 이 값이 위의 조건문을 성립할 경우에만 Correct! 라는 문자열을 확인할 수 있다.

 

정답

'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Ransomware  (0) 2019.12.09
Reversing.kr :: Easy_Unpack  (0) 2019.03.01
Reversing.kr :: Easy_Keygen  (0) 2019.02.26
Reversing.kr :: Easy_Crack  (0) 2019.02.24

문제 소개

설명을 보면 뉴비를 위한 뻔한 crackme 문제라고 한다. 바이러니의 checksum 비교를 위하여 hash 값을 비교해 본다.

 

hash 값은 동일하며, 맥에서 사용되는 Mach-0 파일임을 알 수 있다.

 

함수 분석

__int64 start()
{
  void *v0; // ST30_8
  __int64 result; // rax
  char v2; // [rsp+40h] [rbp-30h]
  __int64 v3; // [rsp+68h] [rbp-8h]

  v0 = calloc(0x29BuLL, 4uLL);
  scanf("%s", &v2);
  result = sub_100000DB0(sub_100000D00, sub_100000C80, sub_100000BA0, sub_100000C10, &v2, v0);
  if ( __stack_chk_guard == v3 )
    result = 0LL;
  return result;
}

ida 로 열어보면, start 함수에서는 v0 에 0x29B 만큼 calloc 해주고 v2 에 입력값을 받는다.

그 후 sub_100000DB0 함수에서 여러 인자들로 result 값을 만든다.

 

__int64 __fastcall sub_100000DB0(
  void (__fastcall *a1)(__int64, __int64),          // sub_100000D00
  unsigned int (__fastcall *a2)(__int64, void *),   // sub_100000C80
  unsigned int (__fastcall *a3)(__int64),           // sub_100000BA0
  unsigned int (__fastcall *a4)(__int64),           // sub_100000C10
  __int64 a5,                                       // scanf address
  __int64 a6)                                       // calloc(0x29BuLL, 4uLL)
{
  unsigned int (__fastcall *v6)(__int64);           // sub_100000BA0
  __int64 v7;                                       // scanf address
  __int64 v9;                                       // calloc(0x29BuLL, 4uLL)
  unsigned int (__fastcall *v10)(__int64);          // sub_100000C10

  v6 = a3;
  v10 = a4;
  v7 = a5;
  v9 = a6;
  a1(a6, a5);
  if ( v6(v7) && v10(v9) && a2(v9, &unk_100001040) )
    printf("Congratz - You have earned it!");
  else
    printf("%s", "You've got to do better");
  return 0LL;
} 

sub_100000DB0 함수로 들어가 보면, 인자들을 로컬변수에 넣고 그 값들을 이용하여 입력값이 올바른지 확인한다.

이를 요약하면 아래의 함수들로 정리할 수 있다.

  • al(a6,a5) => sub_100000D00(v0,&v2)
  • v6(v7) => sub_100000BA0(&v2)
  • v10(v9) => sub_100000C10(v0)
  • a2(v9, &unk_100001040) => sub_100000C80(v0,&unk_100001040)

 

__int64 __fastcall sub_100000D00(__int64 a1, const char *a2)
{
  __int64 result; // rax
  int j; // [rsp+0h] [rbp-20h]
  int i; // [rsp+4h] [rbp-1Ch]
  int v5; // [rsp+8h] [rbp-18h]
  int v6; // [rsp+Ch] [rbp-14h]

  v6 = strlen(a2);
  v5 = 0;
  for ( i = 0; i < v6; ++i )
  {
    for ( j = i + 1; j < v6; ++j )
      *(_DWORD *)(a1 + 4LL * v5++) = a2[j] + a2[i];
  }
  result = a1;
  *(_DWORD *)(a1 + 2668) = 0;
  return result;
}

sub_100000D00 함수에서는 a1 을 이용하여 a2 값을 생성한다.

 

_BOOL8 __fastcall sub_100000BA0(__int64 a1)
{
  signed int i; // [rsp+4h] [rbp-18h]
  int v3; // [rsp+8h] [rbp-14h]

  v3 = 0;
  for ( i = 0; i < 37; ++i )
    v3 += *(char *)(a1 + i);
  return v3 == 3504;
}

sub_100000BA0 함수에서는 a1의 배열 값들을 더해서 3504 가 맞으면 True 값을 리턴해준다.

 

_BOOL8 __fastcall sub_100000C10(__int64 a1)
{
  signed int i; // [rsp+4h] [rbp-18h]
  int v3; // [rsp+8h] [rbp-14h]

  v3 = 0;
  for ( i = 0; i < 667; ++i )
    v3 += *(_DWORD *)(a1 + 4LL * i);
  return v3 == 126144;
}

sub_100000C10 함수에서는 a1의 배열 값들을 더해서 126144 가 맞으면 True 값을 리턴해준다.

 

_BOOL8 __fastcall sub_100000C80(__int64 a1, __int64 a2)
{
  signed int v3; // [rsp+0h] [rbp-1Ch]

  v3 = 0;
  while ( v3 < 666 )
  {
    if ( *(_DWORD *)(a1 + 4LL * v3) == *(_DWORD *)(a2 + 4LL * v3) )
      ++v3;
  }
  return v3 >= 666;
}

마지막으로 sub_100000C80 함수에서는 a1의 값과 a2의 값이 같은지 확인하는데, 그 길이가 666인지 확인한다.

여기서 a1은 입력값을 기반으로 만든 배열로 a2의 길이가 길이가 666일 경우, a1의 길이가 37인 것을 알 수 있다.

 

위의 함수들을 정리해 보면

  • 입력값의 길이는 37
  • 입력값을 이용하여 만든 배열을 바이너리에 있는 값과 비교
  • 입력값의 문자열들의 총합은 3504

인 것을 알 수 있다.

이제 이를 구하는 간단한 파이썬 코드를 작성한다.

 

flag 구하기

with open('cliche_crackme','rb') as rb:
    rb.seek(0x1040)
    data = rb.read(666*4)
    for i in range(0x21,0x80):
        try:
            rev = chr(i)
            for j in range(0,len(data),4):
                rev += chr(data[j]-ord(rev[0+(len(rev)-1)//37]))
        except:
            a5,chk = rev[:37],0
            for j in range(len(a5)): chk += ord(a5[j])
            if chk == 3504: print('input :',a5)

플래그를 구할때는 거꾸로, 바이너리에서 데이터를 읽어오고 그 값에서 임의의 값을 빼서 문자열을 추측해 본다.

임의의 값을 기반으로 생성한 문자열의 값들을 합한 값이 3504 인지 확인하고, 맞을 경우 이를 출력해 주는 코드이다.

 

숫자, 대문자, 소문자를 거쳐 i로 시작하는 경우에만 그 문자열의 합이 3504이며 플래그를 확인할 수 있다.

'0x00 Reversing > 0x02 CTF' 카테고리의 다른 글

THC CTF 2018 :: Android Serial (with Z3 solver)  (0) 2019.10.13

Intro

MAC 에서는 QuickTime Player 을 이용하여 화면을 쉽게 녹화할 수 있다.
하지만 이는 mov 포맷으로 저장되며, 가끔 mp4 포맷이 필요한 경우가 생긴다.
그래서 오늘은 mov 포맷을 mp4 로 변환하는 방법을 공유한다.
 

방법

온라인 도구를 이용하여 변환시킬 수 있으며, 로컬에서 유로 프로그램 없이 변환할 수 있다.
그 방법은 ffmpeg 를 이용하는 것이다.
ffmpeg 는 brew 를 이용하여 설치할 수 있으며, 여러 미디어 포맷을 다른 포맷으로 변환시킬 수 있다.
mov -> mp4
ffmpeg -i filename.mov filename.mp4
mov -> avi
ffmpeg -i filename.mov filename.avi
대표적으로는 위와 같은 변환을 할 수 있으며, 다른 미디어 포맷도 지원한다.
 

결과

변환하기 전과 변환된 후의 파일들을 비교해 보면
용량은 절반 정도로 감소되며, bitrate 역시 절반 정도로 감소가 된다.
하지만 화면 녹화의 경우 bitrate가 높아서 변환된 영상의 글을 읽는데 문제가 없다.
더 자세한 정보는 [ffmpeg reference] 에서 찾을 수 있다.

'0xF0 ETC >  Tips' 카테고리의 다른 글

[ Tips] Android File Transfer 자동 실행 방지  (0) 2020.03.13
[ Tips] PDF 합치기  (0) 2019.03.09

+ Recent posts