안드로이드 디버깅 탐지란?
안드로이드 모바일 앱에서 LLDB, GDB, IDA 같은 동적 디버깅 도구를 붙였을 때 디버깅이 가능하다면 이는 취약점이 될 수 있다.
앱이 동작할 때 동적 디버깅 도구를 활용한 디버깅 가능 여부에 따라 취약한지 결정된다.
LLDB, GDB, IDA와 같은 동적 디버깅 도구로 디버깅이 가능하다면 공격자는 이를 통해 코드 흐름 파악, 메모리 상태 분석, 실행 흐름을 조작 할 수 있다.
따라서 디버깅 탐지 기능을 통해 동적 도구를 활용한 디버깅을 방지 할 수 있다.
디버깅 탐지 기능 설정
디버깅 탐지 기능은 아래의 방법으로 존재한다.
- 디버깅 시 사용되는 ptrace 시스템 호출 차단 및 선점
- ppid를 확인하여 앱을 실행 시킨 프로세스가 디버깅툴인지 확인
디버깅에 사용되는 ptrace 탐지
안드로이드 앱에서 디버깅 탐지를 구현하기 위해 ptrace 시스템 호출을 감지하고 이를 차단하는 방법을 이용할 수 있다.
ptrace란?
ptrace는 리눅스 커널에서 제공하는 시스템 호출 중 하나로, 프로세스 디버깅 및 추적이 가능하다.
ptrace를 사용하면 디버거는 타깃 프로세스를 attach하여 실행을 중단하고, 메모리 내용, 레지스터 상태, 시스템 호출 등의 정보에 접근할 수 있고, 이를 통해 프로세스 상태를 관찰하고 수정할 수 있다.
따라서 이를 역 이용해 해당 함수로 안티 디버깅 기능을 구현할 수 있다.
ptrace를 활용한 디버깅 탐지 코드
가장 메인이 되는 디버깅 탐지 코드는 다음과 같다.
#include <jni.h> // JIN 헤더파일 포함
#include <sys/ptrace.h> // ptrace 시스템 호출 사용위한 헤더파일
#include <android/log.h> // 로깅 태그 정의
#define LOG_TAG "AntiDebug"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
jboolean detectDebugger() { // 디버거 감지 함수 정의
// PTRACE_TRACEME 요청을 통해 현재 프로세스가 추적되고 있는지 확인
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
// 만약에 false 라면 디버깅이 감지되었음을 의미
LOGE("Debugger detected!");
return JNI_TRUE;
}
// 디버거가 감지되지 않았으면 ptrace 호출을 통해 프로세스를 추적하지 않도록 설정
ptrace(PTRACE_DETACH, 0, 0, 0);
// 디버거가 감지되지 않았음을 반환
LOGI("No debugger detected.");
return JNI_FALSE;
}
// JNI 함수 정의, Java에서 호출될 수 있도록 JNIEXPORT 및 JNICALL 키워드 사용
JNIEXPORT jboolean JNICALL Java_com_example_code_1proguard_1example_AntiDebug_detectDebugger(JNIEnv *env, jobject instance) {
// detectDebugger 함수를 호출하여 결과 반환
return detectDebugger();
}
위 코드에서는 ptrace 시스템 호출을 사용하여 디버거가 붙어 있는지 감지한다. ptrace(PTRACE_TRACEME, 0, 0, 0) 호출이 실패하면 디버거가 붙어 있다고 간주하고, 이를 로그에 띄우도록 하였다.
ptrace(PTRACE_TRACEME, 0, 0, 0) 함수
이는 ptrace 시스템 호출의 한 형태로 프로세스가 디버거에 의해 추적되고 있는지 확인하기 위해 사용된다. 해당 함수에서 사용되는 각 인수는 다음과 같다.
- PTRACE_TRACEME : ptrace 시스템 호출에 사용되는 여러 명령 중 하나로, 현재 프로세스가 디버거에 의해 추적되도록 설정한다. 이 명령을 호출하면 프로세스 자신을 디버거로 설정하여 부모 프로세스가 디버거가 될 수 있도록 한다.
- 따라서 두 번째 인자인 PID도 자기 자신을 가리키는 0을 전달하는 것이다.
해당 함수를 안티 디버깅 기능 구현을 위해 활용한 방법은 아래와 같다.
함수 활용 내용
- 디버거 설정: ptrace(PTRACE_TRACEME, 0, 0, 0) 호출은 현재 프로세스가 디버거에 의해 추적되도록 설정한다. 이 호출을 통해 프로세스는 부모 프로세스가 자신의 디버거가 되도록 요청한다. 일반적으로 디버거는 ptrace를 사용하여 다른 프로세스를 제어하고 디버깅할 수 있다.
- 디버거 감지: 하나의 process에 attach 할 수 있는 다른 프로세스는 오직 1개 이하이다. 만약 현재 프로세스가 이미 디버거에 의해 추적되고 있다면, PTRACE_TRACEME 호출은 실패하고 1을 반환합니다. 이 실패는 디버거가 붙어 있다는 것을 나타내며, 이를 통해 애플리케이션은 디버거가 감지되었음을 알 수 있다.
ptrace 활용 요약 → 프로세스 자신을 디버그로 설정하기 위해 ptrace를 사용할 때, 성공 값 0이 아닌 값이 나온다면 다른 디버거가 감지 되었다라고 생각할 수 있는 것이다.
디버깅 탐지 코드 설정 방법
- Tools → SDK Manager → SDK Tools 에서 NDK, CMake 설치한다.
- C:\Users\User\AndroidStudioProjects\’${자신의 프로젝트}‘\app\src\main 경로에 cpp 폴더 생성
- Android.mk 또는 CMakeLists.txt 파일 설정위에 생성한 cpp 폴더에 CMakeLists.txt 파일을 작성한다.
- JNI C 코드를 컴파일할 수 있도록 Android NDK 빌드 시스템을 설정한다.
cmake_minimum_required(VERSION 3.4.1)
add_library( antidebug SHARED antidebug.c )
find_library( log-lib log )
target_link_libraries( antidebug ${log-lib} )
- 프로젝트 내에 생성한 cpp 파일에 antidebug.c 파일을 작성한다.
- Java 인터페이스 작성
- 위의 C 코드를 Java에서 사용할 수 있도록 JNI 인터페이스를 정의한다.
package com.example.code_proguard_example;
public class AntiDebug {
static {
System.loadLibrary("antidebug");
}
public native boolean detectDebugger();
}
- Java 코드에서 디버거 감지 호출
- 이제 Java 코드에서 디버거 감지를 호출할 수 있습니다.
package com.example.antidebug;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkAntiDebug();
}
private void checkAntiDebug(){
AntiDebug antiDebug = new AntiDebug();
boolean isDebuggerDetected = antiDebug.detectDebugger();
if (isDebuggerDetected) {
Toast.makeText(this, "Debugger 감지됨.",Toast.LENGTH_SHORT).show();
}
else{
Toast.makeText(this, "Debugger 감지됨.",Toast.LENGTH_SHORT).show();
}
}
}
이 코드들을 프로젝트에 통합하면 애플리케이션이 실행될 때 디버거가 붙어 있는지 확인할 수 있다.
TracerPid 활용 디버깅 탐지
TracePid란
앱의 "/proc/self/status" 파일 내용에 있는 값으로 /proc/self/status 파일은 리눅스 기반 운영 체제에서 실행 중인 프로세스에 대한 정보를 제공하는 가상 파일이다.
안드로이드 앱의 경우, 안드로이드 운영 체제는 리눅스 커널 위에서 실행되므로, 이 파일을 통해 현재 실행 중인 앱 프로세스에 대한 정보를 얻을 수 있다.
따라서 만약 디버깅툴이 앱을 실행시킨 경우 TracerPid가 0보다 큰 값을 가지므로 이를 활용하여 디버깅을 탐지한다.
antidebug .c
jboolean tracerPidDetectDebugger(){
int TPid;
char buf[512];
const char *str = "TracerPid:";
size_t strSize = strlen(str);
jstring strDebugging = "NONE";
FILE* file = fopen("/proc/self/status", "r");
while (fgets(buf, 512, file)) {
if (!strncmp(buf, str, strSize)) {
sscanf(buf, "TracerPid: %d", &TPid);
if (TPid != 0) {
strDebugging = buf;
fclose(file);
return JNI_TRUE;
}
}
}
fclose(file);
return JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_example_code_1proguard_1example_AntiDebug_tracerPidDetectDebugger(JNIEnv *env, jobject instance) {
return tracerPidDetectDebugger();
}
MainActivity .java
private void checkTracerPid() {
AntiDebug antiDebug = new AntiDebug();
boolean isDebuggerDetected = antiDebug.tracerPidDetectDebugger();
if (isDebuggerDetected) {
Toast.makeText(this, "tracer pid != 0 Debugger 감지됨.", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "tracer pid == 0 Debugger 감지안됨.", Toast.LENGTH_SHORT).show();
}
}
TracerPid 탐지 실행결과
USB 탐지 기능
USB 탐지 메인 코드
if (intent != null && intent.getAction() != null &&
intent.getAction().equals("android.hardware.usb.action.USB_STATE")) {
boolean connected = intent.getBooleanExtra("connected", false);
Log.d(TAG, "USB connected: " + connected);
}
intent의 action을 활용해 usb 탐지를 확인하는 코드는 다음과 같다.
위 코드는 해당 함수 호출 시에만 확인할 수 있으므로 필요한 경우 리시버를 추가해서 실시간 연결 감지가 가능하다.
Receiver활용 USB 탐지 전체 코드
UsbConnectionReceiver.java
public class UsbConnectionReceiver extends BroadcastReceiver {
private static final String TAG = "UsbConnectionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null &&
intent.getAction().equals("android.hardware.usb.action.USB_STATE")) {
// intent의 action이 android usb action과 동일하다면
// intent의 connected 여부를 가져온다.
boolean connected = intent.getBooleanExtra("connected", false);
Log.d(TAG, "USB connected: " + connected);
}
}
// 리시버 등록
public static void registerReceiver(Context context, UsbConnectionReceiver receiver) {
IntentFilter filter = new IntentFilter();
filter.addAction("android.hardware.usb.action.USB_STATE");
context.registerReceiver(receiver, filter);
}
// 리시버 해제
public static void unregisterReceiver(Context context, UsbConnectionReceiver receiver) {
context.unregisterReceiver(receiver);
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private UsbConnectionReceiver usbReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
usbReceiver = new UsbConnectionReceiver();
UsbConnectionReceiver.registerReceiver(this, usbReceiver);
// 메인 Activity에 동적 리시버 적용
}
@Override
protected void onDestroy() {
super.onDestroy();
UsbConnectionReceiver.unregisterReceiver(this, usbReceiver);
// Activity Context가 유효할 때만 작동하도록 unregister 함.
}
}
메인 Activity에 USB 연결을 탐지하는 동적 리시버를 적용하여
메인 Activity Context가 유효한 동안 실시간 감지가 가능하도록 설정하였다.
USB 탐지 실행결과
+) 안드로이드 4대 컴포넌트 중 하나인 receiver 복습을 위한 정리
안드로이드 컴포넌트 Receiver
Receiver는 안드로이드 4대 컴포넌트 중 하나인 Broadcast Receiver를 의미한다.
Broadcast Receiver란?
안드로이드에서 Broadcast Receiver는 앱 구성 요소 중 하나로, 다른 앱이나 시스템에서 발생한 브로드캐스트 메시지를 수신하는 역할을 한다.
브로드캐스트 메시지는 시스템에서 발생하는 다양한 이벤트를 의미하며, 예를 들어 배터리 부족, 네트워크 상태 변경, 앱 설치/제거 등의 이벤트가 포함된다.
이러한 이벤트는 시스템이나 다른 앱에서 발생할 수 있으며, Broadcast Receiver는 이러한 이벤트를 수신하여 적절한 처리를 할 수 있다.
Broadcast Receiver는 안드로이드의 시스템 브로드캐스트를 수신하는 것 외에도, 개발자가 정의한 앱에서 발생시키는 브로드캐스트 메시지도 수신할 수 있다.
이를 통해 앱 내에서 이벤트를 처리하거나, 다른 앱에 메시지를 전달하는 등의 다양한 작업을 수행할 수 있다.
Broadcast Receiver
- 시스템이나 다른 앱에서 보내는 브로드케스트 메시지를 받아서 처리한다
- 디자인 패턴 중 publish-subscribe 형태 처럼 한쪽에서는 이벤트를 제공하기만 하고 한쪽에서는 이벤트를 받기만 한다.
- 앱들끼리도 미리 사전에 정의한 Action을 주고 받을 수 있다.
등록 방식에 따른 종류
정적 리시버
- AndroidManifest.xml 파일에 등록되며 라이프 사이클과 무관하게 동작한다.
- 앱이 설치되면 즉시 사용 가능하며 등록과 해지가 자유롭지 못하다.
동적 리시버
- Activity와 같은 컴포넌트에서 프로그래밍적으로 등록하며 라이프사이클 내에서 등록 및 삭제 처리가 필요하다.
- 해당 코드가 실행될 때 사용 가능하며 코드 내에서 필요에 따라 등록 및 삭제가 가능하다.
- USB 탐지 기능에서 동적 리시버를 사용하였다.
- Android 4대 컴포넌트 중 Broadcast Recevier만 동적으로 등록할 수 있다.
'Android > 공부' 카테고리의 다른 글
[Android] XML 과 Compose 렌더링 방식의 차이 (0) | 2024.09.23 |
---|---|
[Android/공부] 안드로이드 라이브러리 / AAR 만들기 (0) | 2024.07.17 |
[Android/공부] 안드로이드 소스 코드 난독화 R8 / Proguard (1) | 2024.07.10 |
[Android] Flow란? LiveData와 Flow 비교 (0) | 2023.11.13 |
[Android/코루틴] 2.코루틴의 기본요소들을 알아보자 (0) | 2023.09.21 |