반응형

Magisk v27.0 이후로 많은 부분이 업데이트 되었지만 주목할 만한 것은, Zygisk의 Zygote Injection 방식이 달라진 것입니다.

이전 버전에서는 LD_PRELOAD 방식을 이용하여 Injection 하였습니다. 이 방식은 linker 에 침투 흔적을 남겨서 zygisk 활성화 여부를 탐지 할 수 있었습니다.

 

v27.0 에서는 Zygote Injection 으로 native bridge 방식이 도입되었습니다.

Zygisk 가 없던 시절 Riru (https://github.com/RikkaApps/Riru?tab=readme-ov-file) 가 해당 방식으로 Zygote Injection 을 구현하였는데, Zygisk 도 해당 방식을 차용한 것으로 보입니다.

 

Native bridge 방식이 어떤 흔적을 남기는지 간략히 확인해보겠습니다.

Magisk 에서 Zygisk 기능을 활성화하고 재부팅을 해보면 "Unsupported native bridge API..." 로그를 볼 수 있습니다.

 

 

해당 로그는 안드로이드 소스코드의 native_bridge.cc 에서 찾을 수 있습니다.

출처: https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:art/libnativebridge/native_bridge.cc;drc=1aa5d121d7eb21a96f42cfd396e56bf2acdca162;l=231

소스 코드를 보면 해당 로그를 찍고, callbacks = nullptr; 로 설정합니다.

그 결과 CloseNativeBridge(true); 가 호출됩니다.

bool LoadNativeBridge(const char* nb_library_filename,
                      const NativeBridgeRuntimeCallbacks* runtime_cbs) {
  // We expect only one place that calls LoadNativeBridge: Runtime::Init. At that point we are not
  // multi-threaded, so we do not need locking here.

  if (state != NativeBridgeState::kNotSetup) {
    // Setup has been called before. Ignore this call.
    if (nb_library_filename != nullptr) {  // Avoids some log-spam for dalvikvm.
      ALOGW("Called LoadNativeBridge for an already set up native bridge. State is %s.",
            GetNativeBridgeStateString(state));
    }
    // Note: counts as an error, even though the bridge may be functional.
    had_error = true;
    return false;
  }

  if (nb_library_filename == nullptr || *nb_library_filename == 0) {
    CloseNativeBridge(false);
    return false;
  } else {
    if (!NativeBridgeNameAcceptable(nb_library_filename)) {
      CloseNativeBridge(true);
    } else {
      // Try to open the library. We assume this library is provided by the
      // platform rather than the ART APEX itself, so use the system namespace
      // to avoid requiring a static linker config link to it from the
      // com_android_art namespace.
      void* handle = OpenSystemLibrary(nb_library_filename, RTLD_LAZY);

      if (handle != nullptr) {
        callbacks = reinterpret_cast<NativeBridgeCallbacks*>(dlsym(handle,
                                                                   kNativeBridgeInterfaceSymbol));
        if (callbacks != nullptr) {
          if (isCompatibleWith(NAMESPACE_VERSION)) {
            // Store the handle for later.
            native_bridge_handle = handle;
          } else {
            ALOGW("Unsupported native bridge API in %s (is version %d not compatible with %d)",
                  nb_library_filename, callbacks->version, NAMESPACE_VERSION);
            callbacks = nullptr;
            dlclose(handle);
          }
        } else {
          dlclose(handle);
          ALOGW("Unsupported native bridge API in %s: %s not found",
                nb_library_filename, kNativeBridgeInterfaceSymbol);
        }
      } else {
        ALOGW("Failed to load native bridge implementation: %s", dlerror());
      }

      // Two failure conditions: could not find library (dlopen failed), or could not find native
      // bridge interface (dlsym failed). Both are an error and close the native bridge.
      if (callbacks == nullptr) {
        CloseNativeBridge(true);
      } else {
        runtime_callbacks = runtime_cbs;
        state = NativeBridgeState::kOpened;
      }
    }
    return state == NativeBridgeState::kOpened;
  }
}

 

 

CloseNativeBridge 함수의 소스코드는 다음과 같은데, CloseNativeBridge(true); 호출 결과 had_error 값이 true 로 설정되는 것을 확인할 수 있습니다.

출처: https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:art/libnativebridge/native_bridge.cc;drc=1aa5d121d7eb21a96f42cfd396e56bf2acdca162;l=225

static void CloseNativeBridge(bool with_error) {
  state = NativeBridgeState::kClosed;
  had_error |= with_error;
  ReleaseAppCodeCacheDir();
}

 

 

그렇다면 had_error 가 메모리의 어디에 위치해 있나가 궁금한데, 그것은 libnativebridge.so 파일을 IDA 로 까보면 알 수 있습니다. 음 소스코드와 IDA 를 비교해보니 had_error 변수가 byte_5288 에 있군요.

 

 

byte_5288 은 .bss segment 의 시작부분입니다.

 

 

이는 앱 프로세스에서도 확인 가능합니다. Zygisk 활성화 후 아무앱이나 실행시키고 Frida 로 libnativebridge.so 의 오프셋 0x5288 로 이동해봅니다. 0x1 이 설정되어 있습니다.

Zygisk 가 비활성화된 상태에서는 had_error 값이 false 이므로 앱 프로세스에서 확인하면 0x0 이 설정되어 있습니다.

 

 

헛?! 그렇다면 이것으로 Zygisk 를 탐지할 수 있는것인가!! 라는 생각이 들 수 있지만, 매지스크 개발자 topjohnwoo 도 알고 있겠죠. 아마 이정도 사소한 것은 모듈단에서 알아서 처리하라고 내비두지 않았을까 생각됩니다.

Shamiko (https://github.com/LSPosed/LSPosed.github.io/releases) 나 Zygisk-Assistant (https://github.com/snake-4/Zygisk-Assistant/releases) 모듈을 설치하면 해당 값을 0x0 으로 패치해줍니다. 따라서 유의미한 탐지 방식이 될 수는 없겠죠.

 

음 여기까지 봤을때 제 짧은 식견으로는 Zygisk 자체를 어떻게 탐지해야 할지 아이디어가 떠오르지 않더군요.

이미 뛰어난 해커, 솔루션 개발자들은 탐지하고 있겠죠?? 저도 더욱 분발해야 겠네요 :)

반응형

'Information Security > Android' 카테고리의 다른 글

Android SO File Dump  (0) 2024.08.31
Frida Build & Debug Using Logs For Android  (0) 2024.08.31
Proxy Android Flutter Apps  (4) 2024.05.19
버sucker 키우기  (0) 2024.01.27
Frida-portal 사용법  (0) 2023.12.01

+ Recent posts