[Dreamhack]- x86 Assembly

Chris Kim·2024년 10월 1일

리버싱

목록 보기
7/10

1. 어셈블리어와 x86-64

1.1 어셈블리 언어

앞서 CPU 아키텍쳐를 살펴본 바 있다. 다양한 명령어 집합구조가 있는 만큼 서로 다른 어셈블리어가 존재한다. 본 문서에서는 x64아키텍쳐를 대상으로 설명을 한다.

1.2 x64 어셈블리 언어

  • 기본구조
    x64 어셈블리 언어의 문법은 명령어(Operation Code; Opcode)피연산자(Operand) 로 구성된다.

  • 명령어
    매우 많은 명령어가 존재한다 본 문서에서는 21개의 명령어를 설명한다.

명령코드 분류예시
데이터 이동(Data Transfer)mov, lea
산술 연산(Arithmetic)inc. dec, add, sub
논리 연산(Logical)and, or, xor, not
비교(Comparison)cmp, test
분기(Branch)jmp, je, jg
스택(Stack)push, pop
프로시져(Procedure)call, ret, leave
시스템 콜(System call)syscall
  • 피연산자
    피연산자는 총 3가지가 있다. (1) 상수(Immediate Value), (2)레지스터(Register), (3)메모리(Memory), 메모리 피연산자는 []로 둘러싸여 표현되며, 크기 지정자 TYPE PTR이 추가될 수 있다. TYPE에는 1, 2, 4, 8바이트 순으로 BYTE, WORD, DWORD, QWORD가 지원된다.

2. x86-64 어셈블리 명령어

2.1 데이터 이동

데이터 이동 명령어는 어떤 값을 레지스터나 메모리로 옮기도록 지시한다. mov 명령어는 다음과 같은 형식을 가진다.

mov dst, src // src에 들어있는 값을 dst에 대입

lea는 다음과 같은 형식을 가진다.

lea dst, src //src의 유효주소(EA)를 dst에 저장

2.2 산술연산

add dst, src //dst에 src의 값을 더한다.
sub dst, src //dst에서 src의 값을 뺀다.
inc op //op의 값을 1 증가시킴
dec op //op의 값을 1 감소시킴

2.3 논리연산

and dst, src
or dst, src
xor dst, src
not op

2.4 비교(연산 결과를 대입하지 않음)

cmp op1, op2 // op1과 op2를 비교, ZF 설정시 두 값이 같음
test op1, op2 // op1과 op2에 AND 비트연산 수행

2.5 분기

  • jmp addr: addr로 rip를 이동시킨다.
  • je addr: 직전에 비교한 두 피연산자가 같으면 점프
  • jg addr: 직전 비교에서 전자가 크면 점프

2.6 Opcode: 스택

push val // val을 스택 최상단에 쌓음
pop reg // 스택 최상단의 값을 꺼내서 reg에 대입

2.7 Opcode: 프로시저

프로시저는 특정 기능을 수행하는 코드 조각이다. 이를 이용해서 코드 길이를 줄이고, 가독성을 높인다.
프로시저를 부르는 행위를 호출이라고 한다. 그리고 프로시저에서 돌아오는 것을 반환이라고 한다. 즉 원래의 흐름으로 복귀해야하므로 호출 다음의 명령어 주소를 스택에 저장하고 난 뒤에 rip로 이동한다.

  • call addr: addr에 위치한 프로시저 호출
    push return_address
    jmp addr
  • leave: 스택프레임 정리
    mov rsp, rbp
    pop rbp
  • ret: return address로 반환
    pop rip
  • 스택프레임
    스택프레임은 복수의 함수를 호출하는 경우 같은 스택영역을 사용하지 않도록 만드는 것이다. Application binary interface(ABI)에서는 함수 호출시, 자신의 스택프레임을 만들고 반환할 떄 이를 정리한다.

즉 함수 호출에서 반환에 이르기까지 과정을 설명하면 다음과 같다.
(1) call addr로 addr에 위치한 프로시저를 호출한다. 이때 돌아올 위치를 push한다.
(2) 기존 rbp를 스택에 push하고, rbprspmov한다.(rsp == rbp)
(2-1) 이 단계에서 rsp에는 rbp가 돌아갈 메모리 주소를 담고 있는 메모리를 가리키고있다.
(3) 필요한 크기 만큼 rspsub를 수행, 스택 공간을 확보하면서 스택프레임을 만든다.
(4) 필요한 연산을 한다.
(5) 연산을 마무리 한 뒤, rsprbpmov 한다. rsp는 스택 공간을 확보하기 전, 즉 (2-1)에서의 위치를 가리킨다.(rsp == rbp)
(6) 지금 rsp는 (2-1)에서 말한 메모리를 가리킨다.(rsp는 현재 사용하는 스택을 가리키며, pop을 수행하면 rbp가 돌아가야하는 메모리 주소가 나온다. poprbp에 수행한다. rbp는 프로시저를 호출하기 전의 위치로 돌아온다.
(6-1) pop을 수행하면서 rsp는 (1)에서 push한 스택을 가리킨다.
(7) ret을 수행하면서 (1)에서 푸쉬한 주소를 rip에 넣으면서 프로시저 마무리.

요약하자면
rip, rbp 순으로 복귀할 주소들을 스택에 push해 놓고 스택을 확장해서 필요한 연산을 한 뒤 rsp는 원래 자리를 rbp가 가리키고 있으므로 그 자리로 돌아가고, rbp -> rip 순으로 스택에서 pop을 수행해서 복귀한다.

2.8 Opcode: 시스템 콜

syscallraxarg0(rdi)arg1(rsi)arg2(rdx)
read0x00unsigned int fdchar *bufsize_t count
write0x01unsigned int fdconst char *charsize_t count
open0x02const char *filenameint flagsumode_t mode
close0x03unsigned int fd
mprotect0x0aunsigned long startsize_t lenunsigned long prot
connect0x2aint sockfdstruct sockaddr *addrint addrlen
execve0x3bcons char *filenameconst char *const *argvconst char *const *envp
profile
회계+IT=???

0개의 댓글