!! 본 글은 어느 정도의 디컴파일 지식이 있어야 이해가 가능합니다.
!! 본 글은 요청에 의해 작성되었습니다.
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. 결과 확인
정상적으로 변경된 걸 볼 수 있습니다.
'한글패치 관련 짧은 글들' 카테고리의 다른 글
catalog.json과 catalog.hash가 있는 유니티 게임의 수정법 (1) | 2024.12.18 |
---|---|
유니티 VideoClip 에셋 교체하기 (3) | 2023.11.18 |
UnityPy TypeTree 적용 (작성중) (0) | 2023.09.15 |
XUnity.AutoTranslator에서 DeepL 사용하기 (17) | 2023.09.06 |
UnityPy를 이용한 bundle파일 내 Monobehaviour 일괄수정 (0) | 2023.08.29 |