이 글은 쿠키런 2013/5/30 일 버전에 대한 분석정리내용이다.
일단 쿠키런을 실행시켜놓고 adb shell 상에서 ps 명령으로 패키지명을 확인해보자...
확인해보니 com.devsisters.CookieRunForKakao 라는 패키지명이다.
그렇다면 apk 파일도 이 이름을 사용할테니 find 로 아래처럼 검색해서 찾고 adb pull 로 꺼내오자.
일단 이렇게 apk 파일을 추출한 다음 apktool 로 xml 리소스를 복원하고 dex2jar 를 이용해서 smali 코드를 자바 class 바이트코드로 변환한 뒤에 jad 디컴파일러를 이용해서 최종적으로 자바 디컴파일 코드를 얻어내자...(apktool, dex2jar 쓸때 jdk 환경변수 설정과 기타 여러가지 세팅 잘해줘야 한다, 좀 까다로운편임)
apktool 로 아래처럼 리소스들을 복원했다.
메니페스트 xml 파일을 확인해보니 액티비티 엔트리포인트는 OvenbreakX 라는 클래스이다.
디컴파일된 소스상에서 해당 클래스를 확인해보았다.
확인해보니 onCreate 에서는 기본적인 작업만 수행하고 loadLibrary 로 game 이라는 라이브러리를 로딩한다. dex2jar 가 풀어낸 결과물을 뒤져보니 so 파일이 있었다. 확인해보니 스트립된 ARM 공유 라이브러리이다.
IDA 로 확인해보니 거의 게임 전체가 JNI 로 만들어져 있는것 같다.
대강 훑어보니 libcurl, libxml 등등이 존재하고, Cocos2dx 라는 2D 게임개발용 그래픽 라이브러리가 포함되어 있다.
어느정도 분석해보니 게임 전체가 JNI 로 개발된 것 같았다. 일단 스트립 바이너리라 분석이 어려워서 얼마 분석하지는 못했지만 그래도 이 바이너리상에 게임 로직을 제어하는 부분이 존재할 것이므로 해당 로직을 모두 간파해서 변경한다면 게임을 원하는대로 주무를 수 있을 것이다.
일단 이 바이너리를 변조해서 게임을 플레이하는것이 먹히는지 테스트해보자. 아래처럼 Padding 문자열들중 하나를 4 -> 3 으로 변경시키고 이것을 adb push 해주고 쿠키런을 실행시켜보았다. 그러나 이렇게 하니 로그인은 성공하는데, 게임시작을 누르는순간 프로그램이 종료된다.
그런데 바이너리 분석중에 파일을 열어서 MD5 해시를 계산하는 것으로 추정되는 부분이 발견되었다. 그래서 메모리상에는 변조된 바이너리를 로딩시키고 게임시작을 누르기전에 파일을 원본으로 교체하면 무결성 검증을 우회할 수 있지 않을까 싶어서 시도해보았으나 처음에는 성공적으로 우회되서 게임이 진행되는듯 하다가 얼마 지나지않아 다시 게임이 종료되었다. 아무래도 메모리 무결성 검증도 하는 모양이다...
그렇다면 바이너리를 변조하기 위해서는 먼저 무결성 검증이 어떻게 이루어지는지를 파악하고 이것부터 우회해야 하는데 스트립 바이너리인지라 이것이 매우 어려울듯 하다. 나에게는 루팅된 폰이 있었기 때문에 또다른 책략으로 libc 를 후킹해서 "libgame.so" 에게 조작된 정보를 넘겨주는 방법을 생각해 보았다. libgame.so 의 무결성은 검증해도 이 라이브러리가 사용하는 atoi 와 같은 libc의 무결성까지 검증하지는 않을 것이기 때문에 API 후킹을 통해 이부분에서 게임리절트를 조작할 수 있지 않을까 하는 생각이 들었다.
위의 그림처럼 LD_PRELOAD 환경변수를 이용해서 후킹을 해보려고 했는데... 생각해보니 LD_PRELOAD 후킹은 로컬 쉘상에서만 가능한 방법임을 깨닳았다 -_- 따라서 내가 원하는 API 후킹을 위해서는 아예 오리지널 libc 자체를 재컴파일하고 OS 전체가 이를 사용하도록 해야 했다... 매우 귀찮아질것 같아서 이 방법또 포기했다.
그렇다면 서버와 통신하는 네트워크쪽을 파헤쳐보자... 분명 SSL 로 암호화된 통신을 할테니 헛걸음일거같지만 혹시나하는 마음에 게임 클라이언트 - 서버간의 전체통신을 모니터링 해보기로했다. 스마트폰 어플리케이션의 네트워크 모니터링을 하기위한 방법으로는 WiFi 를 사용하도록 해놓고 공유기 내부 사설망에서 ARP Spoofing 을 이용하는것이 가장 좋은 방법인것 같아서 해당 환경을 위한 준비를 했다.
일단 가상머신을 브릿지모드로 설정하여 공유기에 접속시킨다음 네트워크 인터페이스를 확인하자.
게이트웨이의 MAC 주소를 아래처럼 확인하자.
스마트폰을 공유기에 물린뒤에 ping 을 때리고...
ARP 테이블 엔트리가 채워진것을 확인하면 ARP Spoofing 의 모든 준비가 끝난다.
스마트폰쪽으로 내가 게이트웨이라는 ARP 응답을 날려주고, 게이트웨이쪽에는 내가 스마트폰이라는 ARP 응답을 날리자.
그리고 IP 를 포워딩 시켜주면...
스마트폰과 서버의 통신을 Wireshark 로 모니터링할 준비가 완료되었다. 이제 게임을 한판하면서 통신내역을 확인해보니 잘 잡힌다...
놀랍게도 SSL 을 쓰고있지 않았다! HTTP 를 가지고 통신을 하는것 같은데 이를 필터링해보니 핵심적인 부분은 아래와 같았다.
게임 클라이언트가 접근하는 URL 에 웹브라우저로 직접 접속해보니 아래와 같았다...!
JSON 으로 웹소켓 통신을 하고있었다... 게임 시작을 하게되면 beforePlay.ds 에 뭔가를 전송하고 게임이 끝나면 afterPlay.ds 로 뭔가를 전송한다... 게임 클라이언트가 서버로 보내는 HTTP POST 페이로드상에 isEncryptedData=1 이라는 필드가 보이는것으로 보아 뭔가 암호화를 하는것으로 추정된다... JSON, Websocket 의 기본 암호화들에 대해 조사해보니 관련된게 없다... 아무래도 자체적인 암호화를 쓰는것 같다.
일단 인코딩은 딱봐도 Base64 스러우므로 당장 Base64 디코딩을 해보았다. 그런데 거의 다 풀리기는 하는데 마지막쯤에 Invalid Format 이라고 한다 -_- Base64 가 아닌건가? 하고 있었는데 해은형님께 여쭤보니 Base64URL 인코딩일것이라고 조언해주셨다...
확실히 Base64URL 인코딩인것 같았다. 파이썬으로 이를 디코딩해보니...
역시나 바이너리 데이터가 인코딩 된것 같다. hexdump 로 필터링해보니 아래와같다.
암호화된 데이터치고는 엔트로피가 너무 작다... 일단 혹시나 싶어서 게임리절트가 암호화되서 서버로 날아가는 것을 캡춰해서 그대로 반복해서 날려줬더니.. 어라? 점수랑 돈이랑 경험치가 올라갔다 ㅡㅡ;; 리플레이 어택이 가능한 것이다. 그래서 최선을 다한 한판을 플레이하고 그 기록을 서버로 보내는 과정을 하트가 생길때마다 반복시켰다... 하지만 이 방법으로는 완전한 점수제어는 할수 없다. 만약 암호화도 풀 수 있다면 재미있어질 것이다...
이제부터는 바이너리를 분석해서 암호화루틴을 발견해가고 분석한 과정이다.
일단 URL 경로에 대한 문자열 검색을 해보았더니 아래처럼 관련 부분을 발견할 수 있었다.
sub_17BD70 에서 해당 문자열을 참조하므로 해당 함수 디컴파일결과를 확인해보니 다음과 같다
sub_340280 의 두번째 파라미터로 문자열을 넘긴다... 다시 해당함수의 디컴파일 결과를 보면 다음과 같다
두번째 파라미터에 넘어온 문자열을 첫번째 파라미터의 포인터와 memcmp 한다.
그렇다면 다시 sub_17BD70 으로 돌아가서 이 함수를 역참조하는 다른 함수를 찾아보자.
직접적으로 역참조되는 함수는 없지만 data 섹션의 함수포인터가 참조하고 있었다.
참고로 여기서 +1 이 붙는것은 ARM 의 Thumb Mode Switching 때문이다. 아무튼 이 함수포인터가 있는 근처의 데이터영역을 참조하는 다른 함수들을 좀 따라가봤지만 막다른 길인듯 하였다. 생각해보니 암호화를 수행하는 루틴을 찾으려면 암호화된 데이터를 전송하는 URL 문자열을 역참조해서 추적하는것보다 암호화된 데이터에 대한 POST 페이로드 부분의 문자열 조각을 역참조하는것이 더 가능성이 높겠다는 생각이 들었다. 검색해보니 역시나 아래와같이 isEncryptedData=1 과 같은 문자열을 찾을 수 있었다.
sub_B1F80 함수에서 이 문자열을 참조한다.
해당 함수의 디컴파일 결과는 아래와 같다.
이걸 보는순간 느낌이 딱 왔다. 눈치껏 봤을때는 isEncryptedData=1&data=... 이런 문자열을 생성해내는 루틴인것 같은데 확실하게 확인해보자. 조사해야할 함수들은 sub_341B74, sub_B3758, sub_3416F0, sub_341368, sub_3415B0, sub_3411D4 인데 여기서 함수들의 주소가 대부분 바이너리상에 근접한곳에 모여있지만 sub_B3758 혼자서 다른동네에 위치하고있다. 이것으로 미루어 해당 함수만 뭔가 남다른 일을 수행할것같다는 느낌이 든다...
일단 sub_341B74 디컴파일 결과부터 보자.
"isEncryptedData=1" 문자열의 길이를 구하고 그것을 자기자신의 index 로 한 포인터를 v5 에 저장 즉, 문자열의 끝을 v5 에 저장하고 문자열의 시작은 v3 에 복사한다. 그리고 sub_341A48(v3, v5) 를 호출하고 그 결과를 리턴한다.
다시 sub_341A48 의 디컴파일 결과를 보자
예외처리로 보이는 부분들은 분석에서 제외하고, 먼저 sub_340400 을 호출하는데 첫번째 파라미터로 a2 - a1 을 넘긴다. 이는 문자열의 끝포인터 - 문자열 시작포인터 이므로 문자열의 길이가 된다. 그럼 다시 sub_340400( 문자열길이, 0 ) 의 디컴파일 결과를 보자.
여기서 a2 는 0 으로 고정되어있으므로 a1(문자열의 길이) 가 0 이상이라면 if 문 안으로 들어간다. 첫번째 중첩 if 문은 진입할리가 없고, v3 에
a1+29 > 0x1000 의 조건식 결과가 들어간다. 보아하니 문자열의 길이가 4096 정도로 매우 길때에 대한 처리인듯 하다, 따라서 여긴 분석할 필요가 없다. if 문을 다 빠져나가면 마지막에 new( v2 + 13 ) 으로 문자열의 길이 + 13 바이트만큼을 할당하고 그 포인터를 리턴한다.
이제 다시 앞전의 sub_341A48 로 돌아가자.
sub_340400 가 리턴한 결과는 문자열의 길이 + 13바이트 버퍼에 대한 포인터였다. if~else 문을 빠져나온뒤에 memcpy 를 보면 목적지 포인터 v6 으로 v2 에서부터 v3 만큼을 복사한다. 여기서 v6 는 v4+12 인데, v4 는 아까전에 문자열길이 +13 크기의 버퍼 시작주소이다. v3 의 경우 문자열의 길이이다. 즉, 처음에 +13 바이트를 더 할당했던 이유는 문자열 포인터 앞에 12바이트의 다른 메타데이터를 위한 메모리를 확보하고 마지막 null 문자가 들어갈 1바이트를 추가확보하였기 때문이라고 생각할 수 있다.
[12바이트 헤더][ 문자열 본체 ][null]
이런 자료구조를 만들어서 다루고있다고 추측할 수 있다... 아마도 12바이트 헤더는 문자열의 길이, 속성, 타입 등등을 저장하기 위해 쓰일것으로 추정된다. 이제 다시 돌아오자. sub_341B74 에 대한 분석결과는 결국 두번째 파라미터로 주어진 문자열에 대해서 [12바이트 헤더][ 문자열 본체 ][null] 이런 자료구조를 동적할당하고 문자열 본체의 포인터를 첫번째 파라미터를 통해 리턴하는 역할을 한다고 추측 할 수 있다.
그 아래에 있는 함수는 아쉽게도 동적바인딩 되서 호출되는 함수라서 정적분석이 안된다. 하지만 v2+72 의 함수포인터가 호출되면서 파라미터로 v2 를 전달하는 것으로 보다 분명 C++ 과 같은 언어에서의 class 함수 호출부분일 것이다. 첫번째 파라미터인 v10 이 output 포인터인것 같은데, 이 함수는 v2 가 가리키는 객체포인터로부터 그 속에있는 어떤 데이터를 v10 으로 출력하는 것 같다. 그다음 sub_B3758 이 이 포인터를 받아서 어떤 처리를 하고 결과를 v11 로 출력하는것으로 예상된다...
이제 sub_B3758 의 디컴파일 결과를 분석해보자.
sub_341B74 의 두번째 파라미터에 "dev!Wkwkdwhgdk(짜장좋아)!sisters@#$" 라는 매우 수상한 문자열이 발견되었다. 일단 sub_341B74 함수의 역할을 확인해보자. 앗!? 그런데 생각해보니 sub_341B74 는 이미 앞에서 분석이 완료된 함수였다.
"sub_341B74 에 대한 분석결과는 결국 두번째 파라미터로 주어진 문자열에 대해서 [12바이트 헤더][ 문자열 본체 ][null] 이런 자료구조를 동적할당하고 문자열 본체의 포인터를 첫번째 파라미터를 통해 리턴하는 역할을 한다고 추측 할 수 있다."
이렇게 이미 추론을 했었다. 그 아래에는 3837AC 번지에서 마찬가지로 v17에 데이터를 읽어오는데 이부분은 동적으로 채워지는 부분이라 정적분석시는 무엇이 들어있는지 알수 없다.
객체포인터가(v2-12) null 인지 확인하고 if 문 속으로 들어가면 반복문을 돌면서 sub_3411D4 를 호출하는데 이 함수의 두번째 파라미터를 잘 보면 v5 는 "dev!Wkwkdwhgdk!sisters@#$" 문자열의 베이스 포인터고 v6 % i가 루프카운터인 형태로 v10 과 XOR 되고있는것을 볼 수 있다.
정황상 분명히 i 는 문자열의 길이일 것이다. 즉, "dev!Wkwkdwhgdk!sisters@#$" 의 바이트스트림을 반복적으로 v10 이 가리키는 바이트와
XOR 하고있다. v10 의 경우 v4 가 가리키는 베이스포인터에 v6 카운터가 더해진위치의 바이트인데 v4 는 다시 이 함수의 두번째 파라미터였다.
이 파라미터를 다시 역추적해보면 아래의 상위 함수 B1F80 에서 16번째 라인의 함수가 v10 으로 출력해준 바이트 스트림이었던 것을 알 수 있다.
다시 B3758 로 돌아와보자
이쯤 왔으면 분명 sub_3411D4 는 두번째 파라미터를 첫번째 파라미터가 가리키는 문자열 객체에 concat 시키는 역할을 할 것이라고 추정 할 수 있는데, 이를 좀더 확실하게 디컴파일 결과를 보고 확인해보자.
이미 강한 확신이 들었기 때문에 자세한 분석은 안했지만 앞전에 분석했던 내용과 연관지어볼때 문자열의 길이에 대한 검증같은것을 한 뒤에 21번째 라인에서 바이트포인터에 두번째 파라미터를 복사하는 것을 확인 할 수 있다. 이정도면 거의 확실하다고 봐도 될 것 같다.
이제 다시 B3758 함수에서 XOR 암호화가 모두 진행된 이후에 호출되는 sub_C0C64 함수(48번째줄) 를 확인해보자.
참고로 앞전의 디컴파일 결과에서의 sub_C0C64 는 파라미터 정보가 잘못되어있었다. 뭔가 이상해서 디컴파일을 다시해보니 결과가 달랐다...
이래서 HexRay를 무조건 믿으면 안된다고 하는것같다...
두번째 파라미터로부터 어떤 프로세싱을 해서 첫번째 파라미터로 출력을 하는데 딱보면 쉬프트연산과 0xF 랑 and 연산하고 하는 모양새가 base64 인코딩 하는 연산인것 같다. 정확히 분석 안해도 이정도면 거의 확신하고 넘어갈 수 있을것 같다.
결국 sub_B3758 함수는 다음과 같이 정리해 볼 수 있다.
1. 서버로 전송할 데이터 스트림을 입력받음
2. 데이터 스트림을 XOR 키 "dev!Wkwkdwhgdk!sisters@#$" 로 암호화
3. 암호화된 스트림을 Base64URL 인코딩
이제 그 이후의 함수들을 다시 보자.
21, 22 번째 줄의 sub_3411D4 함수는 이미 character 를 스트링에 cancat 하는 함수로 추정을 끝냈고 sub_341368, sub_3415B0 두개가 남았는데, 안봐도 정황상 첫번째 파라미터가 가리키는 스트링에 두번재 파라미터의 내용을 concat 하는 일을 할거라고 이쯤이면 거의 확신할 수 있다.
혹시나해서 확인해보면...
역시나 append 의 기능을 하는 함수인것 같다. 아마도 원래 이름이 basic_string::append 라는 함수였는데 심볼테이블에서는 strip 되었지만 에러메시지를 출력하는 문자열 로그는 남아있는것 같다. 이런것이 스트립 바이너리 분석에서는 매우 중요한 실마리가 된다.
아무튼 바이너리 분석은 좀 머리가 아프지만 분석된 결과 자체는 매우 단순한 XOR 암호화일 뿐이다. 이것은 파이썬스크립트 10줄정도면 해독하는 루틴을 짤 수 있다.
대칭암호화이므로 암호화도 간단하게 짤 수 있다.
이제 처음에 네트워크상에서 확인했던 암호화된 문자열들에 이를 적용해보자.
서버로 전송하는 JSON 원문 데이터가 나왔다...!! 지금까지 추론했던 모든 분석이 맞았음이 입증되는 순간이다. 이제 이 JSON 원문상의 점수를 7777777 과 같은 임의의 숫자로 변경한뒤에 다시 암호화시키자...
변경된 JSON 내용을 암호화 시킨 스트림을 생성해내고, 각각 beforePlay.ds, afterPlay.ds 로 전송해주는 파이썬코드를 아래처럼 작성하자.
이제 마지막으로 스크립트를 실행시켜보면... 서버에서 COMPLETE 라는 JSON 응답이 날아온다...!!
게임상에 이 점수가 반영되었는지 확인해보자
성공적으로 점수가 반영되었다(4444444 도 이런식으로 만든것이다) 이런 방식으로 돈, 경험치 모두 제어해보았는데 서버가 1회의 play 리절트로 받아들이는 수치의 한계가 있었다(각각 3만코인, 6만 exp 였다). 쿠키런에는 리플레이어택 취약점 또한 존재했으므로 이렇게 최대 코인과 경험치를 하트가 있는한 반복해서 계속 쏘아주니 10분정도만에 레벨 20에서 만랩(50) 까지 도달할 수 있었고 돈 또한 300만코인정도를 얻을 수 있었다. APK 를 추출한뒤부터 게임점수 조작까지 총 3~4일정도 걸린 것 같다. 여기에 분석된 내용은 어느정도 의미있는 분석내용만 있는 것이고 실제로는 훨씬많은 삽질을 하였다. 가장 어려운 부분은 스트립 바이너리를 분석하여 각 함수가 어떤 역할을 하는지 추정하는 부분들이었으며 어느정도 스트립된 의미들을 재구성한 이후부터는 매우 순조로웠다. 암호화루틴파악의 경우 너무 단순한 XOR 방식이라 분석에 거의 시간이 들지 않았다.
아래는 조작과정에 사용된 파이썬 스크립트들이다. DNS Round Robin 을 쓰는지 서버 IP 가 접속할때마다 자주 바뀌고, 로그인 과정의 인증이 아마 IP 와 bind 될 것이기 때문에 이 스크립트는 환경구축을 할 수 없는사람에게는 무용지물이다.
root@ubuntu:/var/www/cookierun# cat decrypt.py
import sys, os, base64
cnt=0
for i in cipher:
decipher += chr( ord(i) ^ ord(key[cnt]) )
cnt += 1
if cnt==len(key):
cnt = 0
import sys, os, base64
cnt=0
for i in decipher:
cipher += chr( ord(i) ^ ord(key[cnt]) )
cnt += 1
if cnt==len(key):
cnt = 0
root@ubuntu:/var/www/cookierun# cat cookiefuck.py
import urllib, urllib2, time, random
ip = '176.34.47.129'
url = "http://"+ip+"/game/beforePlay.ds"
login_form={"isEncryptedData":"1", "data":"H0cbRDoJEhk3EhlFXkkXS1....."}
login_req = urllib.urlencode(login_form)
request = urllib2.Request(url,login_req)
response = urllib2.urlopen(request)
data = response.read()
print data
time.sleep(1)
url = "http://"+ip+"/game/afterPlay.ds"
login_form={"isEncryptedData":"1", "data":"H0cbRDoJEhk3EhlFXkkX.... "}
login_req = urllib.urlencode(login_form)
request = urllib2.Request(url,login_req)
response = urllib2.urlopen(request)
data = response.read()
print data
'Secret' 카테고리의 다른 글
QEMU Internal - Monitor (0) | 2014.03.14 |
---|---|
QEMU Internal - TCG (0) | 2014.03.14 |
QEMU Internal - TLB (0) | 2014.03.13 |
Return to Dynamic Linker Exploit (0) | 2014.02.19 |
QEMU Internal - Memory (0) | 2014.01.28 |