본 게시글은 메모리에 대한 깊은 이해를 위해 에이콘 출판사의 "봇을 이용한 게임 해킹" 서적을 보고 필기한 자료입니다.
따라서 디테일한 부분에서 본 서적의 실제 내용과 다를 수 있고 글쓴이의 주관이 들어가 있음을 참고해주시기 바랍니다.
문제 시 비공개 처리하도록 하겠습니다.
근성 있는 해커라면 지속적인 해킹 방어에 대응하기 위해 혹은 메모리의 복잡한 데이터 구조를 파악하고 특정 데이터 위치를 찾아내기 위해 최고 수준의 메모리 포렌식 기법을 배워야 한다
목적 추론하기
- 캐릭터의 체력값을 찾는 방법
struct PlayerVital {
int current, maximum;
};
PlayerVital health;
printString("Health: %d of %d\n", health.current, health.maximum)
- printString() 함수가 인게임 인터페이스에서 텍스트를 출력하는 함수라면 우리가 찾는 함수와 가까운 것
- PlayerVital 구조는 current와 maximum이라는 두 속성을 갖고 있다.
- health 값은 PlayerVital 구조에 속하기 때문에 이 값도 두 가지 속성을 가질 것임
- 이를 통해 health는 플레이어의 체력정보를 표시한다고 유추할 수 있다.
mov eax, dword ptr [406050]
push eax
mov ecx, dword ptr [40604C]
push ecx
push chapter5_advancedmemoryforensics_scanning.405120 405120:"Health: %d of %d\n"
call chapter5_advancedmemoryforensics_scanning.4010C0
- 실제 x64dbg상에서 찾은 예제 파일의 printString 함수 호출부분 실습
- 인자가 무엇이고 그것이 역순으로 대입된다는 사실을 알고 있으면 원래 코드가 뭔지 유추할 수 있다.
int currentHealth;
int maxHealth;
--생략--
someFunction("Health: %d of %d\n", currentHealth, maxHealth)
- EAX와 ECX에 있는 값도 인접해있음을 통해 이들이 구조의 일부임을 알 수 있다.
- 게임 로그를 좀 더 익숙하게 다룰 수 있다면 원하는 값을 쉽게 찾을 수 있음
복잡한 게임 데이터 식별하기
- 앞서 4장에서는 정적 구조에 데이터를 저장하는 방법이었음
- 간단한 데이터면 이정도면 되겠지만 동적 구조에 저장된 데이터는 좀 더 깊은 지식을 필요로 함
- 동적 구조는 각기 다른 메모리 구조에 흩어져서 구성되며, 긴 포인터 체인을 가짐
std::string 클래스
- 게임 내 등장하는 몬스터 이름 같은 스트링 데이터는 다 string을 사용함
class string {
union {
char* dataP;
char dataA[16];
};
int length;
};
string* _str = (string*)stringAddress;
- 이 클래스는 스트링을 저장하기 위해 16개 문자 사용
- 처음 4바이트는 문자의 포인터로 사용함
- 이런 구조는 최적화된 결과.
- 이보다 긴 스트링은 처음 4바이트가 스트링 문자열의 포인터로 사용됨
멤버함수 c_str()의 내부 구현 추론 코드
const char* c_str() {
if (_str->length <= 15)
return (const char*)&_str->dataA[0];
else
return (const char*)_str->dataP;
- std::string이 짧은 스트링도 다룰 수 있고, 긴 스트링의 포인터로 활용이 가능하다는 점은 흥미로운 부분
std::string으로 실패하는 경우
struct creatureInfo{
int uniqueID;
char name[16];
int namelength;
int healthPercent;
int xPosition;
int yPosition;
int modelID;
int cretureType;
}
- 메모리에서 크리처 데이터를 스캔한 다음 크리처의 고유한 앞 4바이트를 주의해서 봐야 함
- 이 바이트는 uniqueID라고 부르며, 하나의 int 속성이라고 볼 수 있음
- 그 뒤에 name이 있고, nameLength가 위치하고 있다.
- 그런데 이런 경우 char 배열이 아니라 string일 확률이 높다는 것.
이걸 판단하기 위해서는?
- 왜 널 종료 문자가 스트링에 포함되는지? 적합한 이유가 없다면 std::string일 수 있음
- 일부 크리처 이름을 16자 이상으로 설정했는데 16개만 있다면? 그건 std::string에 저장된 것.
std::vector 클래스
- 게임 내부엔 다양한 동적 배열이 존재함
- 게임 개발자들은 std::vector를 주로 사용한다.
template<typename T>
class vector
{
T* begin;
T* end;
T* reservationEnd;
};
//추상화된 vector
std::vector<DWORD> _vec;
- 메모리에서 DWORD 유형의 std::vector가 어떻게 나타날까?
- _vec의 주소를 이미 알고 있고 동일한 메모리 공간을 공유하고 있다면 다음과 같은 클래스로 접근이 가능할 것임
class vector
{
DWORD* begin;
DWORD* end;
DWORD* tail;
};
vector* _vec = (vector*)vectorAddress;
- 길이는 알 수 없으므로 begin과 end를 가지고 계산해야 함 (원래 vector에는 size와 capacity도 있는데 왜지?)
- end는 마지막 멤버 다음에 위치한다는 것도 유념해야 할 내용
int length()
{
return ((DWORD)_vec->end - (DWORD)_vec->begin / sizeof(DWORD));
}
- end 주소에서 begin 주소를 빼고 타입의 크기로 나눔으로써 요소의 개수가 몇 개인지 알 수 있음
std::vector에 저장할 수 있는 데이터
- 정적 주소를 가진 배열은 std::vector에 저장될 수 없다.
- std::vector의 객체는 근본적으로 배열에 접근하기 위한 포인터 경로가 필요함
std::list 클래스
- vector와 유사하게 링크드리스트 안에 아이템을 저장함
- 링크드리스트 특성 상 요소들이 인접해있지 않아서 인덱스로 접근할 수 없음
- 아이템에 접근할 때 오버헤드가 발생해서 게임에 많이 쓰이지는 않음
template<typename T>
class listItem {
listItem<T>* next;
listItem<T>* prev;
T value;
};
template<typename T>
class list {
listItem<T>* root;
int size;
};
//추상화된 list
- 그러면 이런 식으로 가리킬 수 있음
class listItem {
listItem* next;
listItem* prev;
DWORD value;
};
class list {
listItem* root;
int size;
};
list* _lst = (list*)0x00002222;
- list 클래스는 리스트의 헤더를 표시하고 listItem은 저장된 값임(노드)
- 각 아이템은 다음 아이템, 이전 아이템을 가리키는 포인터를 포함하고 있음
listItem* it = _lst->root->next;
for (; it != _lst->root; it = it->next)
{
printf("Value is %d\n", it->value);
}
- 따라서 이런 식으로 순회할 수 있음
- list 안의 객체 메모리가 순차적으로 있지 않아서 빠르게 크기를 계산할 수 없음
- 하지만 list의 멤버로 size 변수를 제공하고 여기서 개수를 파악할 수 있음
게임 데이터가 list에 저장되었는지 살펴보려면?
- list 안에 위치한 객체들은 정적 주소를 가질 수 없다.
- 아이템들이 메모리에 연속적으로 배치되어 있다면 list가 아님
- list안의 객체들은 아주 긴 포인터 체인을 가지고 있다.
std::map 클래스
- map도 list처럼 요소 간 링크를 형성함
- map의 요소들은 키와 값의 두 가지 데이터 유형으로 저장되어 요소를 분류
- 내부적으로 레드블랙트리를 사용함
template<typename keyT, typename valT>
struct mapItem
{
mapItem<keyT, valT>* left;
mapItem<keyT, valT>* parent;
mapItem<keyT, valT>* right;
keyT key;
valT val;
};
template<typename keyT, typename valT>
struct map {
DWORD irrelevant;
mapItem<keyT, valT>* rootNode;
int size;
};
// 추상화된 맵
- 레드블랙트리는 자체적으로 균형 이진 트리임
- 트리 구조 기반으로 정렬된 노드는 서로의 키와 비교됨
- 노드의 left 포인터는 그보다 작은 값을 가진 노드를 가리키고 right는 그 반대
- parent는 상위 노드를 가리킴
- 트리의 첫 번째 노드는 rootNode라고 부름
- 루트 노드의 left는 가장 작은 값을 가진 노드를 가리킬 것이고, right는 가장 큰 값을 가진 노드를 가리킴
typedef int keyInt;
typedef int valInt;
struct mapItem
{
mapItem* left;
mapItem* parent;
mapItem* right;
keyInt key;
valInt val;
};
struct map {
DWORD irrelevant;
mapItem* rootNode;
int size;
};
- std::map의 데이터에 액세스하기 위해 임의로 만든 사용자 정의 map
- 게임 안에서 map의 구조 안의 데이터에 접근하려면 몇 가지 중요한 알고리즘을 알고 있어야 한다.
- 전체 맵을 도는 함수를 이렇게 만들 수 있다.
void iterateMap(mapItem* node)
{
if (node == _map->rootNode) return;
iterateMap(node->left);
printNode(node);
iterateMap(node->right);
}
- 이 함수를 호출하기 위해선 다음처럼 root 노드를 지나가야 함
iterateMap(_map->rootNode->parent);
- 하지만 이렇게 하는 것보다 내부 검색 알고리즘을 모방하는 것이 더 효율적임
mapItem* findItem(keyInt key, mapItem* node)
{
if (node != _map->rootNode)
{
if (key == node->key)
return node;
else if (key < node->key)
return findItem(key, node->left);
else
return findItem(key, node->right);
}
else return NULL;
}
- 트리는 꼭대기부터 시작해서 현재 키와 검색하는 키랑 비교, 크다면 왼쪽, 작다면 오른쪽으로 회귀하는 구조이기 때문
mapItem* ret = findItem(someKey, _map->rootNode->parent);
if (ret)
prinitNode(ret);
데이터가 std::map에 저장되었는지 살펴보기
- 찾는 데이터가 배열, 벡터, 리스트가 아니라면 list와 마찬가지로 값 앞의 정수값 3개를 살펴보고
- 다른 맵 노드를 가리키는지 확인해야 함
'게임 보안 > [서적] 봇을 이용한 게임 해킹' 카테고리의 다른 글
6. 코드 인젝션 (1) | 2022.10.01 |
---|---|
5. 게임 메모리 읽고 쓰기 (1) | 2022.09.29 |
3. x86 어셈블리 크래시 코스 (1) | 2022.09.28 |
2. 코드에서 메모리로 - 기본 원리 (2) | 2022.09.24 |
1. 해킹 도구 (0) | 2022.09.18 |
댓글