-
Assembly Language 어셈블리어CTF/KNOWLEDGE 2021. 1. 14. 20:41
PRELIMINARY 서론
어셈블리어는 기계어와 일대일 대응되는 저수준 프로그래밍 언어이다. 고수준 언어로 작성된 소스 코드를 컴파일하면 어셈블리어로 변환되고, 어셈블러를 통해 이를 다시 기계어로 대응시키면 바이너리 파일이 만들어지게 된다. [1]
바이너리 파일이 주어졌을때, 원형이 되는 소스 코드가 무엇인지는 정확히 알 수는 없다. 그러나 시스템 해킹(pwnable)을 위해서는 프로그램이 어떻게 동작하는지 알아야 하고, 바이너리를 역으로 분석하여 작동 과정을 알아내는 리버스 코드 엔지니어링(reversing)이 필요하다.
IDA나 Ghidra와 같은 디컴파일러를 통해 코드의 윤곽을 유추하는데 도움을 받을 수 있지만, 디스어셈블된 어셈블리 코드는 프로그램에 대해 많은 정보를 가지고 있으므로 어셈블리어에 대한 이해는 중요하다.어셈블리어의 구성요소인 명령어(instruction)와 레지스터(register)에 대해 정리해보자.
Register 레지스터
레지스터는 컴퓨터 프로세서가 빠르게 접근할 수 있는 저장 공간으로, 현재 계산중인 값을 저장하거나 메모리 영역의 주소를 보관하는 등의 역할을 수행한다. 레지스터마다 정해진 명칭과 역할이 존재하므로, 각각이 어떤 기능을 담당하는지 숙지하면 어셈블리어를 이해하는데 큰 도움이 될 것이다. [3]
레지스터 문단을 작성하면서 참고한 글은 다음과 같다. [4][5][6][7]
General Purpose Register 범용 레지스터
산술 연산 레지스터는 각종 산술 연산과 입출력 등에서 범용적으로 사용된다.
- EAX(Accumulator) : 산술 연산에 사용되는 상수, 변수와 함수의 리턴값을 저장
- EBX(Base Index) : 배열의 인덱스와 같이 데이터를 가리키는 포인터
- ECX(Counter) : 반복문의 반복 횟수를 저장하거나 shift/rotate 명령어에 사용
- EDX(Data) : AX와 같이 산술 연산에서 사용되거나 입출력 포트 접근에 사용
인덱스 레지스터는 배열의 복사나 비교와 같이 source와 destination이 있는 연산에서 포인터로 사용된다.
- ESI(Source Index) : 해당 연산에서 source를 가리키는 포인터
- EDI(Destination Index) : 해당 연산에서 destination를 가리키는 포인터
포인터 레지스터는 스택 영역의 특정 주소를 가리킨다.
- ESP(Stack Pointer) : 스택의 맨 위를 가리키는 포인터
- EBP(Base Pointer) : 스택의 기준점을 가리키는 포인터, 한 함수 호출 내에서 변하지 않음
Instruction Pointer 명령 포인터 레지스터
- EIP(Instruction Pointer) : 다음 명령어의 주소를 저장
Segment Register 세그먼트 레지스터
컴퓨터 메모리 영역의 관리를 위해 사용되는 레지스터이다.
- SS(Stack Segment) : 스택 영역을 가리키는 포인터
- CS(Code Segment) : 코드 영역을 가리키는 포인터
- DS(Data Segment) : 데이터 영역을 가리키는 포인터
- ES(Extra Segment) : 위의 레지스터를 변경하는 것이 어려운 상황에서 예비로 사용되는 포인터
- FS(F Segment) : 다른 예비 포인터 ('E' 다음이 'F')
- GS(G Segment) : 또다른 예비 포인터 ('F' 다음이 'G')
EFLAGS Register 상태 레지스터
Extended-Flag 레지스터는 기존의 16bit 상태 레지스터가 확장된 것으로, 프로세서의 상태와 각종 연산의 결과를 boolean 형태로 저장하는 flag들을 한군데에 모아놓은 레지스터이다.
- 0, CF(Carry Flag) : 덧셈이나 뺄셈에서 올림, 버림이 발생했을 때 true
- 2, PF(Parity Flag) : 연산의 결과에서 켜진 비트의 개수가 짝수일 경우 true
- 4, AF(Auxiliary Carry Flag) : BCD(이진화 십진법) 연산에 사용되는 Carry Flag이다.
- 6, ZF(Zero Flag) : 연산의 결과가 0일 경우 true
- 7, SF(Sign Flag) : 연산의 결과가 0보다 작을 경우 true
- 8, TF(Trap Flag) : true일 경우, 한 명령어가 실행될 때마다 인터럽트가 발생한다. 디버깅에 사용된다.
- 9, IF(Interruption Flag) : true일 경우 인터럽트 요청을 받아들인다.
- 10, DF(Direction Flag) : 문자열 조작의 진행 방향을 조작한다. true일 경우 주소가 증가하는 대신 감소한다.
- 11, OF(Overflow Flag) : 부호 있는 산술 연산의 결과가 레지스터의 크기를 초과할 경우 true
위에서 설명한 x86 Architecture 상의 레지스터들을 한눈에 정리해보면 다음과 같다.
x64 Architecture
x64 Architecture 상의 레지스터는 R(register)로 시작하고, 64-bit로 크기가 확장되었다. 32-bit와 대응하는 레지스터는 E(extended)로 시작하며, 이는 기존 16-bit 레지스터에서 확장되었음을 의미한다. 또한 AX와 같은 16-bit 레지스터는 상위 8-bit(AH)와 하위 8-bit(AL)로 나뉘게 된다.
새로 추가된 레지스터의 기능과 기존 레지스터의 변경 사항은 다음과 같다.
- RBP : 더이상 base pointer의 역할을 하지 않고, 다른 범용 레지스터처럼 활용된다.
- R8~R15 : 새로 추가된 8개의 범용 레지스터, R8D(32-bit), R8W(16-bit), R8B(8-bit)와 같은 크기로도 사용
- RDI, RSI, RDX, RCX, R8, R9 : 순서대로 처음 여섯 함수 인자가 저장된다.
함수 인자와 레지스터에 대한 자세한 내용은 스택 프레임 파트에서 다룬다.
Instruction 명령어
어셈블리에는 Intel과 AT&T의 두가지 문법이 존재한다. 디버깅에 사용할 도구인 pwngdb가 기본적으로 Intel 문법을 지원하므로 아래의 모든 명령어는 Intel 문법을 기준으로 작성하겠다.
명령어 문단을 작성하면서 참고한 글은 다음과 같다. [9]
Main Instructions 주요 명령어
- PUSH dest : 스택의 맨 위에 dest를 저장한다. 따라서 esp가 감소하고 esp에 dest 값이 저장된다.
- POP dest : 스택의 맨 위 값을 dest에 저장한다. esp가 가리키는 값이 dest에 저장되고 esp가 증가한다.
- MOV dest src : src의 값을 dest에 저장한다.
- LEA dest src : src의 메모리 주소를 dest에 저장한다.
lea rbp [rax+0x1] 은 mov rbp rax, add rbp 0x1과 같다. [ ]는 포인터와 같은 역할을 하므로 mov rbp [rax+0x1]은 rax+0x1이 가리키는 값을 rbp로 옮기는 역할을 하지만, lea 명령어를 통해 주소 rax+0x1 자체를 rbp로 옮길 수 있다. mov rbp rax+0x1과 같이 레지스터에 직접 연산하는 것은 문법적으로 허용되지 않기 때문에 이러한 명령어를 만든 것으로 추정된다. [10]
- CALL label : 지정한 label로 jump한다.
CALL은 JMP와 비슷하지만 함수 호출에 사용되며, 호출이 끝난 다음에 실행될 명령을 스택에 return address로 저장한다는 특징이 있다. call label은 push eip, jump label과 동치이다.
- RET : esp가 가리키는 값을 eip에 저장한다.
함수 에필로그에서 esp는 return address를 가리키게 되므로 RET 명령어 이후에는 CALL에서 저장했던 다음 명령이 실행된다. ret은 pop eip, jmp eip와 동치이다.
- NOP : 아무 일도 하지 않는다(No operation). 특정 부분을 공백처리하기 위해 사용한다.
NOP 명령어는 이를 활용한 공격 기법인 NOP sled에서 자세히 다룰 것이다.
- INT label : 지정한 label의 인터럽트를 발생시킨다.
int 0x80은 시스템 콜을 호출하는 명령어로, 쉘코드를 작성할 때 자주 등장한다.
Conditional Instructions 조건 명령어
- CMP dest src : dest에서 src를 빼는 연산을 수행한다.
- JMP label : 지정한 label로 jump한다.
- JZ label : 비교한 결과가 같으면 label로 jump한다. (ZF=1)
부호가 있는(signed) 데이터의 비교
- JG label : dest가 크면 label로 jump한다. (ZF=0 and SF=OF)
- JGE label : dest가 크거나 같으면 label로 jump한다. (SF=OF)
부호가 없는(unsigned) 데이터의 비교
- JA label : dest가 크면(above) label로 jump한다. (CF=0 and ZF=0)
- JB label : dest가 작으면(below) label로 jump한다. (CF=1)
플래그의 값을 통한 비교
- JC label : CF가 1이면 jump한다.
- JNO label : OF가 0이면 jump한다.
E는 equal을, N은 not을 의미하므로 JL은 JNGE, JZ는 JE 등으로 대체할 수 있다.
Arithmetic Operation 산술 연산
- ADD dest src : src의 값을 dest에 더한다. (dest = dest + src)
- SUB dest src : src의 값을 dest에서 뺀다. (dest = dest - stc)
Logical Operation 논리 연산
- AND dest src: bitwise AND 연산의 결과를 첫 번째 피연산자에 저장한다. (dest = dest & src)
- OR dest src : bitwise OR 연산의 결과를 첫 번째 피연산자에 저장한다. (dest = dest | src)
- XOR dest src : bitwise XOR 연산의 결과를 첫 번째 피연산자에 저장한다. (dest = dest ^ src)
- TEST dest src : bitwise AND 연산을 수행한다.
- NOT dest : bitwise NOT 연산의 결과를 피연산자에 저장한다. (dest = !dest)
Type Specifier (BYTE, WORD, DWORD, QWORD) 타입 지정자
어셈블리를 분석하다 보면 다음과 같이 ~ PTR 이라는 표현을 볼 수 있을 것이다.
이때 PTR은 포인터를 의미하고, 그 앞에 붙는 접두사는 포인터가 가리키는 대상의 크기를 의미한다. 따라서 위 명령어는 주소 rsp+0x1008에 존재하는 8바이트 만큼의 값을 rax에 넣으라는 뜻으로 해석할 수 있다.
BYTE(1byte), WORD(2byte), DWORD(double word, 4byte), QWORD(quad word, 8byte)라는 명명법은 초창기 16-bit가 기본 데이터 처리 단위이던 시절, 이 단위를 WORD라는 자료형으로 이름붙인 것에서 시작되었다.
Reference
[1] en.wikipedia.org/wiki/Assembly_language
[3] en.wikipedia.org/wiki/Processor_register
[4] ece-research.unm.edu/jimp/310/slides/micro_arch1.html
[5] en.wikibooks.org/wiki/X86_Assembly/X86_Architecture
[6] blog.naver.com/mjnms/220460825993
[7] peemangit.tistory.com/37?category=820239
[9] www.tutorialspoint.com/assembly_programming/index.htm
[10] stackoverflow.com/questions/1699748/what-is-the-difference-between-mov-and-lea
'CTF > KNOWLEDGE' 카테고리의 다른 글
CTF 사이트 만드는 방법 / How to make CTF site (0) 2021.02.12 Memory Mitigation 메모리 보호기법 (0) 2021.01.19