Layer7 동아리 과제

리버싱 3차시 과제

msh1307 2022. 7. 23. 23:01

x86 VS x64 

x86은 x86 계열 32비트 CPU(Intel, AMD)의 ISA이다.

x86_32라고 부르는 것을 더 많이 본 것 같다.

 

x64는 Intel이나 AMD 같은 x86 계열 64bit CPU들이 사용하는 ISA(Instruction Set Architecture)를 말한다.

x64를 x86의 64비트 버전이라고 해서 x86_64라고도 많이 부른다.

 

기본적으로 x64는 x86을 지원한다.

 

x64, x86 registers

레지스터는 되게 많다. 

그런데 범용 레지스터(General Purpose Register)만 살펴보겠다. 

그 이유는 사용자가 직접 사용할 수 있는 몇 안 되는 레지스터이기 때문이다.

실제로 디버깅할때도 거의 범용 레지스터만 본다. 

가끔 eflags도 본다. 

cs, ss, ds, es, fs, gs 같은 특수한 레지스터들은 사실상 우리가 직접 사용할 수 있는 레지스터가 아니고 리눅스가 알아서 잘 세팅해서 사용하는 레지스터들이다.

 

앞에 r이 붙은 레지스터들이 x64의 범용 레지스터다. 64비트의 크기를 가지고 있다.

앞에 e가 붙은 레지스터들은 x86의 범용 레지스터다. 32비트의 크기를 가지고 있다.

 

앞에서 x64가 x86을 지원한다고 했는데, 지원되는 이유가 명령어 셋이 같기도 하지만 레지스터가 호환이 돼서 그렇다.

rax를 예시로 들면 하위 32비트를 eax로 사용하기 때문에 x86에서 돌아가는 프로그램도 잘 돌아간다.

 

범용 레지스터들은 말 그대로 어떻게 쓰던 상관은 없지만, 어느 정도는 컴파일러가 용도를 정해두고 사용한다. 

 

rax(eax)는 함수의 return 값이 저장하는데 사용된다.

rbx(ebx)는 메모리 주소를 저장하기 위해 사용된다.

rcx(ecx)는 반복문에 카운터 변수로 사용된다.

rdx(edx)는 산술 연산에서 rax와 함께 자주 사용된다.

rsi(esi)는 데이터 복사할 때 source의 주소를 저장하는 데 사용된다.

rdi(rsi)는 데이터 복사할 때 destination의 주소를 저장하는 데 사용된다.

rbp(ebp)는 스택의 base를 가리키려고 사용된다.

rsp(esp)는 스택의 top을 가리키려고 사용된다.

나머지는 여분의 레지스터이고 x86에는 존재하지 않는다. 

x64, x86 calling convention

x86이냐 x64이냐에 따라서 상황이 다 다르기 때문에 함수 호출 규약도 조금씩 다르다.

함수 호출 규약은 함수를 호출하는 방식에 대한 약속이라고 볼 수 있다.

다양한 함수 호출 규약이 존재하지만 주로 보게되는 함수 호출 규약만 정리했다.

 

x86 호출 규약 : 호출 규약에 따른 동작 구분이 존재한다.

__cdecl

  • C/C++의 기본 호출 규약이다.
  • 인자를 오른쪽부터 왼쪽 순서로 스택에 push한다.
  • caller가 스택을 정리한다.

__stdcall

  • Win32 API의 기본 호출 규약이다.
  • 인자를 오른쪽부터 왼쪽 순서로 스택에 push 한다.
  • callee가 스택을 정리한다.

__fastcall

  • Win32 API가 지원은 하는데 잘 안 쓰는 호출 규약이다.
  • 첫 번째 인자는 ecx에, 두 번째 인자는 edx에 넣고 나머지를 오른쪽부터 왼쪽 순서로 스택에 push 한다.
  • callee가 스택을 정리한다.

x64 호출 규약 : x86과는 다르게 __fastcall로 통일되었다. 

같은 __fastcall이지만 파일 포맷에 따라 세부 규약은 약간 다르다.

__fastcall

  • x64에서 다 쓴다.
  • Linux의 ELF 같은 경우에는 왼쪽부터 rdi, rsi, rdx, rcx, r8, r9 순서대로 인자를 전달하고 나머진 스택으로 전달한다.
  • Windows의 PE 같은 경우에는 왼쪽부터 rcx, rdx, r8, r9 순서대로 인자를 전달하고 나머진 스택으로 전달한다.
  • __fastcall의 일종이라서 callee가 스택을 정리한다.

 

EFLAGS

흔히 flag 레지스터라고 부른다.

다 알 필요는 없고 일부 부분만 알면 충분하다.

 

CF(Carry Flag)

  • 자리 올림(carry)이 발생하면 1이 된다.

ZF(Zero Flag)

  • 연산 결과가 0이면 1이 된다.
  • 분기문에서 자주 쓰인다.

SF(Sign Flag)

  • 연산 결과가 양수이면 0, 음수이면 1이 된다.
  • 부호 있는 수라면 MSB가 sign bit고 그 bit랑 같다.

OF(Overflow Flag)

  • 연산 결과가 허용 범위를 벗어나면 발생
  • Overflow

 

메모리 구조

프로세스마다 할당받는 메모리를 영역별로 나눌 수 있다.

완전히 똑같은 건 아니고 대략적으로 이렇다. 

BSS와 GVAR을 DATA 영역으로 보겠다.

 

STACK

  • 함수의 지역 변수가 위치한다.
  • 함수의 임시 공간이라고 봐도 좋다.

HEAP

  • 프로그래머가 동적으로 할당하는 메모리이다.
  • malloc 같은 함수로 할당받는 것을 생각하면 된다.

DATA

  • 상수나 전역 변수들이 위치한다.

TEXT

  • 코드가 위치한다.

STACK FRAME

 

함수마다 스택 프레임이 생성된다.

아까 위에서 언급한 스택 영역에 이 스택 프레임들이 쌓인다.

 

함수가 실행되면 그 함수의 스택 프레임이 스택에 쌓이고, 그 함수가 return 해서 끝나면 스택 프레임이 정리된다. 

 

조금 더 자세히 확인해보면 아래와 같이 생겼다.

여기서 stack pointer는 rsp나 esp를 말하는 것이고 frame pointer는 rbp나 ebp를 말하는 것이다.

frame pointer를 base pointer라고도 하는 것 같다.

DrawLine과 DrawSquare를 함수라고 보면 된다. 

아까와 위아래가 바뀌었는데, 그 이유는 high address를 위로 뒀기 때문이다. 

아까 사진은 high address가 아래이다.

Frame pointer는 항상 saved frame pointer를 가리킨다.

이 saved frame pointer를 흔히 SFP라고 말한다.

이 SFP는 전 함수의 스택 프레임의 base pointer를 저장하기 위해서 쓰인다.

return address는 RET라고 부르기도 하는데 함수가 끝나고 나서 다시 돌아갈 코드의 주소가 적힌다.

 

Assembly

어셈블리어는 기계어와 1 대 1 매칭 되는 저수준 언어이다.

여기선 x64 기준으로 설명한다.

Intel 문법을 기준으로 opcode operand1, operand2의 명령 구조를 가지고 있다.

 

예시로 알아보자

  • mov rax, 3 : rax에 3을 복사해라
  • mov rax, qword ptr [esp] : esp가 가리키는 주소부터 8바이트(QWORD)를 읽어서 rax에 복사해라
  • mov edi, dword ptr [rax+0x10] : rax+0x10이 가리키는 주소부터 4바이트(DWORD)를 읽어서 edi에 복사해라

[]는 포인터의 참조 연산과 같다. 

앞에 dword ptr이나 qword ptr은 주소로부터 몇 바이트를 읽어올 것인지를 나타낸다. 

qword ptr : 8 바이트

qword ptr : 4 바이트

word ptr : 2 바이트

byte ptr : 1 바이트

  • lea rax, [rbp+0x4] : rbp+0x4의 effective address를 rax에 넣어라

effective address는 segment의 base부터의 offset이다. 

아까 데이터 영역, 스택 영역이라고 언급했었는데 그 영역의 base부터의 떨어진 거리라고 생각하면 편하다.

  • push rax : 스택에 rax 추가 -> 이때 rsp 8 바이트 이동
  • pop rbp : 스택의 rsp에서 8바이트만큼 읽어서 rbp에 저장 -> rsp 8 바이트 이동(push에서의 rsp 방향과 반대)
  • call func : func 함수를 호출함 -> 스택에 RET(Return address)를 push 하고 이동
  • ret : 현재 실행 중인 함수에 리턴함 -> 정확히는 스택의 rsp에서 8바이트 읽어서 rip에 넣음
  • jmp addr : addr로 점프함 -> rip를 addr로 옮김
  • je addr : jump if equal -> ZF가 세팅되어있으면 addr jump
  • jne addr : jump if not equal -> ZF가 세팅 안되어있으면 addr jump
  • ja addr : jump if above -> SF를 확인해서 A > B면 addr로 jump
  • jb addr : jump if below -> SF를 확인해서 A < B면 addr로 jump
  • jae addr : jump if above or equal -> SF와 ZF를 확인해서 A >= B면 addr jump
  • jbe addr : jump if below or equal -> SF와 ZF를 확인해서 A <= B면 addr jump

pwndbg 자주 쓰는 명령어

  • start : 처음부터 시작
  • disass main : main 함수 disassemble
  • ni : next instruction - 함수 있어도 바로 다음 명령
  • si : step into - 함수 있으면 안으로 들어감
  • i r : info registers인데 레지스터 값 확인
  • i r [레지스터]: 레지스터 확인
  • b * [함수+오프셋] : 브레이크 포인트 걸기
  • b [함수] : 함수 이름으로 브레이크 포인트 걸기 
  • info break : 브레이크 포인트 확인
  • d : 모든 브레이크 포인트 지우기 
  • x/[범위][출력형식][범위 단위] [주소] : 메모리 값 확인
  • x/1xg [주소] : 주소부터 QWORD * 1 만큼 hex로 읽기
  • x/4xw [주소] : 주소부터 WORD * 4만큼 hex로 읽기
  • x/dw [주소] : 주소부터 WORD * 1 만큼 decimal로 읽기
  • p * [심볼] : 심볼로 주소 print

 

'Layer7 동아리 과제' 카테고리의 다른 글

리버싱 5차시 과제  (0) 2022.07.31
리버싱 4차시 과제  (0) 2022.07.26
리버싱 2차시 과제  (0) 2022.07.20
하드웨어 3, 4차시 과제  (0) 2022.06.19
하드웨어 2차시 과제  (0) 2022.06.11