안드로이드 리듬 게임 핵의 동작 분석

dusty·2024년 10월 5일
post-thumbnail

(썸네일은 글의 내용과 무관합니다 - 저는 mod apk가 정상 동작 하는지 튜토리얼에서 노트 두세개만 쳐보고 껐습니다.)

mod apk는 apk를 수정해서 원본과 다른 기능을 하도록 한 apk를 말합니다. 원본 앱의 기능에 따라 mod apk에서 수정하거나 추가하는 기능이 크게 달라지겠지만, 게임 앱의 경우는 주로 치트, 핵이라고 부르는 부정행위를 가능하게 하는 mod apk가 많이 배포되고 있습니다.

이번 글에서는 특정 리듬 게임의 mod apk를 대상으로, 어떤 방식으로 부정 행위를 하는지 간단하게 분석해보았습니다. 리듬 게임인 만큼, mod apk는 판정을 전부 퍼펙트(가장 높은 판정)로 바꿔주는 기능과 노트를 치는데 실패해도 체력이 감소하지 않는 기능을 가지고 있습니다.

분석에 사용한 mod apk는 liteapks[.]com에서 다운로드 받았습니다. (590b44efdb8051cce659c4ba232ae7a6ab573e5810f1efcae9b7b40633d32a27)

들어가기에 앞서

당연한 이야기이지만, 이 글의 목적은 '어떤 방식으로 부정 행위를 진행하는지' 분석해보는 것입니다.

이 게임 혹은 다른 게임의 mod apk나 치트, 핵을 제작하는데 도움이 되고자 하는 의도는 전혀 없으며, 따라서 mod apk에서 부정 행위를 구현하는 정확한 방식을 모두 글에서 공개하지 않으려 합니다.

(물론 mod apk의 분석 자체가 어렵거나 하지 않으므로, 글의 내용을 기반으로 직접 분석이 가능하신 분들이 많으실 것 같기는 합니다. 다만, 제 글의 내용만 가지고 부정 행위가 가능한 분들은 이 글 없이도 원하는 목적을 달성하실 수 있는 분들이라고 생각합니다.)

무결성 검사 우회 등

대상 mod apk는 흔히 PmsHook이라고 불리는 방식을 이용해서 PackageManager를 이용한 apk의 서명 정보 검사를 우회합니다.


https://github.com/L-JINBIN/ApkSignatureKiller/tree/master
레포 내의 코드를 그대로 사용하지는 않았고, assets 폴더에 원본 apk를 포함하여 이를 이용해 apk 파일에 대한 무결성 검사 등을 우회하려는 시도 또한 진행하고 있습니다.

c1afe52005fb6ee27681655996a99afa73ba2f7a4da633d209c2a55818779d41  BanG Dream! Girls Band Party!_7.0.1_APKPure.apk
c1afe52005fb6ee27681655996a99afa73ba2f7a4da633d209c2a55818779d41  bang-dream-en-v7.0.1-mod/assets/xctmx

47cc56681ce1a566e80cdbb4f704b30021df31c11d19bf4cc586d5bcb01ce3b8  iozon.out/lib/arm64-v8a/libil2cpp.so
47cc56681ce1a566e80cdbb4f704b30021df31c11d19bf4cc586d5bcb01ce3b8  BanG-Dream-Girls-Band-Party-v7.6.0-mod-LiteApks/lib/arm64-v8a/libil2cpp.so
47cc56681ce1a566e80cdbb4f704b30021df31c11d19bf4cc586d5bcb01ce3b8  BanG Dream! Girls Band Party!_7.6.0_APKPure.xapk/config.arm64_v8a/lib/arm64-v8a/libil2cpp.so

(com.bushiroad.en.bangdreamgbp 7.6.0을 aab (xapk) 형태가 아니라 apk로 떠둔 미러가 없어서 부득이하게 동일한 제작자의 7.0.1 버전 sha256을 비교한 결과입니다. 7.6.0 mod apk도 iozon 파일과 원본 각각의 libil2cpp.so 등의 해시를 비교하면 사실상 동일한 apk인 점을 확인할 수 있습니다.)

후킹을 통한 동작 수정

모딩의 대상이 되는 리듬 게임은 유니티 엔진을 이용하여 개발되어있고, apk 내의 lib 디렉토리를 확인하면 알 수 있듯이 IL2CPP 백엔드를 사용하고 있습니다. (즉 libil2cpp.so 파일 내에 실제 게임 코드가 존재하는 상태입니다.)

mod apk의 dex에서 원본과 다른 부분은 위에서 다룬 PackageManager 관련 수정과, libliteapks.so 파일을 로딩하는 코드, mod apk의 기능을 설정하기 위한 플로팅 메뉴를 구현하는 코드 외에는 없습니다. (JNI 메서드들 또한 이러한 플로팅 메뉴를 구현하는 코드 이외에, 부정 행위를 직접 구현하는 코드는 별도로 없었습니다. 플로팅 메뉴는 https://github.com/LGLTeam/Android-Mod-Menu 를 기반으로 하는 것으로 보입니다.) 이러한 점으로 보아 실제 동작의 수정은 해당 파일 내의 네이티브 코드를 통해 이루어질 것으로 볼 수 있습니다.

libliteapks.so 파일은 문자열 등에 난독화를 진행하여, 분석을 저해하려는 시도를 하고 있습니다.

디버거 탐지 등은 별도로 진행하지 않으므로 Frida 등의 DBI나 기타 디버거를 사용하여 바이너리를 동적으로 분석하는 데에 큰 문제가 없습니다. 또한 mod apk의 기능 자체가 게임 내의 판정을 수정하는 등의 동작임을 감안하면, libil2cpp.so 파일을 변조하는 식으로 이루어질 것이라고 예상할 수 있습니다.

libliteapks.so 파일 내의 문자열이 대다수 난독화된 상태지만, "A64_HOOK" 등의 문자열은 난독화되지 않은 상태입니다. 해당 문자열을 검색하면 https://github.com/Rprop/And64InlineHook/tree/master 레포를 찾을 수 있습니다. (코드를 100% 동일하게 사용했다고 볼 수는 없지만,) 레포 내의 .cpp 파일을 확인해보면 A64_HOOK이라는 문자열을 동일하게 __android_log_print 함수에 사용하고 있고, log_print 함수에 사용하는 문자열을 통해 해당 함수들의 원래 이름과 시그니쳐, 동작 등을 유추할 수 있습니다.

해당 문자열을 통해, 0x664e4, 0x66f44에 위치한 함수가 각각 A64HookFunctionV, FastAllocateTrampoline 와 동일한 함수이거나 혹은 그와 유사한 기능을 할 것이라고 추측할 수 있습니다.
두 함수를 분석해보면, FastAllocateTrampoline 함수에서 A64HookFunctionV 함수를 이용하여, arg1을 arg2로 점프하도록 후킹하는 식의 동작을 하는 점을 확인할 수 있습니다.

FastAllocateTrampoline 함수의 xref는 0x63ab8의 함수 1곳에만 존재합니다.
해당 함수에서 FastAllocateTrampoline 함수 호출 시에 사용하는 아규먼트를 확인하면, 어떤 함수를 후킹하여 부정 행위를 진행하는지 파악할 수 있습니다.

63ab8의 함수는 0x65714의 함수에서 별도의 스레드로 생성되며, 65714의 함수는 .init_array 섹션에 등록된 것을 확인할 수 있습니다.

즉, 63ab8 함수의 동작을 확인하기 위해서는 pthread_create를 후킹하여, 63ab8을 후킹하는 경우 해당 함수에 추가로 훅을 진행하도록 프리다 스크립트를 짜거나 .init_array 내의 함수 실행이 완료되기 전에 해당 모듈에 후킹 할 방법이 필요합니다. (DT_INIT_ARRAY 내의 함수 호출이 끝나야 dlopen 함수가 종료되고, Frida 쪽 JS API의 Module API를 이용해서 후킹을 진행하는 경우 dlopen 함수가 종료되어야 후킹이 가능한 것으로 알고 있습니다.)

https://github.com/iddoeldor/frida-snippets?tab=readme-ov-file#early-hook
레포 내의 스니펫을 이용하면, DT_INIT_ARRAY 내의 함수가 실행되기 전에 특정 모듈의 후킹을 진행할 수 있습니다. 이 스니펫을 기반으로, 위에서 언급한 63ab8, 664e4, 66f44 함수 등을 후킹하여 libil2cpp.so의 함수를 후킹하는게 맞는지 등을 확인할 수 있습니다.

위 스니펫을 기반으로, 적절한 함수를 후킹하여 libil2cpp 내의 어떤 함수가 후킹의 대상이 되는지 등을 확인할 수 있습니다.

cnt -> 5
liteapks base -> 0x7c43ac4000
il2 base -> 0x7c35e6d000
rwx_region should be: 7c43b9c000
do_hooks_maybe: fin

sym: 0x7c3877eb74, offset 2911b74
rep: 0x7c43b27a08, offset 63a08
rwx: 0x7c43b9c000, offset d8000, sz : 0x32

후킹의 대상이 되는 sym이 libil2cpp.so 내에서 어떤 동작을 하는지 파악하면, mod apk가 어떤 함수를 후킹하여 판정 변조 / 체력 감소 제거 등의 기능을 하는지 알 수 있습니다.

대상 게임의 libli2cpp.so 파일은 100MB를 넘는 큰 바이너리이고, 원본 심볼 등이 별도로 존재하지 않습니다. 하지만 유니티 엔진 및 IL2CPP 백엔드를 이용하여 빌드된 게임은, 게임 내의 global-metadata.dat 파일을 이용해 컴파일 이전의 함수 이름과 각 함수의 오프셋, 원본 타입 정보(구조체 등)을 복원할 수 있습니다.

https://github.com/Perfare/Il2CppDumper
해당 레포와, mod apk 내의 assets/bin/Data/Managed/Metadata/global-metadata.dat 파일을 이용하면 libil2cpp.so 파일 내에 어떤 함수가 어떤 오프셋에 존재하는지 등을 복원할 수 있습니다. 레포에서 단순히 DummyDll 이외에, 스크립트로 생성된 타입 및 함수 정보 등을 IDA, Ghidra 등의 분석 도구에 자동으로 적용할 수 있는 스크립트 또한 제공하고 있습니다(만... 이 글 작성을 위해 사용 중인 Binary Ninja 4.0에서는 제공된 스크립트가 심볼 이름 등을 올바르게 적용하지 못했습니다. 다만 libil2cpp.so 파일 자체를 분석하는게 아니라, 특정 오프셋의 일부 함수의 동작만 확인하면 되므로 스크립트의 결과물인 script.json에서 오프셋에 맞는 함수를 직접 확인하는 방식으로 추가로 분석을 진행했습니다.).

2911b74 -> 43064180
    {
      "Address": 43064180,
      "Name": "NoteFrontBase$$calcAddDamage",
      "Signature": "int32_t NoteFrontBase__calcAddDamage (NoteFrontBase_o* __this, int32_t result, int32_t* damageGuardType, const MethodInfo* method);",
      "TypeSignature": "iiiii"
    },

위의 첫 번째 훅을 보면, NoteFrontBase$$calcAddDamage 함수를 후킹하는 것을 확인할 수 있습니다.

해당 함수 대신 호출되는 0x63a08의 함수를 보면, 플로팅 메뉴에서 설정한 값에 따라 원본 코드를 호출하거나, 대미지 계산의 결과를 0으로 하는 동작을 하는 것을 확인할 수 있습니다. (hook_to_god_mode_or_nop_3 등에는 위의 스크립트 결과에서 rwx로 표기되어 있는 오프셋으로 점프하도록 하는 코드가 저장된 상태입니다.)

대미지를 계산하는 함수가 해당 함수 하나만있지 않으므로, mod apk는 다른 대미지를 계산하는 함수들도 유사한 동작을 하는 코드로 후킹을 진행합니다.

2911d00 -> 43064576
    {
      "Address": 43064576,
      "Name": "NoteFrontBase$$getNoteResultType",
      "Signature": "int32_t NoteFrontBase__getNoteResultType (NoteFrontBase_o* __this, int32_t resultType, GamePlayButton_o* targetButton, const MethodInfo* method);",
      "TypeSignature": "iiiii"
    },

판정을 강제로 퍼펙트로 하거나, 기타 설정에 맞는 판정으로 변조하는 동작은 getNoteResultType이나, 유사한 판정 관련 함수 등을 후킹하여, 판정 결과를 설정에 맞는 임의의 enum을 반환하도록 하는 식으로 구현되어 있습니다.

판정 변조 동작 관련

분석 대상 mod apk는 '노트를 자동으로 플레이하는' (= 즉 사용자가 터치를 하지 않아도 알아서 모든 노트를 플레이하는) 기능은 가지고 있지 않습니다.

대상 게임은 일반 노트 이외에 롱노트 (노트의 시작에 맞춰 터치를 유지하다가, 끝에 맞춰 손을 떼어야 하는 노트), 플릭 (터치 시에 좌우 등으로 노트를 밀어야 올바르게 노트를 플레이한 것으로 판정되는 노트) 등이 존재합니다.

현재 7.x 버전의 APK가 아니라 3-4년 전의 mod apk를 분석했을 때 확인한 내용이지만, 단순히 onMiss 등 노트를 터치하지 않았을 경우에 호출되는 함수를 후킹해서 임의의 판정 결과를 반환하는 함수를 호출하는 방식으로는 롱노트 등의 노트 개수가 올바르게 처리되지 않는 등의 문제점이 있었습니다.

이러한 문제 없이 노트를 자동으로 플레이하게 하려면, 단순히 함수 1-2개만 후킹해서 될 게 아니라, 각 노트 타입 등에 맞춰서 필요한 동작을 적절히 진행하도록 꽤 많은 작업이 필요하고, 이러한 작업에 시간이 생각보다 많이 걸리거나 해서 지금과 같이 판정만 변조하는 방식으로 mod apk를 제작하지 않았을까 추측합니다.

마치면서

대상으로 삼은 mod apk는, 해당 사이트 이외에도 gamedva[.]com 등의 사이트에서도 배포되고 있었습니다. ELF 이름만 libgamedva.so 등으로 다르고, assets 내에 원본 APK를 보관하는 방식 등으로 무결성 탐지를 우회하려는 시도, 플로팅 메뉴 이외에는 별도로 dex 내에 수정 사항이 없는 점 등을 보면, 동일한 제작자가 여러 이름의 사이트를 이용하여 mod apk를 배포하거나 하는 것으로 보입니다. (또는 한 쪽에서 제작한 mod apk를 다른 사이트에서 임의로 수정하여 배포하는 것일 수도 있기는 합니다.)

원래는 이러한 실질적으로 동일한 두 APK 내의 ELF 바이너리를 BinDiff(https://github.com/google/bindiff)를 이용했을 때 어떤 결과를 얻을 수 있는지 또한 보려고 했으나, 현재 배포되고 있는 BinExport 버전은 제가 사용 중인 Binary Ninja (4.0, macOS arm64용)에서 정상적으로 동작하지 않아 진행해보지 못한 점이 아쉽습니다. (BinExport 레포에 최신 바이너리 닌자 API를 타게팅하는 PR이 있긴 합니다만, 빈자 라이센스를 연장을 안 한 상태라... 최신 버전으로 업데이트가 불가능하여 나중에 라이센스를 새로 구매한 이후에 진행해보기로 했습니다.)

사이트 이름을 직접 거론하지는 않겠습니다만, mod apk를 유료로 판매하는 사이트도 있습니다. (사이트에 따라 아예 계정에 맞는 유료 라이센스를 발급하고, 해당 라이센스를 기기 내에 포함해야 mod가 동작하도록 하는 mod apk를 배포하는 곳도 있습니다.)

이러한 유료 mod apk도 가능하면 분석해보려고 했습니다만, 예전과 다르게 이런 유료 mod apk를 구매하여, 추가로 라이센스 체크를 제거 후 재배포하는 mod apk의 mod apk를 배포하는 사이트를 별도로 찾을 수 없어 나중에 기회가 되면 다시 분석해보려 합니다. (나쁜 일 하는 사람들 주머니에 굳이 돈을 채워주고 싶지는 않습니다...)

profile
모르는게 너무 많네요 8ㅅ8

0개의 댓글