aobscan() 명령에서 메모리 relocation이 일어나는 코드를 제외하고 '48 89 47 60 83 EC 0C 57 E8'를 검색하도록 한다. 원래는 89부터 시작이었으나 1바이트 전의 48부터 시작했으므로 스크립트에는 INJECT+1을 사용했다.
이렇게 하는게 싫다면 와일드카드 캐릭터를 적용해서 검색한다. (x, ?, *)
주소를 하나 추가한다.
지금까지 잘 진행되었다면 위와 같이 세팅된다.
스크립트를 활성화 하고 HP에 변화가 생기면 그때부터 HP를 추적할 수 있다.
처음에 찾은 옵코드가 HP에 변화가 생기는 순간 실행되는 명령이기 때문에 항상 HP를 감시하지는 않는다. 하지만 게임을 종료하고 다시 실행해 메모리 주소에 변화가 생겨도 언제나 올바른 HP 값을 표시하고 변경할 수 있다. 위 그림에서도 확인할 수 있듯이 처음에 찾은 HP 메모리 주소(149CDB50)는 게임이 다시 시작되어 무효한 값이 되었지만 [HP] 심볼로 추가한 메모리 주소는 스크립트 덕분에 언제나 올바른 HP 메모리(149CDC18)를 가리킨다.
치트 엔진으로 게임에서 수정하고 싶은 값의 주소를 찾아도 게임을 다시 실행시키면 그 주소가 변경되어 처음부터 다시 진행해야만 하는 경우가 있다.(포인터 등으로 해결 불가) 예를 들어 도스박스로 고전 게임을 실행시키는 경우 값의 주소가 변경될 뿐만 아니라 그 값을 기록하는 명령이 다른 여러 주소에 접근하기 때문에 쉽게 에디터를 만들 수가 없다.
Tutorial-i386.exe+25B1E: 8D 85 D4 FE FF FF - lea eax,[ebp-0000012C]
}
{}와 //의 내용은 주석이므로 설명하지 않는다.
1
2
3
4
5
//// -------- Main Section --------
[ENABLE]
//// -------- Enable Section --------
[DISABLE]
//// -------- Disable Section --------
전체 내용은 위와 같이 [ENABLE]과 [DISABLE]을 기준으로 Main, Enable, Disable 섹션으로 나눌 수 있다. Main 섹션은 항상 실행된다. Enable 섹션은 스크립트를 실행하면(Cheat Table Address List에서 Active 체크 박스를 클릭해서 선택하면) 실행된다. Disable 섹션은 스크립트를 정지하면(Active 체크 박스 선택 해제하면) 실행된다.
aobScanModule(SymbolName, ModuleName, AOBString) - Scans the memory used by the module ModuleName for a specific byte pattern defined by AOBString and sets the resulting address to the symbol SymbolName.
지정된 모듈(Tutorial-i386.exe)에서 특정 바이트 패턴(29 83 B0 04 00 00)이 있는 메모리 영역을 찾는다. 만약 찾으면 그 주소를 심볼(INJECT)로 세팅한다. 3개의 캐릭터를 와일드카드로 사용할 수 있다. 'x', '?', '*'
alloc(SymbolName, Size, AllocateNearThisAddress OPTIONAL) - Allocates a memory block of Size bytes and defines the SymbolName in the script, pointing to the beginning of the allocated memory block.
지정한 사이즈(1000)의 메모리 블럭을 할당하고 시작 주소를 심볼(newmem)로 정의한다. ($1000은 16진수 1000을 의미한다)
dealloc(newmem)
dealloc(SymbolName) - Deallocates a block of memory allocated with alloc.
alloc()으로 할당된 메모리 블럭(newmem)을 해제한다.
registersymbol(INJECT)
registerSymbol(SymbolName) - Adds a symbol to the user-defined symbol list so cheat tables and the memory browser can use that name instead of an address.
심볼(INJECT)을 사용자 정의 심볼 리스트에 추가한다. 추가된 심볼은 치트 테이블과 메모리 브라우저에서 주소 대신 사용할 수 있다.
unregistersymbol(INJECT)
unregisterSymbol(SymbolName) - Removes a symbol from the user-defined symbol list.
사용자 정의 심볼 리스트에서 심볼(INJECT)을 제거한다.
label(code)
label(LabelName) - Enables the word 'LabelName' to be used as a symbol.
지정된 단어(code)가 심볼로 사용될 수 있게한다.
※ 참고 1
aobScan(SymbolName, AOBString) - Scans all the memory for a specific byte pattern defined by AOBString and sets the resulting address to the symbol SymbolName.
메모리에서 특정 바이트 패턴(AOBString)을 찾고 그 주소를 심볼(SymbolName)로 지정한다. 3개의 캐릭터를 와일드카드로 사용할 수 있다. 'x', '?', '*'
assert(Address, ArrayOfBytes) - Will check the memory address for the given bytes. If the address's memory is not what is defined by the array of bytes given, the auto assemble script will not execute.
지정된 Address에 ArrayOfBytes가 있는지 확인한다. 만약 없다면 auto assemle script는 작동하지 않는다.
ex) assert(Game.exe+123ABC, 01 02 03 0A 0B 0C)
globalAlloc(Symbol, Size, AllocateNearThisAddress OPTIONAL) - Allocates a certain amount of memory and registers the specified name. Using GlobalAlloc in other scripts will then not allocate the memory again, but reuse the already existing memory. (Or allocate it anyhow if found it wasn't allocated yet) If 'AllocateNearThisAddress' is specified CE will try to allocate the memory near that address. This is useful for 64-bit targets where the jump distance could be bigger than 2GB otherwise. 지정한 사이즈의 메모리를 할당하고 할당된 메모리 주소를 이름(Symbol)으로 등록한다. 등록된 메모리 이름은 다른 스크립트나 메모리 뷰어에서 사용할 수 있다.
define(Name, Value) - Creates a token with the specified name that will be replaced with the text of its value. Note: Uses basic replacement before script is ran, whitespace is not stripped.
스크립트가 실행되기 전 교체될 텍스트 토큰을 생성한다.
ex 1)
아래 코드가.. define(address, 00 12 3A BC) ... address: db 90 90 90
이렇게 바뀐다. 00 12 3A BC: db 90 90 90
ex 2)
아래 코드가.. define(fullValue,(float)100.0) ... mov eax,fullValue
번개 숫자가 변하면 번개가 저장된 메모리를 찾을 수 있다. (Value Type은 2Bytes로 설정해야 한다)
번개 메모리 주소에서 Find out what writes to this address를 실행한다.
다시 번개를 한 번 쏘면 어떤 코드가 번개 메모리 주소를 사용하는지 찾을 수 있다. Show disassembler 버튼을 클릭한다.
Memory Viewer에 어셈블리 코드가 표시된다.
Tools - Auto Assemble을 클릭한다.
Auto Assemble 창이 뜨면 Template - AOB Injection을 클릭한다.
번개 메모리에 6(번개 최대치)을 저장하는 코드를 newmem영역에 작성한다.
File - Assign to current cheat table을 클릭한다.
Cheat Table Address List에 스크립트가 추가되었다. Active 체크박스를 클릭해 활성화 한다.
게임에 오류가 발생한다. 왜 그럴까?
다시 Memory Viewer를 확인해 보자. 새로운 코드가 생성된 곳으로 점프하는 명령어가 잘 들어가 있다.
새로운 코드도 잘 들어가 있다. 이유를 알 수가 없다.
처음부터 다시 해 보자. 게임을 시작하고 번개가 저장된 메모리 주소를 찾는다.
어떤 코드가 번개가 저장된 메모리 주소를 사용하는지 찾은 후 이번엔 Find out what addresses this code accesses를 실행한다. 이 코드가 어떤 메모리 주소에 접근하는지 찾아준다. 예상대로라면 번개 메모리 주소만을 접근해야 한다.
게임에 오류가 발생한 이유를 찾았다. 우리가 찾은 코드는 번개 저장 메모리뿐만 아니라 다른 메모리에도 엄청나게 많은 접근을 하고 있었던 것이다. 이 모든 메모리에 6을 덮어 썼으니 오류가 나지 않을 수 없었다. 어떻게 해야 번개 메모리에만 6을 저장할 수 있을까?
다시 Memory Viewer로 돌아와서 메모리 덤프창에서 우클릭 - Goto address를 클릭한다.
번개가 저장된 메모리 주소를 입력하고 OK를 클릭한다.
번개가 저장된 메모리(04 00)가 표시된다. (그 옆의 06 00은 폭탄이 저장된 메모리이고 다시 그 옆의 06 00은 생명이 저장된 메모리이다) 주변을 살펴보자. 00, 09, FF등 별 특이한 값은 보이지 않지만 0A590B31~0A590B34에 JOHN이라는 이름이 기록되있다. 우리가 찾은 코드가 접근하는 다른 메모리 근처엔 없을거 같은 특이한 값이다. (JOHN 이외의 다른 이름들은 높은 점수를 기록한 사람들의 이름이 저장된 HALL OF FAME에 나오는 이름들이다. JOHN은.. 모르겠다)
이번엔 JOHN이라는 값을 이용해 번개 저장 메모리 주소를 구분하고 번개와 폭탄을 모두 6으로 덮어쓰는 코드를 작성한다. JOHN은 번개 메모리 주소에서 offset이 0x61이다. 또 번개 메모리 주소 바로 옆, offset 0x02에 폭탄 갯수가 저장되어 있다.
치트 엔진으로 삽입한(inject) 코드를 enable/disable 하기 위해 Cheat Table Framework code 사용할 수 있다. 하지만 자세히 보지 않으면 원치 않는 결과를 얻게 되므로 조심해야 한다.
치트 엔진 튜토리얼을 실행하고 Step 7: Code Injection 까지 진행한다.
Health 메모리를 찾는다.
Health 메모리에 쓰기를 시도하는 코드를 찾는다.
쓰기 코드를 찾았으면 Show disassembler 버튼을 클릭한다.
Health 메모리에 저장된 값을 1 감소(sub) 시키는 코드가 선택된다.
Tools - Auto Assemble 을 클릭한다.
삽입할 코드를 enable/disable 할 수 있도록 Cheat Table framework code를 선택한다.
Code injection을 선택한다.
코드를 삽입하고 복구하는 기본 코드가 작성되었다. 코드를 수정하지 않았으므로 프로그램은 변경되지 않는다.
Assign to current cheat table을 선택한다.
Cheat Table Address List에 스크립트가 추가되었다.
Active 체크박스를 클릭해 enable 시킨다.
스크립트가 삽입된 주소로 점프하는 코드로 변경되었다.
다시 Active 체크박스를 클릭해 disable 시킨다.
변경된 점프 코드가 다시 원래 sub 코드로 복구되었다.
하지만 튜토리얼의 Hit me 버튼을 클릭하면 오류가 발생하고 프로그램이 종료되어 버린다.
왜 그럴까? 원래 코드와 복구된 코드를 다시 살펴보자.
위 Memory Viewer가 원래 코드고 아래 Memory Viewer가 복구된 코드다.
원래 코드나 복구된 코드나 ebx 레지스터에 저장된 메모리 주소에서 4A4 바이트 떨어진 곳에 저장된 값에서 1을 빼는 내용으로 동일하지만 자세히 보면 Opcode가 다른것을 확인할 수 있다. 원래 코드의 Opcode는 83이고 복구된 코드의 Opcode는 81이다. 두 번째 operand도 값은 1로 동일하지만 크기가 1바이트(01)와 4바이트(00000001)로 큰 차이가 발생했다. 이렇게 코드가 바뀌었지만 내용은 같기때문에 별 문제가 없을것 같지만 명령어 크기가 바뀌는 바람에 그 다음 명령어 영역까지 침범한 것은 큰 문제이다. (그래서 프로그램이 오류로 종료되어 버리는 것이다)
스크립트의 복구 내용을 살펴보면 원래 어셈블리어 코드 그대로 사용했음을 확인할 수 있다. 그런데 그 어셈블리어 코드가 원래 옵코드와 오퍼랜드로 복구되지 않는것이 문제이다.
원래 코드의 83은 imm8(8비트 값)을 오퍼랜드로 사용하지만 복구된 코드의 81은 imm32(32비트 값)을 오퍼랜드로 사용한다. 복구할 스크립트의 sub 명령어를 기계어로 번역할때 Opcode를 81로 사용하기 때문에 sub dword ptr [ebx+000004A4], 01 이라는 어셈블리 명령어의 두 번째 오퍼랜드 01을 0x01로 1바이트 값이 아닌 0x00000001로 4바이트 값으로 번역하는 것이다. 치트엔진은 튜토리얼이 32비트 프로그램이므로 당연히 4바이트 값을 사용했을것이라 생각하는것 같다. 어쨌든 원래 코드로 복구시키지 못하므로 버그라고 봐야 할거같다.
해결 방법은 간단하다.
위 그림처럼 sub dword ptr [ebx+000004a4], 01 코드를 주석처리하고 그 아래 db 83 AB A4 04 00 00 01을 주석 해제한다. 어셈블리어로 작성한 코드를 기계어로 번역하지 않고 원래 있던 기계어를 다시 그대로 바이트 단위로 정의(db: define byte)하는 것이다.
아니면 위 그림처럼 dword(4바이트)를 byte(1바이트)로 바꿔도 된다.
r/m8, imm8 오퍼랜드를 사용하는 80 옵코드로 복구되지만 기계어로 번역된 결과의 길이가 같아 다른 명령어를 침범하지 않고 실행에도 문제가 없다.