Reversing/Game Hack

Unity 구조 분석 : 2. 메모리에 저장된 값 찾기

빌드 완료된 게임 클라이언트를 실행해서 분석 할 것이다.

초기값을 모르니 Unknown 으로 세팅하고 first scan 후, 적을 쏴서 체력을 감소시킨 뒤 감소한 값을 찾는다.

근데 이게 4바이트 값인지 8바이트 값인지 알수가 없어서.. 둘다 해보자.

 

Decreased 로 세팅한 뒤 next scan 진행

이후 가만히 냅둔 상태에서 변하지 않은 값을 찾는다.

Unchanged 로 세팅한 뒤 Next scan 진행, 과정을 반복한다.

음.. 쉽게는 안나온다. 아까 설정했던 정확한 수치인 1000 을 두고 찾아봐도 메모리에 실제 값이 1000이 저장되는게 아닌모양이다. 다른값이 저장되어 사용되는걸로 보인다.

6개 정도가 남았는데, 로봇의 체력이 감소할 떄 마다 저 값들이 한꺼번에 같이 변하는게 확인된다.

즉, 로봇 체력값이 하나만 적용되는게 아니고 여러 값들이 동시에 적용된다고 생각하면 될듯.

맨 아래 값(FDB28)을 바꾸니 로봇 체력이 바뀌는걸 확인했다. 일정 배수 형식으로 적용되는지를 확인해야할듯.

정확하게는 메모리에 존재하는 값이 로봇 체력에 영향을 미치는걸로 확인된다.

일단 해당 값을 찍어놓고 죽었다가 다시 시작해보면 447A 가 찍히는걸로 봐선 최대값은 0x447A(17530)인듯 한데.. 쏘다보면 그 아랫자리 값도 변화가 생기더라. 그럼 447A0000으로 생각해야하나?

싶었는데 맨 아래 한바이트는 값에 관여를 안하는걸로 보인다.

내가 설정했던 값은 1000인데 값이 저렇게 바뀌어있다. 어셈 루틴을 통해 정확히 어떻게 바뀌었는지 파악 해보자.

워치를 걸어놓고 두번 쏘면 위와 같은 명령어들이 값에 관여한다고 나온다.

rdi+0x38 에서 가리키는 메모리 위치가 체력값이 있는 메모리 위치였고, movss 명령어는 4바이트 float 부동소수점 값 하나를 복사하는 명령어다. xmm0, 1, 5 레지스터의 값을 복사 해 주는것.

어라, 그러면 위에서 값이 저렇게 보인건 int가 아니라 float 형태라서 메모리상에 저렇게 보인건가?

http://www.binaryconvert.com/convert_float.html

맞다. 1000이라는 정수값은 4바이트 float 형태로 메모리에 저장될 때 0x447A 로 표현된다.

따라서, 해당 위치에는 내가 생각한 1000이라는 값이 변환 없이 그대로 저장되는 것이 맞다.

이 부분을 좀 자세히 알아보기 위해 turret prefab에 연결된 component 중 체력에 관련된 사항을 담당하는 Health 소스코드를 열어보았다. (health.cs)

//health.cs 
~~
public class Health : MonoBehaviour
{
    [Tooltip("Maximum amount of health")]
    public float maxHealth = 10f;
    [Tooltip("Health ratio at which the critical health vignette starts appearing")]
    public float criticalHealthRatio = 0.3f;

    public UnityAction<float, GameObject> onDamaged;
    public UnityAction<float> onHealed;
    public UnityAction onDie;

    public float currentHealth { get; set; }
    public bool invincible { get; set; }
    public bool canPickup() => currentHealth < maxHealth;

    public float getRatio() => currentHealth / maxHealth;
    public bool isCritical() => getRatio() <= criticalHealthRatio;

    bool m_IsDead;

    private void Start()
    {
        currentHealth = maxHealth;
    }
~~

기본적으로 유니티 엔진에서는 저러한 변수값(max health, critical health ratio)을 엔진상에서 할당이 가능한데, 코드상에서는 위와 같이 보여진다.

maxHealth 변수가 public float 형으로 선언된것이 보이고, 그 아래애 currentHealth 변수도 public float 으로 선언되어 Start() 함수 실행시에 maxHealth의 값을 받아오는게 확인된다.

값이 메모리에 어떤식으로 올라갔는지를 알았으니, 이제 찾아야할건

  1. 메모리 상에서 해당 오브젝트의 정보가 담긴 구조체
  2. 오브젝트들의 정보를 담고 있는 테이블
  3. 값이 저장될 포인터 연산

의 세가지.

오브젝트가 데미지를 입었을 때 이를 처리하는 함수가 호출되는 루틴이 있다.

여기서 RCX 레지스터를 인자로 해당 오브젝트의 기준 주소값이 들어가는게 확인된다. 적 터렛이나 플레이어 캐릭터가 피해를 입을때를 구분하지 않고 동일하게 호출되는 함수다.

터렛이 데미지를 입은 경우 RCX에 0x2093E4FDAF0 가 들어갔고, 플레이어가 데미지를 입은 경우 0x2093E4FD8C0가 들어갔다.

그럼 0x2093E4FD8C0 + 0x38위치에는 플레이어의 체력 정보가 들어있을까?

0x2093E4FD8F8를 지정하여 값을 바꿔보니, 플레이어의 체력바가 움직이는걸 확인할 수 있었다.

즉, 오브젝트 구조체 베이스주소 + 0x38 위치에는 해당 오브젝트의 체력 정보가 담기는 공통적인 구조를 가진다는걸 알아냈다.

이제 콜스택을 역으로 따라 올라가 오브젝트 베이스 주소가 전달될 RCX값을 어디서 가지고 오는지를 추적하기 위해 포인터 스캔을 진행한다.

해당 값에 대해 pointer scan 진행 후 게임을 재시작, pointer scanner에서 rescan memory → only filter out invalid pointer 옵션을 걸고 진행한다.

이때 마지막으로 rdi+0x38 을 통해 포인터 참조가 이뤄졌으니, must end with offsets 옵션에서 0x38 을 걸고 필터링을 해주면 적당한 포인터들이 필터링 된다.

Unityplayer.dll + 0x017A7B20 에서 출발하는 포인터들이 똑같은 값으로 도달하는게 확인되는데, 게임을 껏다 켰다 하면서 필터링을 여러번 반복해서 어떤 포인터가 올바른 포인터로 남는지 확인하는 과정을 거쳐야한다.

이 과정을 간략화하기 위해 pointermap을 사용해볼거다. 값을 가리키는 메모리 주소(0x13FC0FA75D8)를 찾고, pointermap을 생성해준다.

이후 게임을 종료 - 재시작 한 다음, 동일한 값을 가리키는 메모리(0x1862E8A75D8)에 대해 다시 포인터맵을 생성해주자.

이후 pointermap 2에 대해 포인터 스캔을 진행하되, 만들어놨던 pointermap 2를 사용하고, compare 옵션에서 pointermap1과 비교하는 필터를 추가해준다.

saved pointermap은 방금 새로 켠 게임에서 생성한 2번이고, compare 는 아까 만든 1번이다.

compare에서 지정해줄 address는 아까 pointermap1 을 만들면서 추가했던 주소값을 지정해준다.

이때 끝나는 오프셋이 0x38인걸 알고 있으니 end offset 옵션도 추가한다.

그러면 갯수가 조금 더 적게 나오고, 해당 포인터 테이블에서 값을 기준으로 재검색을 하면 후보가 더 줄어든다.

포인터 찾는 순서를 정형화해 보면,

  1. 피격 이벤트 발생시켜 RCX-베이스 찾기
  2. 찾은 주소에 대해 포인터 스캔 진행
  3. 게임을 재시작하고, 스캔한 포인터들에 대해 원래 값을 가지는 포인터만 스캔하여 포인터 찾기

위 세가지 과정을 여러 오브젝트에 대해 반복 진행하여 포인터를 찾아보자.

먼저 오브젝트별 포인터를 다 준비한 다음, 게임을 재시작하여 아직 유지되는 유효한 포인터 중 각 오브젝트별 초기값을 가지고 있는 포인터를 필터링해준다.

결과를 많이 추려내긴 했는데,

아까 찾아낸 베이스 주소 기반으로 구조체를 만들어 보니, 좌표에 대한 정보는 보이지 않았고 이러한 체력 정보와 체력 하한선 정보(0.3) 만 보였다.

즉, 오브젝트 베이스주소를 담고있는 테이블이 없을 가능성이 있다는 것.

유니티 엔진에서 오브젝트에는 컴포넌트가 붙어있고, 각 컴포넌트별로 변수와 함수가 선언되니, 내가 찾은 베이스 주소가 오브젝트 자체의 베이스 주소가 아니라 오브젝트에 연결된 health 컴포넌트의 베이스 주소일 가능성이 높다.

즉, 오브젝트 → ? → health 컴포넌트체력 변수 순으로 포인터가 정렬된단 소리.

그럼 각 오브젝트별로 health 컴포넌트까지는 오프셋이 동일할 수 있다. 만약 이런 구조를 가지고 있다면 지금같은 방식으로는 찾는게 매우 힘들어보인다.

적들별로 health 컴포넌트에 붙어있는 체력정보 정도는 가져올 수 있을지 모르지만, 그 외의 정보는 역참조가 힘든 구조이기 때문.

일단 여기까지만 해보자. 더 이상 파고드는건 힘들듯 하니, 다른 방법을 찾아봐야겠다.