32비트 CPU 환경에서 물리 메모리는 4GB였다. 근데 이 중에서 절반 2GB는 유저 영역에, 절반 2GB는 커널 영역에 할당하고 보면, 결국 프로세스가 돌아갈 메모리가 너무 적지 않냐는 의견이 생기면서 2GB보다 더 많은 영역에 대한 수요가 생김.
그래서 32비트에서 32비트를 더 붙이는 순간 메모리 영역이 16TB로 압도적으로 증가한다. 유저영역 8TB, 커널 영역 8TB가 된다.
Intel64같은 64비트 CPU는 하위호환을 지원한다. 뭐냐면 64비트 환경에서 만들어진 프로세스가 기존의 32비트 환경에서도 잘 돌아가는 거래.
64비트 체제의 윈도우 api가 따로 있진 않고 Win32 API를 그대로 쓴다.
이런 시그올 기존의 32비트 프로그램이 64비트 환경에서 잘 돌아가게 만들어주는 메커니즘이 바로 WOW64다.
만약 64비트에서 kernel32.dll(32비트)를 로드하면 이 WOW64가 이런 32비트 로드 요청을 kernel32.dll(64비트)로 변환(redirection)해준다.
그래서 시스템 폴더를 찾아보면 이름과 내용이 좀 매칭이 안된다. 컴덕들이란....
System32 폴더엔 64비트 시스템 파일이 존재하고, SysWOW64폴더엔 32비트용 시스템 파일이 존재한다. 이게 맞냐?
32를 64에 적고 64를 32에 적는게?
레지스터도 달라진다.
이미 32비트도 기존의 16비트 환경에서 한번 Expanded(확장)를 거친 녀석이다. 그래서 확장된 베이스 포인터여서 EBP, 확장된 스택 포인터라서 ESP, 확장된 AX여서 EAX라고 한거다.
64비트는 이런 확장을 한 번 더 거쳤다는 의미로 Re-expanded(재확장, 추가확장)이란 접두어를 붙여서 RBP, RSP, RIP, RAX, RDX 등등으로 이름이 되었다.
64비트 환경에서 리버싱을 할 때 알아야 하는 건, x64에선 명령어 Instruction이 좀 다르다.
FF15(CALL) 같은 거 뒤에 4바이틑 절대주소가 아니라 상대주소(RVA)라서 계산을 해얗 ㅏㄴ다.
절대주소 = 명령어의 주소 + 상대주소 + 명령어의 길이 바이트
32비트 맨처음 배울 때는 cdecl, stdcall, fastcall 등등 배웠는데 64비트는 걍 변형된 fastcall로 통일된다.
함수 파라미터를 4개까지 레지스터에 저장해서 전달하는데 파라미터 순서마다 어떤 레지스터를 쓰는지 정해져있다.
| 파라미터 | 정수일 때 | 실수일 때 |
|---|---|---|
| 첫번째 | RCX | XMM0 |
| 두번째 | RDX | XMM1 |
| 세번째 | R8 | XMM2 |
| 네번째 | R9 | XMM3 |
4개를 넘어가게 되면 5번째 파라미터부터는 스택에 저장해서 전달한다. 대신 5번째를 스택의 최상위에 바로 넣는게 아니라, 마치 1~4번째 파라미터들이 먼저 스택 최상위에 있다고 치고 4개 사이즈 (8바이트x4=32바이트) 먼저 내주고 rsp+20h부터 저장된다.
스택 프레임을 만들 때는 32비트때와 다르다. 32비트때는 파라미터만큼 공간을 SUB해서 만들었다면, 64비트 때는 미리 스펙 공간을 훨신 넉넉하게 확보한 다음에 MOV로 스택 프레임에 파라미터를 전달한다. 그래서 코드에 PUSH/POP이 잘 없다고 한다.
프레임을 구성할 때도 RBP가 아니라 RSP로 직접 구현한다.
그 원래 32비트때는 함수 스택 프레임 시작할 때 push ebp 했자나. ebp를 스택 맨 위에 깔아놓고 그 다음에 sub ~~해서 공간 만들고 esp를 위로 올렸지.
이제는 그냥 SUB RSP ~~해서 RSP 자체를 ~~만큼 위로 올려버리면서 스택 프레임 공간을 만들어버린다.
함수를 리턴하고 파라미터를 정리할 땐 함수를 부른 Caller하며, 스택포인터를 정리할 필요가 없어서 실행 속도도 향상된다.
PE파일 역시 64비트 환경에선 PE32+라는 이름으로, 하위호환을 위해 기존 PE에서 조금 확장된 형태이다.
NT 헤더 구조체에선 OPTIONAL 헤더 멤버만 이름이 32에서 64로 다르다 정도
IMAGE_FILE_HEAADER에서 원래 맨처음에 32비트 환경이라고 알려주는 Machine 멤버가 014C로 정해졌지. 근데 이제 64비트면은 이게 8664로 달라진다. (IA64용에서는 0200)이다.
IMAGE_OPTIONAL_HEADER가 이제 가장 변화를 많이 겪은 구조체다.
맨 처음 MZ인 010B가 020B로 달라져서 PE로더는 이걸 읽어서 64비트용 PE+인지 확인한다.
데이터섹션 시작 rva를 알려주는 이 항목이 사라짐
크기가 커진 가상 메모리에 대응하기 위해서 이미지베이스 자료형이 8바이트 ulonglong으로 변경됐다는데 뭐 그렇다 치면 되다.
그냥 pe32+는 64비트에선 16TB 가상 메모리 공간 중 어느곳에도 로딩 될 수 있다고 보면 된다.
| 파일 유형 | 메모리 공간 |
|---|---|
| exe, dll | 하위 8TB 유저 영역 |
| sys파일 | 상위 8TB 커널 영역 |
Address of EP, Size of Image가 그대로 32비트 4바이트 DWORD다. 즉 파일에서의 PE+가 아무리 64비트 가상 메모리에 올라가도, 그 메모리 안에서 점유하는 크기는 최대 4GB(32비트)일 뿐이다.
Image가 의미하는 메모리에 로딩된 상태이다. 파일이 그대로 메모리에 올라가는게 아니라, 섹션 헤더에 정의된 시작 주소, 섹션 크기 등에 맞춰서 올라가는 거다. 그래서 메모리에서 pe는 파일에서와 모양이 다르다.
Size of Stack Reserve, Size of Stack Commit, Size of Heap Reserve, Size of Heap Commit 같은 자료형의 크기가 ulonglong인 8바이트로 바뀌었다.
확장된 가상 메모리에 대응하기 위해서지
이 구조체 크기도 4바이트에서 8바이트로 달라졌다.
IAT 따라갈 때 8바이트 배열로 읽어야 함을 주의하면 된다.
64비트 응용 프로그램이나 커널 디버깅 때 유용한 기본 명령어다
u = unassemble : 명령어 표시
| 명령어 | 활용 |
|---|---|
| u | 다음 명령어 |
| u address 주소 | 그 주소 다음 명령어 표시 |
| u L10 | 명령어 10줄 표시 |
| ub | 이전 명령어 표시 |
t = Trace[F11] : Step into
p = Pass[F10] : Step Over
g = Go(Run) : 실행
| 명령어 | 활용 |
|---|---|
| g | 실행 |
| g address | 그 주소까지 실행 |
d = Dump : 내용 표시
| 명령어 | 활용 |
|---|---|
| d address | 주소 내용 표시 |
| db address | byte 표시 |
| dd address | dword 표시 |
| dq address | qword 표시 |
r = Register : 레지스터표시
- r register: 지정된 레지스터만 표시
bp = Break Point : 브레이크포인트 설정
| 명령어 | 활용 |
|---|---|
| bp | 브레이크포인트 설정 |
| bl | 브레이크포인트 목록 표시 |
| bc | 브레이크 포인트 삭제(clear) |
lm = Loaded Module : 디버기 프로세스에 로딩된 모듈(라이브러리) 표시
dt = Display Type : 표시
| 명령어 | 활용 |
|---|---|
| dt struct name | 구조체 멤버 표시 |
| dt struct name address | 주소를 구조체에 매핑시켜 표시 |
!dh = Display PE Header
-!dh loaded address : PE Viewer