Instruction (명령어)
Reversing/Reverse Engineering

Instruction (명령어)

Instruction Format

명령어는 opcode + operand로 이뤄져있다. 

OPCode OPeRand

Opcode(Operation Code)

명령 코드; 명령어에서 실제로 어떤 동작을 하는지 나타내는 부분이다.

IDA에서 opcode가 보이게 설정

  • 명령코드(Opcode) 또는 기계코드(Machine Code)

바이너리를 구성하는 코드들로, CPU가 실제로 수행할 작업을 나타내는 숫자 코드다. 명령코드는 CPU의 종류별로 다른 값으로 표현될 수 있고, 명령코드의 종류에 따라 피연산자(operand)가 필요하기도 하다.

  • 어셈블리 코드(Assembly Code)

숫자로 표현된 명령코드가 어떤 의미를 갖는지 이해하기 쉽도록 작성된(Mnemonic) 코드이다. 어셈블리어가 기계어와 1:1로 대응되 듯, 명령코드와 1:1로 대응된다. 

opcode가 연산할 때 사용할 operand도 알아보기 쉽다. 

CPU의 동작을 그대로 옮겨놓은 것이기 때문에 코드가 매우 직관적이고 단순하다. 실제 소스코드와 같이 고차원적인 전체 흐름을 파악하기는 어렵다.


Operand

피연산자; 명령코드가 연산할 대상을 의미한다.

opcode를 함수라고 생각했을 때, operand는 함수에 들어가는 인자라고 생각하면된다.

Opcode의 종류에 따라 조금씩 다르긴 하지만,

일반적으로 Intel 방식의 어셈블리를 읽을 때는 opcode에 따라 연산한 결과를 "왼쪽" operand에 저장된다.

[출처] dreamhack-reversing x64 기초 4장 5페이지

상수값(Immediate)

정말 간단하게 operand에 상수값이 사용되는 경우다.

(dreamhack 예제)

  • rcx = 0xbeef
  • rcx = rcx + 0x1337 = 0xd226

 

레지스터

레지스터에 저장되어 있는 값(들어있는 값)이 operand로 사용된다.

(dreamhack 예제)

  • rcx = rbx
  • rcx = rcx - rax

 

Addressing Mode (✨)

레지스터에 저장된 메모리 주소를 참조한 값이 operand로 사용되는 경우다.

C 언어의 포인터 개념과 유사하다. 레지스터에 들어있는 값은 메모리주소이고, 해당 메모리 주소를 참조하는 값이 피연산자로 사용되는 것이다.

  • [reg]
  • byte(word/dword/qword) ptr [reg]

레지스터(reg)에 저장되어 있는 주소가 참조하는 값을 operand로 사용한다. 

'byte/word/dword/qword ptr'은 Pointer Directive로, reg가 참조하는 메모리에 저장된 값 중 하위 1/2/4/8 byte를 피연산자로 사용

①rax에 저장되어 있는 값을 rcx가 참조하는 주소의 메모리에 저장한다.

②rax가 저장하고 있는 값 중 1byte()만 rcx가 참조하는 주소에 저장한다.

  • [reg + d]

reg에 저장된 주소에서 d만큼 떨어진 오프셋을 실제로 참조하여 operand로 사용한다.

  • [reg1 + reg2]

reg1에 저장된 값과 reg2에 저장된 값을 더한 결과를 참조할 메모리의 주소로 사용하여 그 주소값을 참조해 operand로 사용한다.

  • [reg1 + reg2*i + d]

위 2가지 방식을 섞은 형태로, 구조체가 사용되는 경우 자주 사용되는 방식이다.

이 때, reg2에 저장된 값은 보통 자료형이나 구조체의 크기인 경우가 많다.


Instructions (어셈블리어)

Data Movement

값을 레지스터나 메모리 주소에 옮기는 명령어들

mov dst, src src에 들어있는 값을 dst로 옮긴다 (dst = src)
lea dst, addr load effective address; dst에 addr를 저장한다. (dst = addr)

dreamhack 예제

위 예제를 풀어보면서 mov 명령어와 lea 명령어의 차이점을 분명하게 확인할 수 있다.

lea 명령어에서는 [rbp+8]에 저장된 주소가 참조하는 값을 사용하는 것이 아니라, 참조하는 값의 주소 즉, rbp+8을 rax에 저장하게 된다.

Arithmetic Operations

산술 연산과 관련한 명령어들; CF, OF, ZF와 관련이 있다.

- Unary Instructions

inc dst
dec dst
dst의 값을 1 증가 / 감소 시킨다.
neg dst dst에 들어있는 값의 부호를 바꾼다. (2의 보수로 바꿈!)
not dst dst에 들어있는 값의 비트를 반전한다. (0→1, 1→0) (:bit wise inverse)

- Binary Instructions

add dst, src dst += src
sub dst, src dst -= src
imul dst, src dst *= src
and dst, src dst &= src (dst에 들어있는 값과 src 값을 AND 논리 연산한 결과를 dst에 저장한다)
or dst, src dst |= src (dst에 들어있는 값과 src 값을 OR 논리 연산한 결과를 dst에 저장한다)
xor dst, src dst ^= src (dst에 들어있는 값과 src 값을 XOR 논리 연산한 결과를 dst에 저장한다)

- Shift Instructions

shl dst,k
shr dst,k
dst << k shift left: dst의 값을 k만큼 왼쪽으로 shift
dst >> k shift right: dst의 값을 k만큼 오른쪽으로 shift (빈 자리는 0으로 채운다)
=> logical shift
sal dst,k
sar dst,k
dst << k shift arithmetic left: dst의 값을 왼쪽으로 shift
dst >> k shift arithmetic right: dst의 값을 오른쪽으로 shift (최상위 비트가 보존된다)
 => arithmetic shift

Conditional Operations

분기문, 조건문과 같이 코드의 실행 흐름을 제어하는 데 중요한 역할을 하는 명령어들

특히, 분기문에서는 FLAGS 레지스터의 각종 플래그에 영향을 받아 코드의 실행 흐름이 결정되므로 각 명령어들이 어떤 플래그의 영향을 받는지 아는 것이 중요하다.

- 비교문

test op1, op2 op1과 op2의 내용을 AND 연산한다 >> ZF의 값 변화 (NULL 체크할 때 주로 사용!)
cmp op1, op2 op2에서 op1의 값을 가상으로 빼서 크기를 비교한다 >> ZF와 CF의 값 변화

- 점프문

jmp location location이 가리키는 곳으로 무조건 점프한다.
** jcc (Jum If Condition is met) **
조건부 jmp; 점프 명령어를 수행하기 전 산술 연산이나, test, cmp 등의 연산을 수행하고 바뀐 플래그 값을 토대로 점프의 수행 여부를 결정한다.
je location
jne location
equal (ZF=1)
not equal (ZF=0)
jg location
jge location
jl location
jle location
ja location
jb location
(signed) >
(signed) >=
(signed) <
(signed) <=
(unsigned) >
(unsigned) <
js location
jns location
negative (SF = 1)
not negative (SF = 0)

Stack Operations - PUSH / POP

Function Prologue / Epilogue

- Function Prologue

함수가 시작할 때 스택포인터 레지스터(rsp)에 들어있는 주소에서 충분한 값을 빼주면서 함수 안의 지역변수를 사용하기 위한 공간을 확보한다.

 

- Function Epilogue

함수가 끝날 때 프롤로그에서 빼준 값 만큼 다시 스택포인터 레지스터(rsp)에 더해주면서 스택 포인터를 복원하여 함수에서 사용한 스택을 정리한다.

 

push op1 스택에 새로운 데이터를 넣는다. 
push op1 은

sub rsp, size // rsp를 들어갈 데이터의 사이즈만큼 빼서 데이터가 들어갈 크기를 확보한다.
mov [rsp], op1  // 데이터를 rsp가 저장하는 주소에 복사한다.
위 2가지 과정과 동일한 효과를 낸다.
pop op1 스택의 최상단에 있는 데이터를 빼낸다.
pop op1 은

mov op1, [rsp]  // rsp에 저장된 주소에 저장된 값을 op1에 복사한다.
add rsp, size  // rsp가 가리키는 주소를 size만큼 더해줌으로써 스택의 사이즈를 줄이고 스택을 정리한다.
따라서, 스택의 최상단 값을 빼내고 op1에 저장하는 효과를 낸다.

Procedure Call Instructions

call location location에 저장되어 있는 함수를 호출한다.
call location 은

push retaddr  // 함수가 종료되고 난 후 다음 실행해야 할 명령어의 주소를 기억해야 프로그램의 실행을 이어갈 수 있다. 따라서, 스택에 return address를 push해 저장해둔다.
jmp location  // 호출할 함수의 주소로 jmp 한다.
과 같다. 
ret(retn) 호출된 함수가 마지막으로 사용하는 명령어; 함수를 종료한 뒤 return address로 돌아가는 역할을 한다.
ret

pop rip // 현재 스택의 최상단 값을 RIP에 저장한다.
jmp rip // RIP에 저장된 주소로 점프하여 실행한다.
와 같다.

retn N 은 스택을 사용하고 난 후 N 바이트를 반환해야 하는 것을 보여준다.

[참고]

리버싱 입문(조성문/프리렉)

dreamhack.io/learn/20#23

SMALL

'Reversing > Reverse Engineering' 카테고리의 다른 글

패킹과 언패킹  (0) 2021.05.18
어셈블리 코드 변환  (0) 2021.04.06
HelloWorld.exe  (2) 2021.04.01
리버싱을 위한 기초 지식(구조)  (0) 2021.03.28
[dreamhack] 리버싱 엔지니어링이란  (0) 2021.03.27