학사 나부랭이

The basic of Assembly language & Debugger 본문

Dot-Gabi/Reversing

The basic of Assembly language & Debugger

태양왕 해킹 (14세) 2021. 7. 8. 21:17

 어셈블리어는 16진수인 기계어를 사람이 쉽게 이해할 수 있도록 표현을 바꾼 것이며, 각 명령어는 기계어와 일대일로 완벽히 매칭 된다. 기본적으로 한 번에 하나씩만 수행할 수 있다.

 

> PUSH

 스택에 값을 저장한다. PUSH 후에는 스택이 4바이트 커지기에, 그리고 스택은 높은 주소에서 낮은 주소로 할당되기에 SP 레지스터는 4바이트 감소한다.

ex) PUSH BP: BP 값을 스택에 PUSH || PUSH 5: 5를 스택에 PUSH

 

> POP

 스택의 끝에 있는 값을 가져온다. POP 후에는 스택이 4바이트 줄어들기에, SP 레지스터가 4바이트 증가한다.

ex) POP BP: 스택 끝의 값을 꺼내서 BP에 저장 || POP CX: 스택 끝의 값을 꺼내서 CX에 저장

 

> MOV

 지정한 값을 지정한 곳에 넣어준다. 바이트에 따라 다양한 파생 명령어가 있지만, 일단 값을 넣어준다고만 하자.

ex) MOV CX, 4: 4를 CX에 저장 || MOV SI, SP: SP의 값을 SI에 저장

 

> LEA

 MOV와 거의 비슷하지만, MOV는 주소에 있는 값을 저장하고, LEA는 주소를 그대로 저장한다. 즉, 포인터 형태의 변수 같은 느낌이다.

ex) LEA AX, [BP+10]: BP+10 위치의 주소를 AX에 저장 || LEA SI, [AX+4]: AX+4 위치의 주소를 SI에 저장

 

> INC, DEC

 INC: 값을 1 증가, DEC: 값을 1 감소

 

> ADD

 두 오퍼랜드의 덧셈 연산을 수햏 아햏햏

ex) ADD AX, 3A: AX에 3A를 더한 뒤 AX에 저장

 

> SUB

 두 오퍼랜드의 뺄셈 연산을 수행

ex) SUB SP, 10: SP에서 10을 뺀 뒤 SP에 저장

 

> CALL

 함수를 호출한다. 후에 리턴할 주소를 PUSH 한 뒤, 바로 뒤의 주소 값으로 점프한다.

ex) CALL 주소 값: 주소 값의 함수 실행 || CALL CX: CX에 저장된 주소로 이동 및 실행

 

> RET

 함수 내부에서 다시 원래 코드로 돌아오는 명령어이다. CALL 명령 수행 시, 스택에 저장했던 복귀 주소로 돌아온다. POP IP와 비슷하다.

CALL sum() // 함수 호출
sum():
 PUSH BP
 MOV BP, SP
 ...
 RET // 함수 호출 전의 코드로 복귀

 

> NOP

 아무런 동작도 하지 않는 잉여 코드예요. 기계어로는 0x90이며, 공격 코드를 작성할 때 자주 사용되는데, 단순히 주소를 채우거나, 주소 값을 모르는 상황에서 공격 성공률을 높이기 위해 사용되어요.

 

> XOR, OR, AND, SHR, SHL

 각각 해당하는 비트 연산(비트 단위 연산)을 수행한다.

명령어 설명
XOR 각 비트를 비교해서 값이 같으면 0, 다르면 1로 계산
ex) 10 XOR 13 = 7
     1010 (10)
     1101 (13)
===XOR===
     0111 (7)
OR 두 비트 중 하나라도 1이면 1, 아니면 0으로 계산
ex) 10 OR 13 = 15
     1010
     1101
===OR===
     1111 (15)
AND 두 비트 모두 1이면 1, 나머지는 0으로 계산
ex) 10 AND 13 = 8
     1010
     1101
===AND===
     1000 (8)
SHR 각 비트를 오른쪽으로 쉬프트 연산, 벗어난 비트는 CF 플래그 레지스터에 저장
    1010
==SHR 1==
    0101 (5)
==SHR 2==
    0010
      ↓
    0001 (1)
SHL 각 비트를 왼쪽으로 쉬프트 연산, 벗어난 비트는 CF 플래그 레지스터에 저장
     1010
==SHL 1==
    0100 (4)
==SHL 2==
    1000
      ↓
    0000 (0)

 

> CMP, JMP, JE, JNE, ...

 비교와 점프를 수행한다. 일반적으로 cmp 명령어 실행 결과에 따라 플래그 레지스터에 특정 값이 세팅되고, 그 조건에 따라 분기된다. if, for 문 등의 제어 구문을 구현할 때 사용된다.

명령어 설명
JMP 무조건 점프
JE 비교 결과가 같으면 점프
JGE 비교 결과가 크거나 같으면 점프
JLE 비교 결과가 작거나 같으면 점프
JNE 비교 결과가 서로 다르면 점프
JZ 0이면(제로 플래그가 1이면) 점프

 

아래는 디버거의 UI이다.

그리고 간단한 단축기는 F7, F8, F2인데, F7과 F8은 코드를 한 줄씩 실행해 주는데, F7은 함수 내부로 들어가고, F8은 함수 내부로 들어가지 않고 함수 실행 다음 줄로 바로 넘어간다. F2는 브레이크 포인트를 설정해준다.

 

그럼 간단한 프로그램을 디버깅해보자!

#include <stdio.h>
int sum(int a, int b) {
    int res = 0;
    res = a + b;
    return res;
}
int main() {
    int x = 9, y = 4, res = sum(x, y);
    if (res > 10) printf("GOOD! RES: %d", res);
    else printf("BAD... RES: %d", res);
}

초기화 루틴을 거치고 나면

main 함수

main 함수의 루틴을 볼 수 있다.

이 두 줄로 main 함수의 스택 프레임을 선언해 준다. 그리고 스택은 높은 주소에서 낮은 주소로 가니

이렇게 SP를 감소시켜 스택 메모리를 할당한다.

이들은 main 함수가 끝나고 돌아갈 때를 위한 세팅이다.

지역 변수인 x와 y의 값인 4, 9를 스택에 저장했다.

스택에 저장된 모습

stdcall 호출 규약에 따라 인자 값을 두 번 PUSH 하며

sum 함수

sum 함수 안에서 스택을 정리해 준다.

보면, 초반에 스택 프레임을 선언해 주거나, 스택 메모리를 할당해주는 과정은 main 함수와 비슷하며,

수식의 값을 AX에 저장해 가며 결과를 도출한다. 그리고 변수 값은 그림처럼 main 함수의 스택프레임에서 참조한다.

나머지는 다시 main 함수로 돌아가기 위한 절차이며,

이 코드는 BP와 SP가 같은지(스택메모리를 모두 반환했는지) 확인하는 명령과 함수이다.

그리고 다시 main 함수로 돌아와서,

AX 레지스터에 있는 계산 결과를 스택에 저장하고, 그 스택의 값과 Ah (10)와 비교해서 그 결과 값이 작거나 같은 상태면(JLE) 점프한다.

이는 Ah보다 큰 경우이며, print 함수를 CALL 하고, 스택을 정리한 후, 그 아래의, 작거나 같은 경우 동작할 코드 부분을 JMP 명령어로 피한다.

이는 Ah와 같거나 작은 경우이며, 마찬가지로 print 함수를 CALL 하고,

레지스터와 스택을 정리하고, main 함수를 끝낸다.

Comments