루팅된 Android 환경에서 특정 모바일 애플리케이션을 조작하는 방법은 크게 세가지 정도로 구분할 수 있습니다.


1. ptrace() 시스템 콜 사용

2. LD_PRELOAD 환경변수 사용

3. LKM (Loadable Kernel Module) 사용


다른 방법이 더 존재할지는 모르겠지만 크게 위와 같은 맥락에서 움직이고 있는 것 같습니다.

위 방법들 중에서 오늘은 LD_PRELOAD에 대해 조금 말씀드릴까 합니다.


Android 환경에서 애플리케이션이 실행될때에는 zygote라는 프로세스에 의해 apk파일이 로드되며 실행되는데, 이러한 zygote 프로세스는 전체적으로 애플리케이션의 권한 할당이나 프로세스 생성에 관여하게 됩니다.

zygote 프로세스는 애플리케이션이 실행되는 가장 기본적인 환경이므로 init (pid 1번 프로세스) 프로세스에 의해 관리되고 있으며, init 프로세스는 zygote 프로세스가 정상적인 동작을 하지 않거나 외부환경에 의해 동작을 멈추면 즉시 재시작하도록 되어있습니다.


init 프로세스는 부팅 초기에 init.rc 파일을 읽어들여 환경설정과 함께 zygote 프로세스를 생성하도록 수행합니다.

다음은 init.rc 파일의 일부분입니다.

...

> snip <

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-sys

    class main

    socket zygote stream 660 root system

    onrestart write /sys/android_power/request_state wake

    onrestart write /sys/power/state on

    onrestart restart media

    onrestart restart netd

> snip <

...


zygote의 실제 실행프로그램은 /system/bin/app_process 이며, 옵션에 따라 zygote 프로세스 역할을 수행하는 것으로 보입니다.

zygote프로세스가 실행될 때 LD_PRELOAD 환경변수를 추가하여 실행되도록 하면 특정 라이브러리 함수들을 후킹하여 변조하는 기법이 존재하는데, 대부분 이를 위해 init.rc 파일을 수정하게 됩니다.

그러나 init.rc 파일을 수정하기 위해서는 boot 이미지파일의 압축해제, 수정 및 변조, 재 압축, boot 이미지파일 overwrite 등의 과정을 거쳐야 하므로, 귀찮은 작업이 될 수도 있습니다.

그래서 다른 방법이 없을까 고민해 보다가 다음과 같이 환경변수를 injection 하는 방법을 구상해보았습니다.


[그림 1] LD_PRELOAD injection


"zygote가 정상적인 실행상태가 아니면 init 프로세스에서 zygote 프로세스를 재 시작한다"는 점에 착안하여, 먼저 init 프로세스를 ptrace로 감지하면서 기존에 실행되고 있던 zygote 프로세스에 SIGKILL 시그널을 보냅니다.

그러면 zygote의 부모프로세스인 init 프로세스에서는 SIGCHLD 시그널을 받게 되고, zombie 프로세스가 되지 않도록 zygote가 사용하고 있던 리소스들을 kernel에 반환 후 zygote 프로세스의 재 시작 작업을 수행합니다.

zygote 프로세스의 재시작은 fork 시스템 콜을 수행하여 새로운 프로세스를 생성 한 후, execve 시스템 콜을 사용하여 zygote 바이너리를 실행하는 구조로 되어 있습니다.

바이너리 실행에 사용되는 execve 시스템 콜의 원형은 다음과 같습니다.


int execve(const char *filename, char *const argv[], char *const envp[]);


execve 시스템 콜의 3번째 인자가 envp라고 되어 있는데 바로 이 envp 인자가 실행되는 프로그램의 환경변수를 결정하게 됩니다.

따라서 zygote를 실행하기 위한 execve 시스템 콜 실행 직전에 LD_PRELOAD 환경변수를 injection한다면 zygote는 변조된 환경변수를 가진 상태에서 실행될 수 있습니다.

다음은 이러한 시나리오를 가지고 만든 injector 소스코드의 일부입니다.


/*

* god_daewoong.c

*

* Version: v1.0

* Date: 2014.09.10

*

* You can compile using gcc with -D option: -D_SO_PATH=\"${SO_PATH}\"

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <ctype.h>

#include <errno.h>

#include <sys/wait.h>

#include <sys/ptrace.h>

#include <linux/user.h>

#include <linux/ptrace.h>


#define EXTRA_ENVIRON   "LD_PRELOAD="_SO_PATH

#define ZYGOTE_PATH     "/system/bin/app_process"


long internal_ptrace(int, pid_t, void *, void *);

#define PTRACE(r,p,a,d) internal_ptrace(r,p,a,d)

#define SYSCALL_REGISTER(r) r.ARM_r7

#define IP_REGISTER(r) r.ARM_ip

#define RESULT_REGISTER(r) r.ARM_r0

#define ENV_REGISTER(r) r.ARM_r2


#define MAX_BUFFER_SIZE 1024

#define MAX_PROC_PID    65536


int init_pid = 1;


void sigint(int signo)

{

        fprintf(stdout, "[!] Received Ctrl+C signal.\n");

        if(init_pid != 0) {

                if(PTRACE(PTRACE_DETACH, init_pid, (void*)1, 0) < 0)

                        fprintf(stderr, "[!] PTRACE_DETACH error\n");

        }

        exit(0);

}


long internal_ptrace(int request, pid_t pid, void *addr, void *data)

{

        int ret, stat;


        errno = 0;


        while(1) {

                stat = 0;

                ret = waitpid(pid, &stat, WNOHANG);

                if((ret == pid && WIFEXITED(stat)) || (WIFSTOPPED(stat) && !WSTOPSIG(stat))) {

                        fprintf(stderr, "[!] Killed Process: %d\n", pid);

                        return -1;

                }

                if((ret = ptrace(request, pid, addr, data)) == -1) {

                        switch(request) {

                        case PTRACE_DETACH:

                        case PTRACE_SYSCALL:

                        case PTRACE_KILL:

                                return 0;

                        default:

                        break;

                        }

                } else {

                        break;

                }

        }

        return ret;

}

int doPunchLine(void)
{
        struct pt_regs regs;
        siginfo_t sig;
        int zygote_path_check = 0;
        char buf[128];
        int i, ret = -1;
        int zygote = 0;

        fprintf(stdout, "[*] Get the zygote pid..... ");
        if((zygote = get_zygote_pid()) < 0) {
                fprintf(stdout, "failed\n");
                return -1;
        }
        fprintf(stdout, "%d\n", zygote);

        if(PTRACE(PTRACE_ATTACH, init_pid, (void*)1, 0) < 0) {
                fprintf(stderr, "[!] PTRACE_ATTACH error\n");
                return -1;
        }

        fprintf(stdout, "[*] Attached the init process successfully: %d\n", init_pid);

        kill(zygote, SIGKILL);
        fprintf(stdout, "[*] Sending a SIGKILL signal to zygote\n");

        // XXX: tracing...
        while(1) {
....
> snip <
...
        }

failed:
        if(PTRACE(PTRACE_DETACH, init_pid, (void*)1, 0) < 0) {
                fprintf(stderr, "[!] PTRACE_DETACH error\n");
        }

        return ret;
}

int get_zygote_pid(void)
{
        char fname[64];
        char status[64];
        int i, zygote_pid;
        FILE *fp = NULL;

        zygote_pid = -1;

        for(i = 0; i < MAX_PROC_PID; i++) {
                snprintf(fname, sizeof(fname), "/proc/%d/status", i);
                if((fp = fopen(fname, "r")) == NULL)
                        continue;
                if(fgets(status, sizeof(status), fp) != NULL) {
                        if(strstr(status, "zygote") != NULL) {
                                zygote_pid = i;
                                fclose(fp);
                                break;
                        }
                }
                fclose(fp);
        }
        return zygote_pid;
}

int HerpoongIs0Per(int pid)
{
        void *environ, *mem, *sp, *init_sp, *data;
        int text, j, newdata, empty = 0;
        const char *add = EXTRA_ENVIRON;
        struct pt_regs regs;

        if(PTRACE(PTRACE_ATTACH, pid, (void*)1, 0) < 0) {
                fprintf(stderr, "[!] PTRACE_ATTACH error\n");
                return -1;
        }

        // XXX: Finding execve() syscall
        while(1) {
                if(PTRACE(PTRACE_GETREGS, pid, 0, &regs) < 0) {
                        fprintf(stderr, "[!] PTRACE_GETREGS error\n");
                        goto failed;
                }

                // XXX: execve() sys-call number is '11'
                if(SYSCALL_REGISTER(regs) == 11 && IP_REGISTER(regs) == 0) {
                        environ = (void*)ENV_REGISTER(regs);
                        fprintf(stdout, "[*] Environ: %p\n", environ);
                        break;
                }
                if(PTRACE(PTRACE_SYSCALL, pid, (void*)1, 0) < 0) {
                        fprintf(stderr, "[!] PTRACE_SYSCALL error\n");
                        goto failed;
                }

        }
...
> snip <
...
        if(PTRACE(PTRACE_DETACH, pid, NULL, NULL) < 0) {
                fprintf(stderr, "[!] PTRACE_DETACH error\n");
        }
failed:
        return 0;
}


int main(void)
{
        int new_zygote;

        signal(SIGINT, sigint);

        fprintf(stdout,
                "*****************************************************\n"
                "************ Android LD_PRELOAD Injector ************\n"
                "*****************************************************\n"
                "                Implemented by TeamCR@K in A3Security\n\n"
                "  Greetz to: 1ndr4, bash205, blpark, fr33p13, alrogia\n"
                "                              kgyoun4, maz3, rhaps0dy\n\n");

        if((new_zygote = doPunchLine()) != -1) {
                HerpoongIs0Per(new_zygote);
        }
        fprintf(stdout, "[*] Done\n");
        return 0;
}


악의적으로 사용될 수 있는 우려로 인해 전체 소스코드를 올리지 못하는 점 양해 부탁드립니다.

위 코드를 컴파일 하여 실행하면 다음과 같이 재시작된 zygote의 실행환경에서 특정 라이브러리가 적재되어 실행되는것을 볼 수 있습니다.


[그림 2] Injector 실행 전 zygote의 기본 환경변수


[그림 3] Injector 실행 후 재 시작된 zygote 프로세스에 적용된 LD_PRELOAD 환경변수


[그림 4] LD_PRELOAD에 의해 적재된 libopen.so 라이브러리가 정상적으로 동작한 화면


LD_PRELOAD로 로딩한 Shared Object의 소스코드는 다음과 같습니다.


/* libopen.c */

#include <stdio.h>

#include <stdlib.h>

#include <stdarg.h>

#include <unistd.h>

#include <dlfcn.h>


#define LIBC_PATH       "/system/lib/libc.so"

#define LOG_PATH        "/data/test/openhook.log"


static int (*orig_open)(const char *f, ...) = NULL;


int open(const char *f, ...)

{

        int fd = 0, flags = 0, mode = 0;

        void *dl = NULL;

        va_list args;

        FILE *fp = fopen(LOG_PATH, "a+");


        if(fp == NULL) fp = stdout;


        fprintf(fp, "[HOOK-LIB] Executed open() system call: %s\n", f);

        if((dl = dlopen(LIBC_PATH, RTLD_LAZY)) == NULL) {

                fprintf(fp, "[!] dlopen() function error.\n");

                return -1;

        }

        orig_open = dlsym(dl, "open");

        if(orig_open == NULL) {

                fprintf(fp, "[HOOK-LIB] dlsym() function error.\n");

                return -1;

        }

        va_start(args, f);

        flags = va_arg(args, int); // 2nd argument of open() syscall (flags)

        mode = va_arg(args, int); // 3rd argument of open() syscall (mode)

        va_end(args);

        if((fd = orig_open(f, flags, mode)) < 0) {

                fprintf(fp, "[HOOK-LIB] orig_open() function error: %s\n", f);

                return -1;

        }

        dlclose(dl);

        fclose(fp);

        return fd;

}


zygote 프로세스가 실행되는 시점에 환경변수를 추가하여 실행하는 형태의 인젝션 방식은 init.rc 파일을 손상시킬 우려가 없으며, 모바일 기기를 재시작하면 본래의 시스템 환경으로 동작하게 됩니다.

injector의 Full Source code를 올려드리지는 못하지만 특정 환경변수를 추가하여 zygote를 실행하도록 되어 있는 pre-compiled binary를 대신 올려드립니다.


god_daewoong-poc


위 바이너리를 실행하면 다음과 같이 특정이름의 환경변수가 추가되어 동작하는 zygote를 확인 하실 수 있습니다.


[그림 5] 테스트 injector 동작 화면


본 injector 프로그램은 루팅된 Android 버전에서 테스트 되었으며, 다음과 같은 디바이스 정보를 가진 기기에서 테스트 되었습니다.


[그림 6] 테스트 환경


감사합니다.

Android Device를 이용한 동적 디버깅

스마트폰 2013. 4. 26. 04:23 Posted by TEAMCR@K

본 문서는 Android APK 파일을 Device에서 동적 디버깅 방법에 대해 작성 하였습니다.

안드로이드 기반 동적 디버깅 환경 구축(emulator): http://teamcrak.tistory.com/342

 

테스트 환경은 아래와 같습니다.

  • OS: Window 7
  • Java version: jdk6
  • APKTools: 1.4.1
  • NetBeans IDE: 7.3
  • APK Sign tool
  • Galaxy S2

APKTool 1.4.1 버전 사용 이슈

APK Tool 1.4.1 이상 버전부터 디버그모드(-d) 일 때 java파일이 생성되지 않아 동적 디버깅을 할 수 없다고 합니다.

http://i5on9i.blogspot.kr/2012/10/smali-apk-debugger.html

 

우선 APKTool 1.4.1 다운로드 합니다.

APKTool 1.4.1: https://android-apktool.googlecode.com/files/apktool1.4.1.tar.bz2

기존 APKTool을 사용하고 있었다면 다음경로의 파일을 삭제해야 에러가 발생하지 않습니다.

C:\Users\<USER_NAME>\apktool\framework\1.apk

다운로드 후 디버깅옵션으로 smali code를 생성합니다.

apktool.bat d –d <file>.apk

[그림1] APKTool 을 이용한 smali code 생성

 

AndroidManifest.xml 파일에서 아래 항목을 추가해줍니다.

Android:debuggable="true" //디버깅 허용

-------------------------------------------------------------------------------------------------------

<intent -filter="-filter">

<action android:name="android.intent.action.MAIN">

// 테스크의 첫 액티비티로 액티비티를 시작한다.

<category android:name="android.intent.category.LAUNCHER">

//액티비티가 어플리케이션의 런처에 표시되고 테스크의 첫 액티비티가 될 수 있다.

</intent>

 

[그림2] AndroidManifest.xml 패치

 

추가 및 패치하기 전 DDMS 화면을 보면 attach 할 수 있는 프로세스가 없습니다.

[그림3] attach 프로세스 유무 확인

 

다시 apktool로 패키징 후 코드사인 합니다.

[그림4] 패치후 리패키징

 

패치 된 APK 설치 전 디바이스에서 환경설정 > 개발자 옵션 > USB디버깅 체크해줍니다.

[그림5] USB 디버깅 모드

 

어플리케이션 설치 후 연결된 디바이스에서 attach 할 수 있는 프로세스를 확인할 수 있습니다.

[그림6] 접근 가능한 프로세스 확인

 

NetBeans 7.3 버전들 다운로드 후 설치합니다.

NetBeans Download: https://netbeans.org/downloads/

NetBeans 설정은 다음과 같이 진행합니다.

[그림7] 새 프로젝트 작성

 

[그림8] 프로젝트 위치 지정

 

APKTool 1.4.1 로 디코딩 한 smali 경로를 설정해 줍니다.

[그림9] smali code 위치 설정

 

[그림10] API Level에 맞춰 라이브러리를 설정

 

[그림11] API Level에 맞춰 라이브러리 추가

 

[그림12] 테스트로 작성된 어플리케이션 실행

 

Debug > Attach Debugger 를 선택합니다.

[그림13] Attach Debugger

 

[그림14] 호스트, 포트 입력

 

Attach 에 성공하면 형광색 아이콘이 생겨 디바이스에서 디버깅을 시작할 수 있습니다.

[그림15] 디버깅 시작

 

지금까지 디버깅 환경 설정을 살펴보았습니다.

이제부터 실제로 디버깅을 시작하기 위해 어플리케이션 실행 시부터 디버깅 할 수 있는 방법에 대해 알아보겠습니다.

디버깅을 어플리케이션이 시작과 동시에 하기 위해서 smali code에서 onCreate 메소드 호출 시 'invoke-static {}, Landroid/os/Debug;->waitForDebugger()V' 를 삽입해 줍니다.

[그림16] 디버깅 대기를 위한 코드패치

 

코드 패치 + 리패키징 + 코드사인 후 어플리케이션을 설치합니다.

어플리케이션을 실행하게 되면 DDMS 에서 붉은색 아이콘이 생성되며 attach 전까지 대기하게 됩니다.

[그림17] 디버깅 대기

 

[그림18] 디버깅 전 화면

 

NETBeans에서 attach 하기 전 ctrl+shift+F8 단축키를 눌러 bp를 설정합니다.

[그림19] break point 설정

 

Attach 시 debugging wait 에서 벗어나 호출된 함수에서 break point 가 걸리게 됩니다.

[그림20] break point 지점 확인

 

[그림21] 라인별로 smali code, java code를 보며 디버깅을 시작

 

[그림22] 어플리케이션 시작

 

버튼 이벤트 발생 시 break point를 설정하여 디버깅 할 수 있습니다.

Ctrl+shift+F8 단축키를 이용하여 break point 지점을 설정합니다.

[그림23] 버튼 이벤트 메소드 확인

 

[그림24] 버튼 이벤트 동작

 

[그림25] 동작 지점 확인

 

[그림26] 디버깅 메뉴를 이용하여 분석

 

이상으로 디바이스에서 APK 동적 디버깅 환경을 살펴 보았습니다.

 

마지막으로 APK 동적 디버깅 순서 정리하면 아래와 같습니다.

  1. APKTool을 이용한 디코딩 (Apktool d –d <file>.apk)
  2. Device 설정(USB디버깅 모드) 및AndroidManifest.xml 수정
  3. MainActivity에 debugging wait 함수 추가
  4. 어플리케이션 리패키징 및 코드사인 후 설치
  5. NETBeans설정 후 호출되는 Method break point 설정
  6. Attach Debug 후 디버깅을 시작

 

 

참고자료

http://d-kovalenko.blogspot.kr/2012/08/breakpoints-in-smali-code.html

http://d-kovalenko.blogspot.kr/2012/08/debugging-smali-code-with-apk-tool-and.html

http://dztall.blogspot.kr/2011/01/blog-post.html

http://i5on9i.blogspot.kr/2012/10/smali-apk-debugger.html

http://code.google.com/p/android-apktool/issues/detail?id=339