일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- vcruntime.dll
- pcap packet
- Windows Kernel Debug
- apphelp.dll
- HackCTF
- 윈도우 커널 드라이버
- 개발 환경 준비
- 개발하기
- Windows
- pwnable
- Windows Kernel
- C언어 패킷캡쳐
- pcap packet capture
- 윈도우 커널
- 해킹
- Network Byte Order
- hacking
- vcruntime140.dll
- 시스템해킹
- 바이트 오더
- 윈도우 커널 디버깅
- packet capture
- IAT Hooking
- windows kernel debugging
- 네트워크 바이트 오더
- Windows Kernel Driver
- ucrtbase.dll
- Msvcrt.dll
- arudino
- 포너블
- Today
- Total
미친해커
[Android] Run Applications as Root for Android 본문
글을 시작하기 전 양해 말씀드립니다. 저는 안드로이드, 리눅스 운영체제에 대해 자세히 알지 못해 인터넷 검색과 연구하면서 알아낸 결과로 추측하여 글을 작성했습니다. 잘못된 정보가 있다면 댓글 부탁드립니다.
How do you typcially run an application as root?
Root 권한으로 애플리케이션을 실행하는 방법은 안드로이드 5.0을 기준으로 이전과 이후로 나뉘게 된다.
또한 현재 안드로이드 운영체제는 기본적으로 시스템 앱(System Application)이 아닌 일반 앱(Application)은 Root 권한으로 실행할 수 없다고 한다.
Before Android 5.0
안드로이드 5.0 이전에는 AndroidManifest.xml
에 android.permission.ACCESS_SUPERUSER
권한을 선언하면 가능하다.
After Android 5.0
안드로이드 5.0 이후에는 일반 앱을 Root 권한으로 실행하는 것은 불가능하다. Android 5.0 Lolipop
에서부터 android.permission.ACCESS_SUPERUSER
가 없어지게 돼 사용되지 않다.
다음 사이트에서 그 내용을 확인할 수 있다.
How applications run on Android (Background Knowledge)
안드로이드 5.0 이후에서 애플리케이션을 Root 권한으로 실행시키는 것이 완전히 불가능한 것은 아니다.
Root 권한으로 애플리케이션을 실행하기 위해서는 먼저 안드로이드의 애플리케이션이 어떤 원리로 실행되는지 알아야 할 필요가 있다.
What language is the application written?
우리가 알고 있다시피 안드로이드 애플리케이션은 주로 Java(자바)로 작성되어 있다. 그렇기 때문에 리눅스인 안드로이드에서 네이티브 프로세스로 실행될 수 없다.
Dalvik
Dalvik은 VM이며 DVM이라고도 불린다. Android 4.4(API 19)에서 처음 등장했으며 자바를 구동하는 JVM 대신 사용된다.
Android가 Java를 사용하면서 JVM을 사용하지 않고 DVM을 사용하는 이유는 라이선스와 메모리 효율성 등의 문제가 있다. JVM이나 DVM이나 모두 Java를 사용한 VM이기에 애플리케이션을 동작하는데 문제는 존재하지 않는다.
Zygote
앞서 말했 듯이 안드로이드 애플리케이션은 달빅(Dalvik) 가상 머신에서 동작한다. 각 안드로이드 애플리케이션은 독립적으로 가상 머신 위에서 동작할 것이며 실행될 때마다 가상 머신을 초기화한다. 즉, 안드로이드 애플리케이션이 실행될 때마다 많은 과정이 필요하고 이로 인해 많은 시간이 소요 돼 애플리케이션의 실행을 지연시킨다.
안드로이드에는 Zygote(자이고트)라는 이름의 프로세스가 존재하는데 Zygote는 애플리케이션이 실행되기 전에 가상 머신의 코드 및 메모리 정보를 공유함으로써 애플리케이션이 실행되는 시간을 단축시킬 수 있다.
Zygote는 가상 머신의 코드 및 메모리뿐만 아니라 안드로이드 프레임워크에서 동작하는 애플리케이션이 사용할 클래스와 자원을 미리 로딩해 해당 자원에 대한 연결 정보를 구성한다. 이 덕분에 새로 실행되는 애플리케이션은 필요한 자원들에 대한 연걸 정보를 매번 구성하지 않아도 되기 때문에 빠르기 실행된다.
Process creation(Application execution) via Zygote
Zygote는 안드로이드 애플리케이션이 실행될 때 필요한 달빅 가상 머신의 초기화와 자원들을 미리 로딩되어 있는데 어떻게 이 리소스를 사용해 애플리케이션을 실행시킬 수 있을까? 바로 fork(clone)
시스템 콜이다.
fork 함수는 새로운 자식 프로세스를 생성하고 새로 생성된 프로세스는 부모 프로세스의 메모리 구성 정보 및 공유 라이브러리에 대한 링크 정보를 공유한 상태이다. 프로세스가 그대로 복제되는 셈이다. Zygote는 fork를 사용해 자신을 복제한 후 그 위에 안드로이드 애플리케이션을 로드함으로써 애플리케이션을 실행한다.
What is app_process?
Zygote 또한 자바로 작성되어 있어 다른 네이티브 서비스나 데몬처럼 init 프로세스에서 바로 실행할 수 없다. 그렇기 때문에 Zygote는 app_process를 통해 Dalvik VM을 초기화한 후 ZygoteInit
클래스를 로딩해 실행하는 절차를 거친다.
init 프로세스는 app_process와 더불어 다양한 데몬 프로세스를 실행시키는데 이는 /init.zygote64_32.rc
파일에서 확인할 수 있다. (AArch64의 경우 x86과 x64 두 개의 zygote가 실행된다. 해당 글에서 zygote는 app_process64만을 의미한다)
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart audioserver
onrestart restart cameraserver
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks
service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
class main
socket zygote_secondary stream 660 root system
onrestart restart zygote
writepid /dev/cpuset/foreground/tasks
위 정의에서 알 수 있듯이 app_process는 zygote의 "진짜 이름"으로 app_process가 init 프로세스의 의해 서비스 데몬으로 실행되어 ZygoteInit 클래스
를 로딩해 Zygote를 실행한다.
/system/bin/app_process
는 /system/bin/app_process64
의 심볼릭 링크이다.
Guess the workflow of Zygote
Zygote가 안드로이드 애플리케이션을 실행시킨다는 사실을 우리는 알았을 때 바로 알 수 있는 한 가지 있다. 바로 Zygote는 init 프로세스의 의해 서비스 데몬으로 실행되기에 Root 권한으로 실행된다는 것이다. 그렇다면 Zygote의 자식 프로세스로 실행되는 안드로이드 애플리케이션도 "원래"는 Root 권한으로 실행되었던 적이 있다는 사실이다.
com.termux
애플리케이션의 부모와 조부모인 zygote
와 init
는 Root
권한이지만 com.termux
애플리케이션은 Root 권한이 아닌 일반적인 애플리케이션 권한으로 내려가 있는 것을 확인할 수 있다.
위와 같은 사실을 통해 Zygote가 안드로이드 애플리케이션을 로드한 후 자신의 권한을 낮춘 다음에 안드로이드 애플리케이션(MainActivity)을 호출한다는 의미가 된다.
Zygote source code anaysis
Zygote 프로세스의 워크플로우를 추측했으니 이제 Zygote의 소스코드를 분석해보려고 한다.
(안드로이드 7.1.2 Release 39 기준)
중요하다고 생각되는 함수가 호출되는 윗 라인에 주석을 달아놓았고 주석에 1), 2) 와 같이 작성되어 있는 함수는 후에 서술할 안드로이드 애플리케이션을 Root 권한으로 실행하는데 영향을 끼치는 함수들로 아래에서 따로 부가적인 설명이 존재한다.
ZygoteInit.main()
부터 안드로이드 애플리케이션 호출까지의 과정은 더보기를 눌러주세요
ZygoteInit.main()
부터 안드로이드 애플리케이션의 호출까지의 과정이다.
Call Trace[1] core/java/com/android/internal/os/ZygoteInit.java - main()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/java/com/android/internal/os/ZygoteInit.java#712 Line 712 */ public static void main(String argv[]) { /* 중간 생략 */ try { /* 중간 생략 */ boolean startSystemServer = false; String socketName = "zygote"; String abiList = null; for (int i = 1; i < argv.length; i++) { if ("start-system-server".equals(argv[i])) { // Zygote를 호출할 때 --start-system-server 플래그가 존재 startSystemServer = true; } else if (argv[i].startsWith(ABI_LIST_ARG)) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { socketName = argv[i].substring(SOCKET_NAME_ARG.length()); } else { throw new RuntimeException("Unknown command line argument: " + argv[i]); } } if (abiList == null) { throw new RuntimeException("No ABI list supplied."); } /* 안드로이드 애플리케이션 실행을 위한 요청 수신을 받기 위한 소켓 등록 UDS(Unix Domain Socket)을 사용하며 init.rc에서 app_process를 실행할 때 등록했던 소켓을 이용 socket zygote stream 660 root system */ registerZygoteSocket(socketName); /* 중간 생략 */ /* Classes, Resources, OpenGL, Shared Libraries, Text Resources 등을 로드 내부에서 다음 각 함수를 호출 preloadClasses() preloadResources() preloadOpenGL() preloadSharedLibraries() preloadTextResources() */ preload(); /* 중간 생략 */ /* UDS를 모니터링 하면서 안드로이드 애플리케이션 생성 요청을 처리하는 루프를 실행 */ runSelectLoop(abiList); closeServerSocket(); } catch (MethodAndArgsCaller caller) { caller.run(); } catch (Throwable ex) { Log.e(TAG, "Zygote died with exception", ex); closeServerSocket(); throw ex; } }
Call Trace
[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/java/com/android/internal/os/ZygoteInit.java#825 Line 825 */ private static void runSelectLoop(String abiList) throws MethodAndArgsCaller { // 안드로이드 애플리케이션 실행 요청 소켓을 저장할 리스트 ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>(); // 안드로이드 애플리케이션 실행 대기 리스트 ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>(); fds.add(sServerSocket.getFileDescriptor()); peers.add(null); while (true) { /* 중간 생략 */ for (int i = pollFds.length - 1; i >= 0; --i) { if ((pollFds[i].revents & POLLIN) == 0) { continue; } if (i == 0) { // 0번 index에서 이벤트 발생 -> 안드로이드 애플리케이션 실행 요청이 들어옴 ZygoteConnection newPeer = acceptCommandPeer(abiList); peers.add(newPeer); fds.add(newPeer.getFileDesciptor()); } else { // 안드로이드 애플리케이션 실행 boolean done = peers.get(i).runOnce(); if (done) { peers.remove(i); fds.remove(i); } } } } }
Call Trace[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
[3] core/java/com/android/internal/os/ZygoteConnection.java - runOnce()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/java/com/android/internal/os/ZygoteConnection.java#132 Line 132 */ boolean runOnce() throws ZygoteInit.MethodAndArgsCaller { /* 중간 생략 */ int pid = -1; FileDescriptor childPipeFd = null; FileDescriptor serverPipeFd = null; try { /* 중간 생략 */ // 애플리케이션 실행을 위해 fork() 및 권한 등 설정 pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo, parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet, parsedArgs.appDataDir); } catch (ErrnoException ex) { logAndPrintError(newStderr, "Exception creating pipe", ex); } catch (IllegalArgumentException ex) { logAndPrintError(newStderr, "Invalid zygote arguments", ex); } catch (ZygoteSecurityException ex) { logAndPrintError(newStderr, "Zygote security policy prevents request: ", ex); } /* 중간 생략 */ }
Call Trace[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
[3] core/java/com/android/internal/os/ZygoteConnection.java - runOnce()
[4] core/java/com/android/internal/os/Zygote.java - forkAndSpecialize()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/java/com/android/internal/os/Zygote.java#91 Line 91 */ // Forks a new VM instance. The current VM must have been started with the -Xzygote flag. public static int forkAndSpecialize(int uid, int gid, int[] gids, int debugFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, String instructionSet, String appDataDir) { VM_HOOKS.preFork(); /* C/C++ 코드로 작성된 네이티브 함수를 호출 함수 안에서 인자의 있는 uid와 gid로 프로세스의 uid와 gid가 변경 됨 */ int pid = nativeForkAndSpecialize( uid, gid, gids, debugFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, instructionSet, appDataDir); // Enable tracing as soon as possible for the child process. if (pid == 0) { Trace.setTracingEnabled(true); // Note that this event ends at the end of handleChildProc, Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork"); } VM_HOOKS.postForkCommon(); return pid; }
Call Trace[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
[3] core/java/com/android/internal/os/ZygoteConnection.java - runOnce()
[4] core/java/com/android/internal/os/Zygote.java - forkAndSpecialize()
[5] core/jni/com_android_internal_os_Zygote.cpp - nativeForkAndSpecialize()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/jni/com_android_internal_os_Zygote.cpp#652 Line 652 */ static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint debug_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring se_name, jintArray fdsToClose, jstring instructionSet, jstring appDataDir) { jlong capabilities = 0; /* 중간 생략 */ /* Utility routine to fork zygote and specialize the child process. Zygote가 fork()를 통해 애플리케이션 실행할 때 호출되는 네이티브 함수 */ return ForkAndSpecializeCommon(env, uid, gid, gids, debug_flags, rlimits, capabilities, capabilities, mount_external, se_info, se_name, false, fdsToClose, instructionSet, appDataDir); }
Call Trace[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
[3] core/java/com/android/internal/os/ZygoteConnection.java - runOnce()
[4] core/java/com/android/internal/os/Zygote.java - forkAndSpecialize()
[5] core/jni/com_android_internal_os_Zygote.cpp - nativeForkAndSpecialize()
[6] core/jni/com_android_internal_os_Zygote.cpp - ForkAndSpecializeCommon()
/* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/jni/com_android_internal_os_Zygote.cpp#444 Line 444 */ // Utility routine to fork zygote and specialize the child process. static pid_t ForkAndSpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray javaGids, jint debug_flags, jobjectArray javaRlimits, jlong permittedCapabilities, jlong effectiveCapabilities, jint mount_external, jstring java_se_info, jstring java_se_name, bool is_system_server, jintArray fdsToClose, jstring instructionSet, jstring dataDir) { /* 중간 생략 */ /* Zygote 프로세스 복제(fork) fork()로 생성된 프로세스가 안드로이드 애플리케이션으로 실행 됨 */ pid_t pid = fork(); if (pid == 0) { // 안드로이드 애플리케이션으로 실행되는 자식 프로세스 루틴 // The child process. /* 중간 생략 */ /* 후에 인자로 들어온 uid 값으로 UID를 변경하게 되는데 이때 UID를 변경해도 Root의 기능을 유지할 수 있도록 설정 이 기능은 함수를 호출한 쓰레드에서만 동작함 즉, UID 변경 후에도 현재 쓰레드는 Root 권한을 행사할 수 있지만 다른 쓰레드는 그러지 못함 */ // Keep capabilities across UID change, unless we're staying root. if (uid != 0) { EnableKeepCapabilities(env); } /* 1) 안드로이드 애플리케이션이 실행된 이후에 Capability를 얻지 못하게 하기 위해 Root 권한이 소유할 수 있는 모든 Capability BoundingSet을 Drop 함 BoundingSet : 프로세스가 실행 중인 동안 얻을 수 있는 Capability */ DropCapabilitiesBoundingSet(env); /* 중간 생략 */ /* 2) 프로세스의 Real, Effective, Saved GID를 설정 현재 함수의 인자에는 gid가 존재한다. 해당 인자에는 설정되어야 하는 안드로이드 애플리케이션의 gid가 넘어온다. 해당 gid로 프로세스의 egid, egid, sgid를 설정한다. */ int rc = setresgid(gid, gid, gid); if (rc == -1) { ALOGE("setresgid(%d) failed: %s", gid, strerror(errno)); RuntimeAbort(env, __LINE__, "setresgid failed"); } /* 3) 프로세스의 Real, Effective, Saved UID를 설정 현재 함수의 인자에는 uid가 존재한다. 해당 인자에는 설정되어야 하는 안드로이드 애플리케이션의 uid가 넘어온다. 해당 uid로 프로세스의 euid, euid, suid를 설정한다. */ rc = setresuid(uid, uid, uid); if (rc == -1) { ALOGE("setresuid(%d) failed: %s", uid, strerror(errno)); RuntimeAbort(env, __LINE__, "setresuid failed"); } /* 중간 생략 */ /* 4) 함수의 인자에는 permittedCapabilities와 effectiveCapabilities가 존재한다. 이는 실행하려는 안드로이드 애플리케이션이 특정 루트 권한의 기능이 필요한 경우 Capability를 이용해 해당 기능을 사용할 수 있도록 설정 하는 것이다. 거의 대부분의 일반적인 안드로이드 애플리케이션은 아무런 권한도 설정되지 않는다. */ SetCapabilities(env, permittedCapabilities, effectiveCapabilities); /* 중간 생략 */ /* 안드로이드 애플리케이션의 실행 준비가 완료되었다면 다음 함수를 통해 안드로이드 애플리케이션을 호출(실행)한다. */ env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, debug_flags, is_system_server, instructionSet); if (env->ExceptionCheck()) { RuntimeAbort(env, __LINE__, "Error calling post fork hooks."); } } else if (pid > 0) { // the parent process /* 중간 생략 */ } return pid; }
안드로이드 애플리케이션의 실행 과정을 추측한 후에 실제로 소스코드를 분석했을 때 거의 100% 추측 결과와 일치했다.
안드로이드 애플리케이션을 호출하기 전에 소스코드에 주석으로 표시한 4개의 함수를 통해 프로세스 스스로의 권한을 낮춘 후 CallStaticVoidMethod()
함수를 통해 안드로이드 애플리케이션 함수를 호출한다.
Call Trace
[1] core/java/com/android/internal/os/ZygoteInit.java - main()
[2] core/java/com/android/internal/os/ZygoteInit.java - runSelectLoop()
[3] core/java/com/android/internal/os/ZygoteConnection.java - runOnce()
[4] core/java/com/android/internal/os/Zygote.java - forkAndSpecialize()
[5] core/jni/com_android_internal_os_Zygote.cpp - nativeForkAndSpecialize()
[6] core/jni/com_android_internal_os_Zygote.cpp - ForkAndSpecializeCommon()
└ static void DropCapabilitiesBoundingSet(JNIEnv* env);
└ int setresgid(gid_t rgid, gid_t egid, gid_t sgid); // Kernel Function
└ int setresuid(uid_t ruid, uid_t euid, uid_t suid); // Kernel Function
└ static void SetCapabilities(JNIEnv* env, int64_t permitted, int64_t effective);
setresgid()
와 setresuid()
함수는 커널 함수로 프로세스의 gid와 uid를 변경하는 함수이다. 다음 링크에 더 자세한 설명이 작성되어 있다.
/*
https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/jni/com_android_internal_os_Zygote.cpp#227
Line 227
*/
static void DropCapabilitiesBoundingSet(JNIEnv* env) {
/*
i의 값에 해당되는 Capability를 가져옴 Capability가 존재한다면
0 이상을 반환, 존재하지 않는다면 음수를 반환
*/
for (int i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0; i++) {
/*
현재 프로세스에서 i 값의 Capability를 BoundingSet에서 제거
Capability가 바운딩(Bounding)되어 있지 않다면 허가(Permiited)되더라도
실제(Effective)로 사용할 수 없다. 이는 Root 권한이라고 할지라도 바운딩과
허가되어 있지 않다며 Root 권한으로써 기능을 수행할 수 없다
*/
int rc = prctl(PR_CAPBSET_DROP, i, 0, 0, 0);
if (rc == -1) {
if (errno == EINVAL) {
ALOGE("prctl(PR_CAPBSET_DROP) failed with EINVAL. Please verify "
"your kernel is compiled with file capabilities support");
} else {
RuntimeAbort(env, __LINE__, "prctl(PR_CAPBSET_DROP) failed");
}
}
}
}
/*
https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-7.1.2_r39/core/jni/com_android_internal_os_Zygote.cpp#241
Line 241
*/
static void SetCapabilities(JNIEnv* env, int64_t permitted, int64_t effective) {
__user_cap_header_struct capheader;
memset(&capheader, 0, sizeof(capheader));
capheader.version = _LINUX_CAPABILITY_VERSION_3;
capheader.pid = 0;
__user_cap_data_struct capdata[2];
memset(&capdata, 0, sizeof(capdata));
capdata[0].effective = effective;
capdata[1].effective = effective >> 32;
capdata[0].permitted = permitted;
capdata[1].permitted = permitted >> 32;
/*
capset 함수를 통해 effective와 permitted capability를 설정한다.
일반적인 안드로이드 애플리케이션의 경우에는 아무런 값도 설정되지 않는다.
이는 실제(Effective)로 사용 가능한 기능과 사용에 허가(Permitted) 받은
Capability를 모두 지우겠다는 말이 된다.
*/
if (capset(&capheader, &capdata[0]) == -1) {
ALOGE("capset(%" PRId64 ", %" PRId64 ") failed", permitted, effective);
RuntimeAbort(env, __LINE__, "capset failed");
}
}
최종적으로 ForkAndSpecializeCommon()
함수의 시작부터 안드로이드 애플리케이션 시작까지 소스 코드의 워크 플로우는 다음과 같다.
How to run application as root for Android
Zygote가 프로세스를 실행하기 전 권한들을 모두 Drop하고 uid와 gid를 변경한다. 이때 특정 uid와 gid를 가지고 있는 안드로이드 애플리케이션의 경우에는 Capabilities를 Drop하지 않고 uid와 gid를 변경하지 않도록 함수를 조작할 수 있다면 특정 안드로이드 애플리케이션을 Root 권한으로 실행시키는 것이 가능해진다.
안드로이드 애플리케이션을 Root 권한으로 실행하기 위해 모니터링하며 조작이 필요한 함수는 다음과 같다.
int setresuid(uid_t ruid, uid_t euid, uid_t suid);
int setresgid(gid_t rgid, gid_t egid, gid_t sgid);
int prctl(int option, ...);
int capset(cap_user_header_t hdrp, const cap_user_data_t datap);
Zygote 프로세스의 특정 함수의 동작을 조작하기 위해서 LD_PRELOAD
를 사용했다.
LD_PRELOAD로 로드되는 후킹 라이브러리 코드는 다음과 같다.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/capability.h>
#include <linux/seccomp.h>
pid_t pid = -1;
int (*origin_setresuid)(uid_t ruid, uid_t euid, uid_t suid);
int (*origin_setresgid)(gid_t rgid, gid_t egid, gid_t sgid);
int (*origin_prctl)(int option, ...);
int (*origin_capset)(cap_user_header_t __hdr_ptr, const cap_user_data_t __data_ptr);
void dropCapabilitiesBoundingSet()
{
for (int i = 0; origin_prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0; i++)
{
if (origin_prctl(PR_CAPBSET_DROP, i, 0, 0, 0) == -1)
{
printf("Failed %d\n", i);
}
}
}
int isRootApplication(pid_t uid)
{
char line[512];
FILE *package_list = fopen("/data/system/packages.list", "r");
FILE *root_app_list = fopen("/data/local/tmp/root_app.list", "r");
if (package_list == NULL || root_app_list == NULL)
goto exit;
while (fgets(line, sizeof(line), package_list) != NULL)
{
char root_package_name[64];
char package_name[128];
pid_t package_uid = -1;
sscanf(line, "%s %d", package_name, &package_uid);
if (package_uid == uid)
{
while (fgets(root_package_name, sizeof(root_package_name), root_app_list) != NULL)
{
if (strncmp(package_name, root_package_name, strlen(package_name)) == 0)
{
fclose(package_list);
fclose(root_app_list);
return 1;
}
}
}
}
exit:
fclose(package_list);
fclose(root_app_list);
return 0;
}
int capset(cap_user_header_t __hdr_ptr, const cap_user_data_t __data_ptr)
{
if (pid == getpid())
return 0;
if (origin_capset == NULL)
origin_capset = dlsym(RTLD_NEXT, "capset");
return origin_capset(__hdr_ptr, __data_ptr);
}
int prctl(int option, ...)
{
va_list args;
va_start(args, option);
long arg2 = va_arg(args, long);
long arg3 = va_arg(args, long);
long arg4 = va_arg(args, long);
long arg5 = va_arg(args, long);
va_end(args);
if (option == PR_CAPBSET_DROP)
return 0;
if (origin_prctl == NULL)
origin_prctl = dlsym(RTLD_NEXT, "prctl");
return origin_prctl(option, arg2, arg3, arg4, arg5);
}
int setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
if (isRootApplication(ruid))
{
pid = getpid();
dropCapabilitiesBoundingSet();
return 0;
}
if (origin_setresuid == NULL)
origin_setresuid = dlsym(RTLD_NEXT, "setresuid");
return origin_setresuid(ruid, euid, suid);
}
int setresgid(gid_t rgid, gid_t egid, gid_t sgid)
{
if (isRootApplication(rgid))
return 0;
if (origin_setresgid == NULL)
origin_setresgid = dlsym(RTLD_NEXT, "setresgid");
return origin_setresgid(rgid, egid, sgid);
}
How does Zygote run?
LD_PRELOAD를 사용하기 위해서는 Zygote의 환경 변수를 조작해야하기 때문에 Zygote가 어떠한 방식으로 init 프로세스의 의해 실행되는지 알아야할 필요가 있다.
Zygote는 init 프로세스가 자기 자신을 fork()
함수를 통해 복제한 뒤 execve()
함수를 통해 app_process를 실행하는 방식으로 실행된다.
execve() 함수의 원형을 보면 envp 인자가 존재하는데 해당 인자가 실행되는 프로그램의 환경 변수를 설정하게 된다.
int execve(const char *pathname, char *const argv[], char *const envp[]);
Zygote를 실행하기 위해 execve() 함수를 호출할 때 envp 인자에 LD_PRELOAD를 삽입하는 방식으로 조작할 수 있다.
How to set the LD_PRELOAD environment variable on Zygote
Zygote의 함수를 조작하기 위해 Zygote 프로세스의 환경 변수에 LD_PRELOAD
를 추가해 해당 함수들을 조작해 안드로이드 애플리케이션이 Root 권한으로 실행될 수 있도록 해야한다.
init 프로세스를 이용해 Zygote 프로세스에 LD_PRELOAD 환경 변수를 삽입하는 순서는 다음과 같다.
※ init 프로세스는 Zygote 프로세스가 종료되었을 때 Zygote 프로세스를 재시작한다. ※
[1] init 프로세스를 ptrace로 감시한다.
[2] 기존 Zygote 프로세스를 KILL 한다.
[3] init 프로세스는 Zygote의 SIGCHLD 시그널을 받고 Zygote 프로세스를 재시작한다.
[4] init 프로세스는 Zygote의 재시작을 위해 fork() 함수를 호출한다. 이때 해당 함수를 통해 생성된 프로세스로 ptrace를 옮긴다.
[5] 새로 생성된 프로세스(New Zygote)에서 execve() 함수로 app_process를 실행한다. 이때 envp 인자에 LD_PRELOAD를 삽입한다.
[6] LD_PRELOAD로 인해 위에서 언급한 4개의 함수가 후킹된 상태로 Zygote 프로세스 실행 성공
PoC Video
PoC 코드는 다음 Github에서 확인할 수 있다.