컴퓨터에서 메모리에 데이터를 저장하는 방식을 의미하는 바이트 오더링(Byte Ordering)의 리틀 엔디언 표기법과 빅 엔디언 표기법을 공부합니다.
바이트 오더링은 데이터를 저장하는 방식을 말합니다. 애플리케이션의 디버깅을 할 때 알아두어야 하는 기본 개념 중 하나로 바이트 오더링 방식에는 크게 두 가지가 있습니다.
빅 엔디언(Big Endian)
과 리틀 엔디언(Little Endian)
방식입니다.
총 4개의 크기가 다른 자료형이 있습니다. 각 엔디언 방식에 따라 같은 데이터를 각각 어떤 식으로 저장하는지 비교합니다.
TYPE | Name | SIZE | 빅 엔디언 Style | 리틀 엔디언 Style |
---|---|---|---|---|
BYTE | b | 1 | [12] | [12] |
WORD | w | 2 | [12][34] | [34][12] |
DWORD | dw | 4 | [12][34][56][78] | [78][56][34][12] |
char[] | str | 6 | [61][62][63][64][65][00] | [61][62][63][64][65][00] |
ASCII 문자 'a'는 0x61과 같고, 'e'는 0x65와 같습니다. 문자열 마지막은 NULL로 끝납니다.
바이트(BYTE) 타입의 b 변수를 저장할 때는 두 방식의 차이가 없습니다. 2바이트 이상의 크기를 가진 자료형을 저장할 때부터 차이가 나타납니다.
빅 엔디언 방식은 데이터를 저장할 때 사람이 보는 방식과 동일하게 앞에서부터 순차적으로 저장합니다.
그러나 리틀 엔디언 방식은 데이터를 저장할 때 역순으로 저장합니다. 즉 저장되는 바이트의 순서가 뒤집어져 있습니다.
리틀 엔디언이라고 할지라도 바이트 자체는 정상적인 순서로 저장이 됩니다. 2바이트 혹은 4바이트 자료형과 같이 멀티바이트(muti-bytes)인 경우 각 바이트가 역순으로 저장되는 것입니다.
str 문자열은 엔디언 형식에 상관없이 동일합니다. 문자열이란 결국 캐릭터(char) 배열이기 때문에 각 바이트를 하나씩 연속해서 저장한다고 생각해보면 리틀 엔디언에서도 문자열 자체는 빅 엔디언과 동일한 순서로 저장됩니다.
데이터를 순서대로 저장시키는 빅 엔디언의 장점은 사람이 보기에 직관적이라는 것입니다. 빅 엔디언은 대형 UNIX 서버에 사용되는 RISC 계열의 CPU에서 많이 사용됩니다. 네트워크 프로토콜에도 빅 엔디언이 사용됩니다.
이런 점들은 x86 계열의 응용 프로그램 개발자와 리버서에게 중요한 의미를 가지고 있습니다. 애플리케이션 개발에 사용된 데이터를 네트워크로 송수신할 때 엔디언 타입을 변경해야 하기 때문입니다.
Intel x86 CPU에서는 리틀 엔디언 방식을 사용합니다. 그렇기 때문에 Windows 계열 리버서들은 리틀 엔디언에 대해 잘 알아야 합니다.
데이터를 역순으로 저장시키는 리틀 엔디언 방식도 산술 연산과 데이터의 타입이 확장/축소될 때 더 효율적이라는 장점을 가지고 있습니다.
#include "windows.h"
BYTE b = 0x12;
WORD w = 0x1234;
DWORD dw = 0x12345678;
char str[] = "abcde";
int main() {
byte lb = b;
WORD lw = w;
DWORD ldw = dw;
char *lstr = str;
return 0;
}
위 코드를 Ollydbg로 디버깅합니다. Go to 명령으로 401000 주소로 갑니다.
main() 함수의 주소는 401000입니다.
전역 변수들의 주소는 40AC40(b), 40AC44(w), 40AC48(dw), 40AC4C(str) 입니다. 이 메모리 영역을 데이터 창에서 Go to 명령을 통해 40AC40으로 갑니다.
전역 변수 메모리 영역을 확인해보니 변수 w와 dw 값들이 리틀 엔디언 형식으로 저장된 것을 볼 수 있습니다.
ⓒ 리버싱 핵심 원리