콘텐츠로 이동

앱 서버를 사용한 향상된 탈옥 탐지

향상된 탈옥 탐지 방법의 개요 및 필요성

Section titled “향상된 탈옥 탐지 방법의 개요 및 필요성”

AppSealing SDK 내의 주요 기능 중 하나는 탈옥된 기기의 환경을 탐지하고 앱을 강제로 종료하는 것입니다. 그러나 이러한 탐지 기능이 더 정교한 공격 방법에 의해 우회될 가능성이 있습니다. 이는 iOS 운영 체제의 특성상 앱이 실행될 때 로드된 동적 라이브러리(dylib)의 코드가 먼저 실행되기 때문입니다. 공격자는 이러한 동적 라이브러리에서 실행 파일의 특정 영역을 패치하는 코드를 배포할 수 있습니다.

이 코드 패치가 AppSealing의 탐지 로직이 실행되기 전에 발생하면 앱을 종료하는 코드가 제거되었기 때문에 탈옥이 탐지되더라도 앱은 계속 실행됩니다.

물론 모든 사람이 이러한 유형의 공격을 쉽게 수행할 수는 없지만, 전문적인 해킹 지식을 가진 해커 그룹에 의해 이러한 유형의 공격이 확인되었기 때문에 AppSealing은 이러한 공격 상황을 극복하기 위한 추가적인 탈옥 탐지 방법을 제공합니다.

이 공격 방법의 특징은 실행 중인 앱의 코드를 미리 변경하는 것이므로 AppSealing 라이브러리 자체에 아무리 강력한 탐지 로직을 추가하더라도 동적 라이브러리에 의해 코드가 패치되는 상황은 피할 수 없습니다. 따라서 새로 제공되는 탈옥 탐지 기능은 앱에서 탐지하지 않고, 앱과 연결된 서버에서 탈옥이 의심되는 단말기의 경우 로그인이나 API 호출 수락 등 모든 서비스와 작업을 거부하는 방식입니다.

기본 방법은 AppSealing 인터페이스를 통해 앱에서 서버 자격 증명을 획득하여 기존 인증 매개변수에 추가하고 서버로 전송하는 것입니다.

이 방법은 서버와 연동되지 않는 클라이언트 전용 앱에는 적용할 수 없습니다.

다음 섹션에서는 서버 자격 증명을 획득하고 검증하는 방법을 예제 코드와 함께 설명합니다.

앱이 서버 자격 증명을 검증해야 하는 추가적인 과정은 AppSealing SDK의 함수를 호출하여 서버 자격 증명 문자열을 얻고 기존 인증 매개변수와 함께 서버로 전송하는 것입니다.

서버와 연동되는 대부분의 앱은 사용자 인증 또는 로그인 과정을 거치며, 이 과정에서 사용자가 입력한 계정 정보가 서버로 전송됩니다. 서버로 전송하는 매개변수에 서버 자격 증명 문자열을 추가할 수 있습니다.

서버 자격 증명 문자열은 아래와 같이 Unity_GetEncryptedCredential 함수를 호출하여 얻습니다. 문자열을 받을 버퍼는 290바이트 이상으로 선언해야 합니다.

Unity3D 프로젝트를 위한 샘플 자격 증명 획득 코드

StringBuilder encryptedCredential = new StringBuilder(290);
result = Unity_GetEncryptedCredential( encryptedCredential );
if ( result == 0 )
{
string strEncryptedCredential = encryptedCredential.ToString();
UnityEngine.Debug.Log( "AppSealing Credential : " + strEncryptedCredential );
// LoginToServer( userID, password, strEncryptedCredential );
}

자격 증명 문자열을 얻은 후, 앱이 로그인 또는 인증 과정을 위해 서버와 통신할 때 추가 매개변수로 전송하고 서버에서 검증하도록 합니다.

서버가 자격 증명 검증에 실패하면 로그인도 실패하도록 강제하고 앱이 더 이상 진행되지 않도록 해야 합니다. 그러나 로그인 결과를 확인하고 앱을 종료하는 코드 등은 공격자에 의해 변조될 가능성이 높으므로, 가장 좋은 방법은 서버가 자격 증명 검증에 실패한 후 해당 클라이언트의 모든 요청에 대해 서비스나 응답을 거부하도록 서버를 구성하는 것입니다.

이는 다음 섹션에서 다시 논의될 것입니다.

AppSealing 모듈에 대한 인터페이스 호출에서 반환되는 자격 증명 데이터(hex 문자열)는 AppSealing 내부의 보안 로직이 정상적으로 수행되고 기기에서 위험한 상황이 탐지되지 않았을 때만 유효합니다.

동적 라이브러리를 통해 코드 패치 공격이 이루어지거나 다른 방법으로 보안 로직이 우회되면 유효한 자격 증명 데이터가 생성되지 않으므로 서버는 이 값을 검증하고 기기의 공격 상황을 차단해야 합니다.

앱 서버는 클라이언트(앱)가 전송한 자격 증명 값이 올바른지 확인해야 하며, 올바르지 않으면 인증(로그인)을 거부한 다음 해당 클라이언트가 요청하는 모든 서비스(API 호출)를 거부해야 합니다.

서버에서 자격 증명 데이터를 검증하려면 클라이언트에서 전송된 데이터를 복호화하기 위한 AES Key와 IV, 그리고 비교 및 검증을 위한 원본 자격 증명 데이터가 필요합니다.

이 모든 값은 ADC의 프로젝트에서 “Check Credential” 버튼을 통해 획득할 수 있습니다. 여기에 표시된 Hex 문자열을 복사하여 예제 코드에 붙여넣고 사용하면 됩니다. 먼저 아래 화면과 같이 ADC에 접속하여 프로젝트 박스의 “Check Credential” 버튼을 클릭합니다.

버튼을 클릭하면 다음 창이 표시되며, 여기서 Credential 값과 복호화에 사용할 IV 및 키를 확인할 수 있습니다. 문자열 왼쪽의 복사 버튼을 사용하여 이 값을 그대로 복사하고 서버 측 검증 코드에 붙여넣어 사용할 수 있습니다.

버튼을 클릭하면 다음 창이 표시되며, 여기서 Credential 값과 복호화에 사용할 IV 및 AES 키를 확인할 수 있습니다. 문자열 왼쪽의 복사 버튼을 사용하여 이 값을 그대로 복사하고 서버 측 검증 코드에 붙여넣어 사용할 수 있습니다.

[서버 코드가 Node.js/Javascript를 사용하는 경우]

Section titled “[서버 코드가 Node.js/Javascript를 사용하는 경우]”

기존 코드에 다음 코드를 추가하고 자격 증명 데이터 검증에 사용하세요. 기존 서버 코드의 로그인 또는 인증 함수에서 매개변수로 전송된 자격 증명 값을 아래 코드에서 제공되는 verfityAppSealingCredential 함수에 전달하여 진위성을 판단할 수 있습니다. (아래 코드는 appsealing_credential.js라는 파일명으로 SDK에도 포함되어 있습니다).

** 참고: 아래 코드에서 ORG_CREDENTIAL, AES_IV, AES_KEY는 ADC의 해당 프로젝트의 “Check Credential” 기능을 통해 얻은 값으로 교체해야 합니다. 예제 코드를 그대로 가져와서 사용하면 자격 증명 값이 제대로 검증되지 않습니다.

var crypto = require('crypto');
function verifyAppSealingCredential( credential )
{
// Need to Change : Get From ADC (via 'Check Credential') -----------------------------------------
const ORG_CREDENTIAL = "572E0E1459453F2078D6576FF71ECD0DBCA0484430C7FA7FE45B788A37DE3A04204F5A55FEA83AC9AFBA2C688594F75A3828B23972DB34858EC4F6CC3202533E44121E5F2614B227E18B6419A83810F7511D5E51FCACD5175A1CC550F83CB874A7378ACDAFE78EB2E329CD5D3C384061C4669674F1EE6B1B59FB7D91835DB7EE";
const AES_IV = "055772B7434A4174749AFE09B1413472";
const AES_KEY = "71CA94A64A4DEBF5566495AB03F6798F";
//-------------------------------------------------------------------------------------------------
// convert credential from hex string to byte array
let decrypted_UTC = 0, decrpyted_buffer, aes_key2;
// decrypt UTC
try
{
const decipher = crypto.createDecipheriv( 'aes-128-ctr', Buffer.from( AES_KEY, 'hex' ), Buffer.from( AES_IV, 'hex' ));
decrpyted_buffer = Buffer.concat( [decipher.update( credential.substr( 0, 32 ), 'hex' ), decipher.final()] );
decrypted_UTC = decrpyted_buffer.slice( 0, 8 ).readUInt32LE();
}
catch( error )
{
throw error;
}
// verfity UTC with current time (+/-) 10sec
const current_UTC = parseInt( Date.now() / 1000 ); // get current UTC in seconds
if ( Math.abs( current_UTC - decrypted_UTC ) > 10 )
{
console.log( "Invalid UTC value has sent, deny login & all services for this client... " + Math.abs( current_UTC - decrypted_UTC ) );
return false;
}
console.log( "** UTC verified : " + decrypted_UTC + " (current = " + current_UTC + ", diff = " + Math.abs( current_UTC - decrypted_UTC ) + ")" );
// get AES KEY2
aes_key2 = Buffer.concat( [new Uint8Array( decrpyted_buffer.slice( 0, 8 )), new Uint8Array( Buffer.from( ORG_CREDENTIAL.substring( 52, 52 + 16 ), 'hex' ))] );
for( let i = 0; i < 16; i++ )
aes_key2[i] ^= Buffer.from( AES_IV.substring( i * 2, i * 2 + 2 ), 'hex' ).readUInt8();
// decrypt credential
let decrypted_credential = [];
try
{
const decipher = crypto.createDecipheriv( 'aes-128-ctr', aes_key2, Buffer.from( AES_IV, 'hex' ));
decrypted_credential = Buffer.concat( [decipher.update( credential.substr( 32, 256 ), 'hex' ), decipher.final()] );
}
catch( error )
{
throw error;
}
// verfity credential with CREDENTIAL(ADC)
// return if fail
if ( ORG_CREDENTIAL.toLowerCase() != decrypted_credential.toString( 'hex' ).toLowerCase() )
{
console.log( "Invalid credential value has sent, deny login & all services for this client..." );
return false;
}
console.log( "** Credential verified : PASS" );
return true;
}

[서버 코드가 Java를 사용하는 경우]

Section titled “[서버 코드가 Java를 사용하는 경우]”

기존 코드에 다음 코드를 추가하고 자격 증명 데이터 검증에 사용하세요. 기존 서버 코드의 로그인 또는 인증 함수에서 매개변수로 전송된 자격 증명 값을 아래 코드에서 제공되는 verfityAppSealingCredential 함수에 전달하여 진위성을 판단할 수 있습니다. (아래 코드는 appsealing_credential.java라는 파일명으로 SDK에도 포함되어 있습니다).

** 참고: 아래 코드에서 ORG_CREDENTIAL, AES_IV, AES_KEY는 ADC의 해당 프로젝트의 “Check Credential” 기능을 통해 얻은 값으로 교체해야 합니다. 예제 코드를 그대로 가져와서 사용하면 자격 증명 값이 제대로 검증되지 않습니다.

[Java 코드는 원본과 동일하게 유지]

[C# 코드는 원본과 동일하게 유지]

[서버 코드가 python을 사용하는 경우]

Section titled “[서버 코드가 python을 사용하는 경우]”

[Python 코드는 원본과 동일하게 유지]

[서버 코드가 ruby 스크립트를 사용하는 경우]

Section titled “[서버 코드가 ruby 스크립트를 사용하는 경우]”

[Ruby 코드는 원본과 동일하게 유지]

[서버 코드가 C++을 사용하는 경우]

Section titled “[서버 코드가 C++을 사용하는 경우]”

[C++ 코드는 원본과 동일하게 유지]