한글패치 관련 짧은 글들

번역을 위한 유니티 Il2cpp 게임의 복호화/암호화

Snowyegret 2024. 12. 18. 05:16

!! 본 글은 어느 정도의 디컴파일 지식이 있어야 이해가 가능합니다.

!! 본 글은 요청에 의해 작성되었습니다.

 

 

 

0. 필요 프로그램

- MelonLoader: https://github.com/LavaGang/MelonLoader/releases

- UnityExplorer(GrahamKracker 포크): https://github.com/GrahamKracker/UnityExplorer/releases

- Python, pycryptodome 라이브러리: https://www.python.org/

- Ghidra: https://ghidra-sre.org/

- dnSpyEx: https://github.com/dnSpyEx/dnSpy

- Il2cppDumper: https://github.com/Perfare/Il2CppDumper/releases

 

 

1. 분석 대상 파악

우선, Il2cppDumper나 Cpp2il로 dll을 추출한 다음, dnSpyEX에 로드하여 분석 대상을 파악합니다.

위 경우, Decrypt로 메서드를 검색하였고, 대충 있을법한 클래스에 들어가서 살펴보았습니다.

MoguraCryptAssembly.dll의 Mogura.Crypt 안에 pw 및 salt 변수가 있습니다.

딱 봐도 중요할 것 처럼 생긴 string 변수들입니다.

저 안에 있는 내용을 게임을 직접 실행하여 확인해봅시다.

 

 

 

2. 게임에서 변수 내 값 확인

 

우선, 모드 로더 및 UnityExplorer를 설치해줍시다.

이번 경우엔 MelonLoader 최신버전 (0.6.6) 및 UnityExplorer (GrahamKracker 포크)를 사용하였습니다.

 

게임을 1회 실행시킨 다음, MelonLoader\Il2CppAssemblies 폴더를 열어봅시다.

 

 

아까 봤던 거랑 비슷한 dll파일이 보입니다.

저 파일을 dnSpy에 로드시키면 Il2CppMogura.Crypt.pw를 입력해야 저 변수를 출력할 수 있다는 걸 알게 됩니다.

 

UnityExplorer의 C# Console - REPL 기능을 이용해서 pw와 salt를 알아냅니다.

 

 

 

3. Assembly 파일 분석

 

위에서 작업했던 il2cppdumper 결과물과 Ghidra를 통해 파일 분석을 하겠습니다.

 

우선, il2cpp_header_to_ghidra.py를 실행시켜 il2cpp_ghidra.h를 만듭니다.

그 다음, Ghidra에 GameAssembly.dll을 로드시킵니다.

 

 

 

먼저, Parse C Source를 누르고

 

Parse Configuration을 VisualStudio22_64.prf로 설정한 후,

Save Profile to new name -> ghidra로 작성한 다음 ok를 누릅니다.

 

우측 상단 지우개 버튼을 눌러 모두 지운 후,

대충 위 사진처럼 아까 생성했던 il2cpp_ghidra.h를 등록해두고

Parse to Program - Continue를 눌러 Type data를 불러옵니다.

만약 "Use Open Archives?" 메세지 윈도우가 뜬다면, "Use Open Archives"를 클릭합니다.

오류가 뜬다면 해당 Struct를 없애버리거나, 그냥 패스하고 다음단계로 넘어갑시다.

전 주로 없애고 될때까지 재진행하는 방식으로 하긴 합니다.

 

 

 

상단 초록색 플레이 버튼을 누른 후, 새 스크립트 추가 버튼을 누릅니다.

 

 

 

스크립트 하단부에 ghidra_with_struct.py의 내용을 복사-붙여넣기합니다.

실행 버튼을 눌러 스크립트를 실행한 후, Script.json을 선택하여 정보를 불러옵니다.

 

그 다음, Auto Analysis를 돌립니다.

 

좌측 중앙부에 아까 봐뒀던 네임스페이스를 검색하면, 저렇게 함수들이 나오게 됩니다.

Decrypt가 두 개가 있는데, 위가 텍스트를 Decrypt하는 것이고, 아래가 바이너리 데이터를 Decrypt하는 것입니다.

아래 부분을 디컴파일 하면 다음과 같은 화면이 뜹니다.

 

 

아찔합니다. 중요한 부분만 짚읍시다.

정 모르겠다 싶으면 ChatGPT한테 맡깁시다.

 

// 0x80 = 128비트 키 설정
(**(code **)(param_4[0x1a] + 8))(pauVar4,0x80,*(undefined8 *)param_4[0x1b]);

// 모드 설정 (2 = CBC)
(**(code **)(*(longlong *)*pauVar4 + 0x268))
          (pauVar4,2,*(undefined8 *)(*(longlong *)*pauVar4 + 0x270));

// ppauVar7 -> this. / pauVar1 -> password / pauVar6 -> salt
System.Security.Cryptography.Rfc2898DeriveBytes$$.ctor
          ((longlong)ppauVar7,pauVar1,pauVar6,(undefined (*) [16])0x0);
          
// 반복횟수: 1000
System.Security.Cryptography.Rfc2898DeriveBytes$$set_IterationCount
          ((longlong)ppauVar7,(undefined (*) [32])0x3e8,(undefined (*) [32])0x0,pauVar10);

// PBKDF2 객체 생성
ppauVar7 = thunk_FUN_180218b90(System.Security.Cryptography.Rfc2898DeriveBytes_TypeInfo,
                             param_2,param_3,param_4);

// 키 크기 계산
iVar3 = (**(code **)(*(longlong *)*pauVar4 + 0x218)) // 키 사이즈 프로퍼티(128비트)
                  (pauVar4,*(undefined8 *)(*(longlong *)*pauVar4 + 0x220));
                  
// 키 생성
uVar8 = (**(code **)((*ppauVar7)[0xc] + 8))
                  (ppauVar7,(int)((iVar3 >> 0x1f & 7U) + iVar3) >> 3, // 비트->바이트
                   *(undefined8 *)((*ppauVar7)[0xc] + 0x10));         // GetBytes() 메소드를 호출

// iv 크기 계산
iVar3 = (**(code **)(*(longlong *)*pauVar4 + 0x198)) // 블록 사이즈 프로퍼티(128비트)
                  (pauVar4,*(undefined8 *)(*(longlong *)*pauVar4 + 0x1a0));
// iv 생성
uVar8 = (**(code **)((*ppauVar7)[0xc] + 8))
                  (ppauVar7,(int)((iVar3 >> 0x1f & 7U) + iVar3) >> 3, // 비트->바이트
                   *(undefined8 *)((*ppauVar7)[0xc] + 0x10));         // GetBytes() 메소드를 호출

사실상 이거만 알고 있으면 됩니다.

- 알고리즘: AES-128
- 모드: CBC
- 키 크기: 128비트
- 블록 크기: 128비트

- key와 iv는 pw 및 salt 변수에 있는 값을 통해 생성

 

 

4. 파이썬 코드 작성

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
from Crypto.Protocol.KDF import PBKDF2


def derive_key_and_iv(password, salt, key_length=16, iv_length=16, iterations=1000):
    derived_key_iv = PBKDF2(
        password.encode(), salt, dkLen=key_length + iv_length, count=iterations
    )
    key = derived_key_iv[:key_length]
    iv = derived_key_iv[key_length : key_length + iv_length]
    return key, iv


def encrypt_file(input_filepath, output_filepath, password, salt):
    key, iv = derive_key_and_iv(password, salt)

    with open(input_filepath, "rb") as input_file:
        plaintext_data = input_file.read()

    cipher = AES.new(key, AES.MODE_CBC, iv)
    encrypted_data = cipher.encrypt(pad(plaintext_data, AES.block_size))

    with open(output_filepath, "wb") as output_file:
        output_file.write(encrypted_data)


def decrypt_file(input_filepath, output_filepath, password, salt):
    key, iv = derive_key_and_iv(password, salt)

    with open(input_filepath, "rb") as input_file:
        encrypted_data = input_file.read()

    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = unpad(cipher.decrypt(encrypted_data), AES.block_size)

    with open(output_filepath, "wb") as output_file:
        output_file.write(decrypted_data)


key_phrase = "mogurasofttype"
salt = b"mogumogumogu"

input_filename = "menu.dat"
output_filename = "menu_dec.dat"

decrypt_file(input_filename, output_filename, key_phrase, salt)

 

위에서 알아낸 정보를 바탕으로 파이썬 코드를 짜봅시다.

 

 

 

5. 결과 확인

 

정상적으로 변경된 걸 볼 수 있습니다.