위협 감지 시 앱을 종료하지 않음(Do not terminate the App on detecting Threat) 옵션을 활성화하면 SDK가 앱을 강제 종료하지 않습니다. 이 가이드의 샘플 코드를 활용해 위협 이벤트를 추적하고 대응 방식을 직접 결정하세요. 옵션을 활성화하지 않은 경우 SDK가 내부적으로 앱을 종료하며, 샘플 코드는 알림 팝업 표시 용도로 동작합니다.
generate_hash 실행 시 서버는 해당 프로젝트의 가장 마지막으로 다운로드된 SDK 설정을 전체 팀에 적용합니다. 팀원들이 서로 다른 설정으로 다운로드하면 마지막 다운로드 설정이 우선합니다. SDK 다운로드 전 팀 전체가 설정을 통일하세요.SDK에 포함된 Code Samples.txt에는 각 프레임워크별로 바로 사용할 수 있는 샘플 코드가 제공됩니다. 코드를 그대로 사용하거나, 사용자 ID, 디바이스 정보, 위협 유형 등 추가 데이터를 수집하도록 확장할 수 있습니다.
동작 방식
각 플랫폼의 샘플 코드는 비트마스크 값(tamper / security_threat / ret)으로 위협을 감지합니다. 종료 호출(_exit(0) 또는 ExitApp())은 알림창의 확인 버튼 핸들러 안에서 이루어집니다. 커스텀 로직은 종료 호출 직전에 추가합니다.
| 프레임워크 | 코드 위치 |
|---|---|
| Xcode (Swift), Xcode (Obj-C), Flutter, React Native | SDK의 Code Samples.txt에서 복사하여 프로젝트의 해당 파일에 붙여넣기 |
| Unreal Engine | SDK에 AppSealingPlugin.cpp가 이미 포함되어 있음 — 해당 파일 직접 수정 |
일반적인 커스터마이징 순서:
- 샘플 코드를 프로젝트에 붙여넣기 (Unreal 제외)
- 필요한 데이터 수집 (사용자 ID, 디바이스 정보, 위협 플래그, 타임스탬프 등)
- 서버로 전송 — 앱이 곧 종료되므로 fire-and-forget 방식 사용
- 필요 시 서버 측에서 세션 무효화
- 기존 종료 호출 유지
_exit(0) 직전의 네트워크 요청은 완료되지 않을 수 있습니다. 전송 보장이 필요하다면 알림창 표시 전에 서버를 호출하거나, 서버 측 세션 타임아웃을 폴백으로 구현하세요.위협 감지 플래그
tamper 값은 비트마스크입니다. 각 비트는 특정 위협에 대응합니다:
| 플래그 | 설명 |
|---|---|
kAppSealingErrorJailbreakDetected | 탈옥된 기기 |
kAppSealingErrorDRMDecrypted | 실행 파일이 암호화되지 않음 |
kAppSealingErrorDebugAttached | 디버거가 연결됨 |
kAppSealingErrorHashInfoCorrupted / kAppSealingErrorHashModified | 앱 무결성 훼손 |
kAppSealingErrorCodesignCorrupted / kAppSealingErrorExecutableCorrupted | 실행 파일 훼손 |
kAppSealingErrorCertificateChanged | 앱이 재서명됨 |
kAppSealingErrorBlacklistCorrupted | 블랙리스트/화이트리스트 누락 또는 훼손 |
kAppSealingErrorCheatToolDetected | 치트 툴 감지 |
프레임워크별 커스터마이징
항목을 클릭하면 상세 내용을 확인할 수 있습니다.
▶ Xcode (Swift)
샘플 코드를 ViewController.swift에 붙여넣습니다. 커스터마이징 위치는 UIAlertAction 핸들러 안, _exit(0) 직전입니다.
inst._IsAbnormalEnvironmentDetectedAsync { tamper in
if ( tamper > 0 ) {
// ... msg 문자열 구성 (기존 코드 유지) ...
let alertController = UIAlertController(title: "AppSealing", message: msg, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Confirm", style: .default,
handler: { (action: UIAlertAction!) -> Void in
#if !DEBUG
// ▼ 커스텀 로직 추가 ▼
let userID = UserDefaults.standard.string(forKey: "userID") ?? "unknown"
let payload: [String: Any] = [
"userID": userID,
"threatFlags": tamper,
"timestamp": Date().timeIntervalSince1970
]
MyAnalyticsClient.sendSecurityEvent(payload) // fire-and-forget
// ▲ 커스텀 로직 끝 ▲
_exit(0)
#endif
}))
self.present(alertController, animated: true, completion: nil)
}
}스위즐링 감지 이벤트에 대해서는 _NotifySwizzlingDetected 콜백에도 동일하게 적용합니다.
▶ Xcode (Objective-C)
샘플 코드를 ViewController.mm에 붙여넣습니다. 커스터마이징 위치는 UIAlertAction 핸들러 블록 안, _exit(0) 직전입니다.
[inst _IsAbnormalEnvironmentDetectedAsync:^( int tamper ) {
if ( tamper > 0 ) {
// ... msg 문자열 구성 (기존 코드 유지) ...
UIAlertAction *confirm = [UIAlertAction actionWithTitle:@"Confirm"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
#if !DEBUG && !defined(DEBUG)
// ▼ 커스텀 로직 추가 ▼
NSString *userID = [[NSUserDefaults standardUserDefaults]
stringForKey:@"userID"] ?: @"unknown";
NSDictionary *payload = @{
@"userID": userID,
@"threatFlags": @(tamper),
@"timestamp": @([[NSDate date] timeIntervalSince1970])
};
[MyAnalyticsClient sendSecurityEvent:payload]; // fire-and-forget
// ▲ 커스텀 로직 끝 ▲
_exit(0);
#endif
}];
[alert addAction:confirm];
[self presentViewController:alert animated:YES completion:nil];
}
}];스위즐링 감지 이벤트에 대해서는 _NotifySwizzlingDetected 콜백에도 동일하게 적용합니다.
▶ Flutter
샘플 코드를 AppDelegate.swift(FlutterAppDelegate 상속)에 붙여넣습니다. Swift 샘플과 구조가 동일하며, 알림창 표시를 DispatchQueue.main.async 안에서 처리한다는 차이가 있습니다.
inst._IsAbnormalEnvironmentDetectedAsync { tamper in
if ( tamper > 0 ) {
// ... msg 문자열 구성 (기존 코드 유지) ...
let alertController = UIAlertController(title: "AppSealing", message: msg, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Confirm", style: .default,
handler: { (action: UIAlertAction!) -> Void in
#if !DEBUG
// ▼ 커스텀 로직 추가 ▼
let userID = UserDefaults.standard.string(forKey: "userID") ?? "unknown"
let payload: [String: Any] = [
"userID": userID,
"threatFlags": tamper,
"timestamp": Date().timeIntervalSince1970
]
MyAnalyticsClient.sendSecurityEvent(payload) // fire-and-forget
// ▲ 커스텀 로직 끝 ▲
_exit(0)
#endif
}))
DispatchQueue.main.async {
self.window?.rootViewController?.present(alertController, animated: true, completion: nil)
}
}
}팁: Flutter 메서드 채널을 거치지 않고 Swift 레이어에서 직접 서버를 호출하는 방식을 권장합니다. 앱이 곧 종료되므로 채널 호출이 완료되지 않을 수 있습니다.
▶ React Native
샘플 코드를 index.js에 붙여넣습니다. 감지 포인트가 두 군데 있으며, 두 곳 모두에 커스터마이징을 적용하세요.
1. 초기 감지 (checkSecurityThreat)
{ text: "Confirm", onPress: () => {
// ▼ 커스텀 로직 추가 ▼
fetch('https://your-server.com/security-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userID: getUserID(),
threatFlags: security_threat,
timestamp: Date.now()
})
}).catch(() => {}); // fire-and-forget
// ▲ 커스텀 로직 끝 ▲
AppSealingInterfaceBridge.ExitApp();
}}2. 주기적 감지 (AppSealingPeriodicCheck)
{ text: "Confirm", onPress: () => {
// ▼ 커스텀 로직 추가 ▼
fetch('https://your-server.com/security-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userID: getUserID(),
threatCode: ret,
timestamp: Date.now()
})
}).catch(() => {});
// ▲ 커스텀 로직 끝 ▲
AppSealingInterfaceBridge.ExitApp();
}}참고: 샘플 코드는 Platform.OS === 'ios' 조건 하에 감지 함수를 호출합니다. Android 동작은 Android SDK 네이티브 레이어에서 별도로 처리됩니다.
▶ Unreal Engine
SDK에 감지 로직이 이미 작성된 AppSealingPlugin.cpp가 포함되어 있습니다. 알림 다이얼로그와 exit(0) 호출은 기본적으로 #if 0 블록으로 비활성화되어 있습니다.
활성화 및 커스터마이징 방법:
- SDK에서
AppSealingPlugin.cpp파일 열기 #if 0을#if 1로 변경exit(0)직전에 커스텀 로직 추가
AsyncTask(ENamedThreads::GameThread, [Fs, tamper]()
{
#if 1 // <-- 0을 1로 변경하여 활성화
// ▼ 커스텀 로직 추가 ▼
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request =
FHttpModule::Get().CreateRequest();
Request->SetURL(TEXT("https://your-server.com/security-event"));
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
FString Body = FString::Printf(TEXT("{\"threatFlags\":%d}"), tamper);
Request->SetContentAsString(Body);
Request->ProcessRequest(); // fire-and-forget
// ▲ 커스텀 로직 끝 ▲
FText title = FText::FromString(TEXT("Security Threat Detected"));
if (FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Fs), &title)
== EAppReturnType::Ok)
exit(0);
#endif
});Unreal_IsAbnormalEnvironmentDetected는 블로킹 호출입니다. 샘플 코드에서 이미 AsyncTask(ENamedThreads::AnyThread, ...)로 래핑되어 있습니다. GameThread로 직접 이동하면 렌더 루프가 블로킹되므로 주의하세요.
수집 가능한 정보
아래는 예시입니다. 실제로 어떤 정보를 수집할지는 서비스 환경에 맞게 직접 결정하세요.
| 데이터 | 수집 방법 |
|---|---|
| 감지된 위협 유형 | tamper 비트마스크 값 (위 플래그 표 참고) |
| 사용자 / 계정 ID | 앱 내 세션 또는 UserDefaults |
| 디바이스 ID | Unreal_GetAppSealingDeviceID (Unreal), 또는 플랫폼 API (UIDevice 등) |
| 앱 버전 | Bundle.main.infoDictionary (iOS), 또는 앱 내 상수 |
| 타임스탬프 | Date() / Date.now() |