오늘은 쉘코드에 대해 공부하면서 포스팅을 해보고자 했지만 ..
어셈블리어에 대한 탄탄한 기초 지식이 쉘코드작성부터 포너블 공부에 중요할 것 같아서 먼저 어셈블리어에 대한 기본기를 위해 다시 어셈블리어로 돌아와서 포스팅을 하겠습니다.
어셈블리어의 기본 구조
어셈블리어는 명령어(Opcode)와 피연산자(Operand) 두가지로 구성됩니다. 피연산자는 연산의 입력 값으로 레지스터, 상수, 메모리 주소 등이며, 피연산자의 개수는 0개~2개까지 올 수 있으며 명령어마다 차이가 있습니다.
mov eax, 3
다음과 같은 어셈블리 한 줄에서 mov는 Opcode에 해당하고 eax, 3에서 eax는 첫번째 Operand, 3은 두번째 Operand가 됩니다.
Opcode
자주 사용하는 명령어들은 다음과 같습니다.
| 데이터 이동 | mov, lea |
| 산술 연산 | add, sub, imul(곱셈), idiv(나눗셈), inc, dec |
| 논리 연산 | and,or,xor,not |
| 비교 및 점프 | cmp, je, jz, jne ,jnz, jmp |
| 스택 조작 | push, pop, leave |
| 함수 호출, 복귀 | call, ret, leave |
| 시스템 콜 | syscall |
Operand
피연산자에는 3가지 종류가 있습니다.
1. 레지스터
2. 상수
3. 메모리
1. 레지스터 : 레지스터끼리 데이터를 주고받거나 연산할 때 사용합니다.
mov rax, rbx #rbx값을 rax에 복사
add rcx, rdx #rcx에 rdx값을 더한 뒤 rcx에 저장
2. 상수 : 상수를 직접 지정하여 연산합니다.
mov rax, 10 #rax에 10을 대입
cmp rcx, 100 #rcx가 100과 같은지 비교
3. 메모리 : 레지스터가 가르키는 메모리 주소를 참고하거나 메모리를 직접 참고할수도 있습니다. []를 사용하며 형태가 다양합니다.
mov eax, [0x00401000] #eax에 0x401000주소의 4바이트 값 저장
mov rax, [rbx] #rbx가 가르키는 주소에서 8바이트를 읽어 rax에 저장
add rax, [rsp+8] #rsp+8주소에 있는 8바이트값을 읽음 (보통 스택에서 값 꺼낼 때)
mov eax, dword ptr [rbx] #rbx가 가르키는 메모리에서 4바이트 읽어서 eax에 저장
mov al, byte ptr ds:[rax] #데이터 세그먼트에서 1바이트를 읽어옴
위의 예제 외에도 좀 다양한 형태의 메모리 주소 연산이 존재합니다.
산술 연산과 논리 연산
산술 연산과 논리 연산의 명령어에 대해서는 간단하게만 정리하고 넘어가겠습니다.
| and a,b | a에 b의 값을 더한다 |
| sub a,b | a에 b의 값을 뺀다 |
| inc a | a의 값을 1 증가시킨다 |
| dec a | a의 값을 1 감소시킨다. |
| and a, b | a와 b를 and 연산한다 |
| or a,b | a와 b를 or 연산한다 |
| xor a,b | a와 b를 xor한다 |
| not a | a의 비트를 반전한다 |
어셈블리어의 연산은 기본적으로 =가 붙어있다고 생각하면 편합니다. add a,b의 경우 a에 b를 더한값을 a에 저장하기 때문에 코딩할때의 a += b 와 동일하게 작동합니다.
비교와 분기
비교 명령어는 두 피연산자의 값을 비교하고, 플래그를 설정합니다.
cmp a, b #a와 b를 비교
비교는 내부적으로 a-b를 연산을 통해서 이루어지는데 결과를 레지스터나 메모리에 저장하지는 않고 결과에 따라 플래그레지스터를 설정합니다.
만약에 a와 b가 같은경우 ZF(ZeroFlag)가 1로 설정 됩니다.
분기 명령어는 rip를 이동시켜 함수의 실행 흐름을 바꿉니다.
jmp a # a로 rip를 이동시킨다
je a #직전에 비교한 두 피연산자가 같으면 a로 jmp한다
jz a #위와 동일한 명령어
jne a #직전에 비교한 두 피연산자가 다르면 a로 jmp한다
jnz a #위와 동일한 명령어
*je,jz는 ZF가 1이면 점프합니다. jne와 jnz는 ZF가 0이면 점프합니다.
*처음 리버싱을 공부하고 간단한 문제들을 풀때는 jz를 jnz로 패치한다던가 하는 방식으로 간단하게 문제를 풀 수 있습니다. 다음에 시간 있을 때 간단한 write-up도 포스팅해보겠습니다.
스택 조작
push a : a를 스택 최상단에 쌓음
#push a 연산
rsp -= 8
[rsp] = a
스택의 최상단 주소를 8바이트만큼 감소시켜 공간을 만들고 그 위치에 a를 저장합니다.
pop a : 스택 최상단의 값을 꺼내서 a에 대입합니다.
#pop a 연산
rsp += 8
reg = [rsp-8]
함수 호출, 복귀
call a : a에 위치한 프로시저 호출
#call a 연산
push return address
jmp a
현재 위치(리턴 주소)를 스택에 저장한 뒤, a로 jmp한다
a에서 함수가 종료되면 다시 스택에서 리턴주소를 꺼낸 뒤 원래의 함수 흐름으로 복귀함
leave : 스택 프레임 정리
#leave 연산
mov rsp, rbp
pop rbp
현재 스택 포인터를 베이스 포인터 위치로 되돌린다 (함수 시작 전 상태로 복구)
스택에서 이전 함수의 rbp값을 꺼내어 복원한다.
ret : 스택에서 리턴주소를 꺼내서 해당 주소로 jmp한다
이상으로 간단한 어셈블리어 기초에 대해서 포스팅을 마치겠습니다.
다음 포스팅은 if문과 반복문의 어셈블리어를 보면서 분석해보도록 하겠습니다.

'취약점분석 > Reversing' 카테고리의 다른 글
| [Reversing]콜링 컨벤션(Calling convention) -2 (64 bits) (1) | 2025.06.27 |
|---|---|
| [Reversing]콜링 컨벤션(Calling convention) - 1 (32 bits) (1) | 2025.06.25 |
| [Reversing] 어셈블리어 실전 구조 분석 (if문과 반복문) (1) | 2025.06.13 |
| [Reversing] 바이러스의 보안 모듈 우회 방법 (0) | 2025.06.11 |
| [Reversing] PE 헤더란? (PE Header) (0) | 2025.06.10 |