봄날은 갔다. 이제 그 정신으로 공부하자

내가 만든 앱은 내가 지키자! (루팅감지, 디버깅감지) 2/3 본문

보안

내가 만든 앱은 내가 지키자! (루팅감지, 디버깅감지) 2/3

길재의 그 정신으로 공부하자 2021. 11. 15. 11:11

개요

지난번 글에 이어 이번에는 앱이 배포된 뒤 크래커에 의해 디버깅 되는 것을 막는 방법에 대해 설명하도록 하겠습니다.

개발자가 엄청난 노력을 기울여 시큐어 코딩을 하고 코드를 난독화해도 디버깅 툴 연결을 허용한다면 크래커는 적은 노력으로 많은 정보를 획득할 수 있으므로 가능한 디버깅을 막아야 합니다.

 

크래커가 앱을 크랙킹하기 위해 어떤 것들을 할까요?

앱을 다운로드 받아 디컴파일하고 디버깅툴에 연결해 실행하거나 실행된 앱이 생성한 파일을 추출해 분석할 것 입니다.

그러므로 우리는 이러한 것들을 막을 필요가 있고 막아야 하는 것은 아래 4가지 입니다.

처음 두개는 반드시 막아야하고 뒤에 두가지는 앱에 따라 막는 것을 선택하면 됩니다.

   - 루팅된 OS 감지

   - 디버깅툴 연결 감지

   - 개발자 모드 감지 (or USB 디버깅 연결 ON)

   - USB Device 연결 감지

 

위 4가지 감지 기능은 앱이 실행이 될 때 뿐만 아니라 수시(앱 활성화 시, 화면 전환 시, 네트워크 통신 시, …)로 감지하는 것이 좋으며,

감지 시 모든 동작을 중지하고 사용자에게 알려주고 앱을 종료해야 합니다.

간혹 저렇게 많은 감지를 수시로 한다면 앱의 성능이 저하되는 것을 걱정하는 경우가 많은데,  4가지를 모두 체크하는 경우, 일반적인 핸드폰에서 2 ~ 3ms 정도의 시간 시간정도 소요 될 정도로 미미한 시간만 소요됩니다.

하지만 세상에는 워낙 다양한 종류의 android 기기가 있고 또 사용자마다 상황이 다르므로 개발자가 상황에 맞게 적절하게 감지 기능을 추가해주시면 됩니다.

 

루팅된 OS 감지

루팅된 OS에서 앱이 실행된다면 앱이 외부에 공유하지 않은 파일이 노출될 수 있으므로 루팅된 OS를 감지하는 기능이 필요합니다.

 

루팅된 OS 감지는 간단합니다.

루팅된 OS는 앱에서 루트 권한 접근이 가능하므로 아래와 같이 루트권한 접근이 가능한지 이를 확인하는 코드를 추가합니다.

private fun isRooting(): Boolean {
    var flag = false
    try {
        Runtime.getRuntime().exec("su")
        flag = true
    } catch (ex: Exception) {
    }
    return flag
}

 

해당 함수에서 true가 리턴된다면 앱이 루팅된 OS에서 실행되는 경우이므로 사용자에게 Noti하고 앱을 종료합니다.

 

디버깅툴 연결 감지

디버깅툴 연결 감지는 앱 실행시에만 체크해주면 됩니다.

디버깅툴 연결 감지는 약간 복잡합니다.

앱에 GDB or LLDB와 같은 디버깅툴이 연결 되어 있는지 확인하는 방법은 두가지 입니다.

 

첫번째는 

앱의 ppid를 확인하여 앱을 실행 시킨 프로세스가 디버깅툴(GDB or LLDB)인지 확인하는 아래와 같은 방법이고

bool checkDebugTool() {
   char filePath[32], fileRead[128];
   FILE* file;

   snprintf(filePath, 24, "/proc/%d/cmdline", getppid());
   file = fopen(filePath, "r");

   fgets(fileRead, 128, file);
   fclose(file);

   if(!strcmp(fileRead, "gdb")) {
       return true;
   }

   if(!strcmp(fileRead, “lldb”)) {
       return true;
   }

   return false;
}

 

두번째는 

앱의 "/proc/self/status" 파일 내용 중 TracerPid 값이 있는지 확인하는 방법인데 디버깅툴이 앱을 실행시킨 경우 TracerPid가 0보다 큰 값을 가집니다.

bool checkDebugTool() {
  int TPid;
  char buf[512];
  const char *str = "TracerPid:";
  size_t strSize = strlen(str);
  std::string 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 true
          }
      }
  }

  fclose(file);
  return false;
}

 

코드를 자세히 보면 위 두 개의 코드 모두 C/C++ 코드인 것을 확인할 수 있습니다.

그러므로 앱에서 이 코드를 사용하기 위해서는 어쩔 수 없이 JNI를 사용해야 합니다.

 

JNI 코드 추가하기

C 코드를 추가하기 위해 JNI를 사용하는 것을 어려워하는 개발자가 많은데 크게 어려울 것 없습니다.

구글링해보면 많은 JNI 예제를 찾을 수 있는데 실제 이걸 따라해보면 빌드가 되지 않는 경우가 많아서 그럴 것 입니다.

어렵게 생각하지 말고 쉽게 생각하면 됩니다.

 

1. 우선 SDK Tools에서 NDK를 다운로드 받습니다.

다운로드 받는 경로 및 방법은 아래와 같습니다.

   - Tools

   - SDK Manager 

   - Android SDK 

   - SDK Tools(Tab) 

   - NDK(Side by side) 체크해서 설치

 

2. 그다음에 app의 build.gradle 파일에 필요한 아래 코드를 추가합니다.

defaultConfig {
    …
    externalNativeBuild {
        cmake {
            cppFlags "-std=c++17"
        }
    }
}

…
externalNativeBuild {
    cmake {
        path "src/main/cpp/CMakeLists.txt"
        version "3.10.2"
    }
}
…

 

3. 그리고 메인 폴더의 아래에 cpp 폴더를 만들고 CMakeLists.txt를 생성합니다.

CMakeLists.txt파일에 아래 내용을 추가한 후 

cmake_minimum_required(VERSION 3.10.2)

project("myapplication")

add_library( # Sets the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             native-lib.cpp )

find_library( # Sets the name of the path variable.
              log-lib
              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

target_link_libraries( # Specifies the target library.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

 

4. cpp 폴더에 위의 디버깅툴을 체크하는 코드를 아래와 같이 추가합니다.

extern "C" JNIEXPORT jstring JNICALL
Java_com_studiolkj_myapplication_MainActivity_checkDebugToolJNI(
        JNIEnv* env,
        jobject /* this */) {
    int TPid;
    char buf[512];
    const char *str = "TracerPid:";
    size_t strSize = strlen(str);
    std::string 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 env->NewStringUTF(strDebugging.c_str());
            }
        }
    }

    fclose(file);
    return env->NewStringUTF(strDebugging.c_str());
}

 

5. 마지막으로 프로젝트의 MainActivity.kt 파일에 아래와 같이 선언해줍니다.

external fun checkDebugToolJNI(): String
...

companion object {
    // Used to load the 'native-lib' library on application startup.
    init {
        System.loadLibrary("native-lib")
    }
}

 

사용은 checkDebugToolJNI()함수를 호출해주면 됩니다.

 

뭔가 갑자기 급발진 한 것 같은데… 이 글을보고 위의 내용 오타 하나 없이 그대로 따라해도 아마 빌드가 안될 겁니다.

 

여러가지 이유가 있는데, 상황에 따라 다르므로 여러분들은 위 내용을 보고 아! 저기저기만 추가 수정해주면 되는구나?라고 이해한 후 아래와 같이 해주시면 됩니다.

 

1. Android Studio에서 New 프로젝트를 선택하면 맨마지막에 아래와 같이 Native C++ 보입니다.

2. Native C++을 선택하고 Next

3. Language Kotlin으로 선택하고 Finish

딱, 여기까지 하면 깔끔하게 내 환경에 최적화된 JNI 샘플 코드를 얻을 수 있습니다.

 

이제 이렇게 만들어진 샘플소스를 참고해서

위에 언급한대로 build.gradle 파일에 관련 내용을 추가해준 후

Main 폴더에 cpp 폴더를 추가하고 CMakeLists.txt 파일을 추가한 후 해당 내용을 그대로 복사한 후 project만 내 앱에 맞게 수정합니다. Native-lib.cpp 파일도 동일하게 해주면 됩니다.

 

단, 여기서 조심해야 하는 부분은 Native 함수 이름인데 이부분을 나누어 설명하면 다음과 같습니다.

Java_com_studiolkj_myapplication_MainActivity_stringFromMyJNI(…)

Java_ 				// 이거 건들지 말고 그대로 유지
com_studiolkj_myapplication_ 	// 프로젝트 패키지명
MainActivity_ 			// 해당 Native 함수가 external로 선언될 클래스의 위치
stringFromMyJNI(…) 		// 함수의 이름

 

이정도만 알면 이제 Native 함수를 추가하는 것은 끝입니다.

개발 중 필요에 의해 cpp 파일 추가가 필요하면 파일을 추가하고 CMakeLists.txt에 해당 내용을 추가해주면 됩니다.

 

개발자 모드 감지

개발자 모드 ON으로 되어 있는 경우는 해당 단말기가 일반적인 핸드폰이 아닌 경우가 높으므로 개발자 모드가 켜져 있으면 앱을 종료해주는 것이 좋습니다. 다만 저 같은 개발자는 개인 단말기도 개발자모드를 켜놓기 때문에 간혹 억울한 피해자도 생깁니다. ㅠ_ㅠ

 

개발자 모드 감지는 Build.VERSION_CODES.JELLY_BEAN_MR1 보다 크거나 같은 경우와 아닌 경우에 따라 다르게 확인해야 합니다.


    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        return Settings.Global.getInt(
            context.contentResolver,
            Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
            0
        ) != 0
    }else{
	return Settings.Secure.getInt(
            context.contentResolver,
            Settings.Secure.DEVELOPMENT_SETTINGS_ENABLED,
            0
        ) != 0
     }

개발자 모드 확인이 아니라 좀 더 세밀하게 ADB 연결이 ON 된 경우만 체크할 경우라고 하면 아래와같이 사용해주면 됩니다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    return Settings.Global.getInt(context.getContentResolver(), Settings.Global.ADB_ENABLED, 0) != 0;
}else{
    return android.provider.Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.ADB_ENABLED, 0) != 0;
}

   

USB Device 연결 감지

개발자 모드와는 다르게 USB Device 연결 감지는 디버깅을 위한 연결만이 아니라 충전기를 연결한 경우도 감지되고 이를 구분할 방법이 없기 때문에 반드시 필요한 경우가 아니라면 이 부분은 무시하셔도 됩니다.

USB Device 연결 감지는 아래와 같습니다.

private fun isUsbConnected(context: Context): Boolean {
    val intent = context.registerReceiver(null, IntentFilter("android.hardware.usb.action.USB_STATE"))
    return intent != null && intent.extras != null && intent.getBooleanExtra(
        "connected",
        false
    ) == true
}

위 코드는 해당 함수 호출 시에만 확인할 수 있으므로 필요한 경우 리시버를 추가해서 실시간 연결 감지가 가능합니다.

 

* "ADB 연결이 ON"과  "USB 연결이 감지"된 경우만 디버깅 중이라고 판단한다면 선의의 피해자를 조금 더 줄일 수 있습니다.

 

정리

이번 글에서는 앱이 루팅된 OS에서 실행되는 것을 방지하고 디버깅툴이 연결된 것을 감지하는 방법에 대해 설명하였습니다.

특히 디버깅툴 연결을 감지하는 것은 JNI 코딩이 필요하며 이에 따라 내 개발 환경에 맞게 간단하게 JNI 코딩을 할 수 있는 방법에 대해서도 설명하였습니다.

 

시큐어 코딩을 통해 크래커가 앱을 분석하는 것을 방지하고 루팅 디버깅툴 연결 확인을 통해 분석을 다시 한번 막는다면 크래커커로부터 안전한 앱을 만들 있습니다.

 

Comments