Tech
앱 보안을 강화하는 유니티 코드 난독화 - 2부
2023. 11. 16


안녕하세요, 공통 모듈 개발과 기술 R&D를 통해 쿡앱스의 기술적 성장을 위한 업무를 맡는 기술지원팀 클라이언트 프로그래머 이진호입니다. 이번 아티클에서는 난독화 과정에서 주의해야 할 체크리스트 5가지를 소개하겠습니다. 이 체크리스트들을 점검하지 않을 경우 정상적인 호출 또는 저장이 불가능하거나, 특정 메서드를 찾지 못하는 상황이 발생할 수 있습니다. 

 


 

첫 번째: Serialize

먼저 정상적인 저장을 위해서는 Serialize에 관련된 모든 것에 난독화 제외가 필수입니다. JSON은 유저 정보 저장, 네트워크 전송 등 상당히 많은 곳에서 사용하는 데이터 포맷입니다. 보통 객체를 직렬화하여 JSON 데이터를 생성하는데, 난독화를 하게 되면 이러한 JSON 데이터의 키 이름도 바뀌게 됩니다. 다음은 JSON 직렬화의 일반적인 표본입니다.

// Serialize 정보
public class UserData
{
    public int Gold;
    public int Jewel;
    public int Level;
} 
/*
json 변환 결과
{
  "Gold":100,
  "Jewel":200,
  "Level":1,
}
*/
JSON 직렬화의 일반적인 표본

 

JSON 직렬화 결과 해당 필드의 이름을 기준으로 JSON 키가 지정됩니다. 만일 난독화를 한다면 기존의 클래스, 필드 이름 모두 난독화가 진행이 되므로, 다음과 같은 결과를 얻을 수 있습니다.

// 난독화 이후
public class 괌괌괈괋
{
  public int 괌괆괎괍;
  public int 괃괋괄괆;
  public int 괏관괆괉;
}
/*
json 변환 결과
{
  "괌괆괎괍":100,
  "괃괋괄괆":200,
  "괏관괆괉":1,
}
*/
난독화되어 JSON 직렬화의 오류 발생 표본

 


 

두 번째: Unity Event 등록

유니티에서 작업할 때 Button, Animation 등의 Event를 인스팩터에서 등록하여 사용하는 경우가 많이 있습니다. 이때 Event는 메서드의 이름을 기준으로 Event가 전달됩니다. 이런 경우 Event 메서드의 이름이 난독화되면 Event를 전달받지 못하게 됩니다.

Event 메서드의 이름을 난독화 제외 필요

 

아래 코드의 경우 Player  OnButton_Fire, OnAnimation_Dead 메서드가 각각 Button, Animation 의 Event로 등록됐습니다. 만일 메서드 이름이 난독화되어 변경되면 유니티는 해당 메서드를 찾지 못하여 정상적인 실행이 되지 않습니다. 따라서 스크립트 상에서 Event 등록을 진행하거나 난독화에서 제외를 하여야 합니다.

public class Player : MonoBehaviour
{
  [Token(Token = "0x6000013")]
  [Address(Offset = "0x2558C0", RVA = "0x2558C0", VA = "0x2558C0")]
  public Player()  {}
  
  [Token(Token = "0x6000019")]
  [Address(Offset = "0x2558DC", RVA = "0x2558DC", VA = "0x2558DC")]
  public void OnButton_Fire()  {}    //<---------- 난독화 제외
  
  [Token(Token = "0x6000017")]
  [Address(Offset = "0x2558D4", RVA = "0x2558D4", VA = "0x2558D4")]
  public void OnAnimation_Dead()  {} //<---------- 난독화 제외
  
  [Token(Token = "0x6000012")]
  [Address(Offset = "0x2558BC", RVA = "0x2558BC", VA = "0x2558BC")]
  public void 괇괄괋괂괏괃괇괇괃괈관()  {}
  [Token(Token = "0x6000014")]
  [Address(Offset = "0x2558C8", RVA = "0x2558C8", VA = "0x2558C8")]
  public void 괃괋괋괂괁괁괅괎괆괏괏()  {}
  [Token(Token = "0x6000015")]
  [Address(Offset = "0x2558CC", RVA = "0x2558CC", VA = "0x2558CC")]
  public void 괇괈괍괊괇괉괍괊괃괍관()  {}
}
Unity Event 등록을 진행해 난독화에서 제외한 경우

 


 

세 번째: Reflection

Reflection을 통해 Foo.Bar 메서드를 사용할 경우, Bar 메서드의 난독화를 제외해야 합니다. GetMethod("BAR") 에서 string타입의 Bar 를 전달하여 메서드를 얻어오게 되는데, 이러한 경우 Bar 메서드 이름이 난독화되면 정상 작동하지 않을 수 있습니다. 

public static class Foo
{
    // Reflection 사용을 위한 제외 필요함
    public static void Bar()
    {
        Debug.Log("Bar");
    }
}
public class ReflectionTest
{
    public void Test()
    {
        Type type = typeof(Foo);
        MethodInfo method = type.GetMethod("Bar");
        method.Invoke(null, null);
    }
}
  [Address(Offset = "0x2558C8", RVA = "0x2558C8", VA = "0x2558C8")]
  public void 괃괋괋괂괁괁괅괎괆괏괏()  {}
  [Token(Token = "0x6000015")]
  [Address(Offset = "0x2558CC", RVA = "0x2558CC", VA = "0x2558CC")]
  public void 괇괈괍괊괇괉괍괊괃괍관()  {}
}
Bar 메서드의 난독화를 제외한 경우

 


 

네 번째: RuntimeInitializeOnLoadMethod

RuntimeInitializeOnLoadMethod 를 사용한 경우 해당 클래스, 메서드를 난독화에서 제외하여야 앱 실행 시 정상적인 호출이 가능합니다. 앱 시작 시 초기화 작업, 기타 등등의 이유로 메서드 호출이 필요한 경우RuntimeInitializeOnLoadMethod 어트리뷰트를 사용하게 됩니다. 이때 유니티는 메서드 이름을 기준으로 이를 감지하기 때문에 RuntimeInitializeOnLoadMethod 를 사용한 경우에는 해당 메서드와 클래스 이름이 난독화에서 제외돼야 합니다.

// 난독화 전
public static class SecretManager
{
    [RuntimeInitializeOnLoadMethod]
    private static void Init()
    {
        // 중요 정보 초기화
    }
    
    private static void Method1()
    {
    }
}
[RuntimeInitializeOnLoadMethod] 사용하여 앱 실행시 초기화 진행 예제

 

위의 코드를 빌드 이후 언패킹하여 살펴보면 RuntimeInitializeOnLoad.json 파일에 아래의 정보를 확인할 수 있습니다.

[RuntimeInitializeOnLoadMethod] 사용시 빌드 최종 저장

 

결국 유니티는 RuntimeInitializeOnLoadMethod 어트리뷰트가 붙은 메서드를 파일에 저장하고, 실행 시 클래스, 메서드 이름을 기반으로 앱 실행시 호출하게 됩니다. 이때 난독화로 SecretManager.Init 의 이름이 바뀌면 유니티는 해당 클래스, 메서드를 찾지 못하여 호출이 불가능합니다. 

// 난독화 후
[Token(Token = "0x2000008")]
public static class SecretManager  // <----- 난독화 제외
{
  [Token(Token = "0x6000023")]
  [Address(Offset = "0x255990", RVA = "0x255990", VA = "0x255990")]
  [RuntimeInitializeOnLoadMethod]
  private static void Init()      // <----- 난독화 제외
  {
  }
  [Token(Token = "0x6000024")]
  [Address(Offset = "0x255994", RVA = "0x255994", VA = "0x255994")]
  private static void 괍괅괌괏괁괋괃괏괁괄괁()
  {
  }
}
클래스, 메서드를 난독화에서 제외한 표본

 


 

다섯 번째: MonoBehaviour 상속

유니티에서 클래스를 MonoBehaviour로부터 상속받아 사용하는 경우 역시 난독화에서 제외하여야 합니다. 이는 MonoBehaviour가 이름 기반의 컴포넌트라 이를 변경하게 되면 기존의 이름으로 액세스가 불가능해지기 때문입니다. 아래 예제를 보면 TextGold MonoBehaviour 로부터 상속받아 구현하였습니다.
 

// 난독화 전
public static class GoldManager
{
    // 보유 골드
    public static int Gold = 100;
}
//----------------------------------------------
// 난독화 전
// 골드를 Text에 표시
public class TextGold : MonoBehaviour
{
    public Text _text;
    private void Start()
    {
      // GoldManager에서 골드 가져와 Text에 표시
      _text.text = GoldManager.Gold.ToString();
    }
    public void Method1() {}
}

// 난독화 후
public class TextGold : MonoBehaviour
{
  public Text _text;
  private void Start() 
  {
    // BJJDFMFNAKN.BOGOAOLFNBE Gold에 관련된것 유추가능
    this._text.text = BJJDFMFNAKN.BOGOAOLFNBE().ToString();
  }
  private void OGFAAFKDIKA()
  {
  }
}
MonoBehaviour로부터 상속 받아 구현한 TextGold

 

TextGold 난독화에서 제외한 후 빌드를 하여야 하는데, 빌드 결과물을 디컴파일러로 복원하면 이러한 클래스의 이름이 그대로 드러나게 됩니다. 따라서 난독화된 BJJDFMFNAKN 클래스가 Gold 에 관련된 것이라고 유추할 수 있습니다.

이러한 경우처럼 MonoBehaviour를 상속받는 컴포넌트로부터 유추가 가능하므로, TextGold 의 이름을 변경하거나 GoldManager 를 직접 액세스 하기보다는 메시지, 이벤트 기반의 로직으로 변경하는 것을 권장합니다.

 


 

마치며

세상에 완벽한 보안은 없습니다. 해커는 전혀 생각도 못 한 틈을 찾아 공격을 시도하기 때문입니다. 예상치 못한 공격으로 해킹에 성공하더라도 사후 데이터 검증을 통해 해커를 제재하는 등 서비스 보호를 위해 다양한 조처를 해야 합니다. 따라서 코드 작성 시 “내가 해커라면 어떻게 분석하고 공격할까?”라는 생각을 가끔 해보는 것도 보안에 도움이 됩니다.