어셈블리어

동동이·2022년 6월 27일
1

어셈블리어는 2개의 구조로 이루어져 있다

문장으로 치면 동사에 해당하는 opcode와 그 목적어에 해당하는 피연산자가 있다.

OPcode

데이터 이동 명령어

  1. mov
mov dst,src

mov명령어는 src에 들어있는 값을 dst에 대입하는 opcode이다.

  1. lea
lea dst,src

lea 명령어는 mov 명령어와 비슷하지만 mov는 src에 들어있는 값을 대입하지만, lea는 src의 주소값을 대입한다

산술 명령어

  1. add
add dst,src

add는 말그대로 dst에 src값을 더하는 명령어이다

  1. sub
sub dst,src

sub은 dst에서 src를 빼는 명령어이다.

  1. inc
inc op

op의 값을 1증가 한다.

  1. dec
dec op

op의 값을 1감소 한다.

비교 명령어

  1. cmp
cmp op1,op2

op1과 op2를 빼서 대소를 비교한다. 결과는 따로 대입하지않고 만약 두값이 같다면 0이되며, ZF(zero flag)가 설정된다.

  1. test
test op1,op2

op1과 op2룰 AND연산한다. 결과는 따로 대입하지않고, 만약 하나의 값이 0이라면 ZF가 설정된다.

분기 명령어

분기 명령어는 rip(instruction pointer)를 이동시켜 실행 흐름을 바꾸는 명령어이다

  1. jmp
jmp addr

addr로 rip를 이동시키는 명령어로, 예시를 들어보자면

1: xor rax, rax
2: jmp 1 ; jump to 1

여기에 파생으로 직전의 비교한 두 피연산자가 같으면 점프하는 je (jump if equal)

1: mov rax, 0xcafebabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1 ; jump to 1

전자가 더 크면 점프하는 jg (jump if greater)

1: mov rax, 0xcafebabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1 ; jump to 1

등 종류가 엄청많다

스택 연산자

  1. push
push val

val을 스택의 최상단에 쌓는다.

  1. pop
pop reg

스택의 최상단의 값을 꺼내서 reg에 대입한다.

프로시저(중요)

프로시저란 특정기능을 수행하는 코드조각을 말한다. 반복되는 연산을 프로시저 호출로 대체하고, 기능별로 코드 조각에 이름을 붙여 가독성을 크게 높일 수 있다.
1. call

call addr

addr에 있는 프로시저를 호출한다

코드실행전

[Register]
rip = 0x400000
rsp = 0x7fffffffc400 
[Stack]
0x7fffffffc3f8 | 0x0
0x7fffffffc400 | 0x0 <= rsp
[Code]
0x400000 | call 0x401000  <= rip
0x400005 | mov esi, eax
...
0x401000 | push rbp

코드실행후

[Register]
rip = 0x401000
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x400005  <= rsp
0x7fffffffc400 | 0x0
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | push rbp  <= rip
  1. leave
    leave 명령어는 스택프레임을 정리하는 명령어이다

    스택프레임이란??
    스택은 함수별로 지역변수나 임시값들을 저장하는 영역이다.
    하지만 이 영역을 아무런 구분없이 사용하게 된다면 서로 다른 함수가 같은 메모리 영역을 사용해 충돌이 일어날 수 있고,
    따라서 스택의 영역을 명확히 구분하기 위해 스택 프레임이 이용된다.

코드 실행전

[Register]
rsp = 0x7fffffffc400
rbp = 0x7fffffffc480
[Stack]
0x7fffffffc400 | 0x0 <= rsp
...
0x7fffffffc480 | 0x7fffffffc500 <= rbp
0x7fffffffc488 | 0x31337 
[Code]
leave

코드 실행 후

[Register]
rsp = 0x7fffffffc488
rbp = 0x7fffffffc500
[Stack]
0x7fffffffc400 | 0x0
...
0x7fffffffc480 | 0x7fffffffc500
0x7fffffffc488 | 0x31337 <= rsp
...
0x7fffffffc500 | 0x7fffffffc550 <= rbp

3.ret
음.. 대충 설명하자면 우리가 call코드를 이용해 프로시저를 호출하게 되면 스택프레임이 새로 할당되면서 원래 있던 스택에 call코드 다음에 실행해야될 코드의 주소를 넣는다. 그 후 프로시저가 종료하게 되면 ret명령어를 통해 원래 있던 스택의 주소값을 가져와 call코드 다음부터 정상적으로 실행할 수 있게 해준다.

코드 실행 전

[Register]
rip = 0x401000
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x400005    <= rsp
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret <= rip

코드 실행 후

[Register]
rip = 0x400005
rsp = 0x7fffffffc400
[Stack]
0x7fffffffc3f8 | 0x400005
0x7fffffffc400 | 0x0    <= rsp
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax   <= rip
...
0x401000 | mov rbp, rsp  
...
0x401007 | leave
0x401008 | ret

시스템 콜

현재의 운영체제는 하드웨어 접근권한, 소프트웨어 접근권한들에 대한 해킹으로부터 보호하기 위해 커널모드와 유저모드를 분리하여 관리한다. 하지만 우리는 그러한 권한을 필요로하는 동작을 수행할때가 있는데, 이럴때 커널모드에게 어떤 동작을 요청하는 코드가 바로 시스템 콜(syscall)이다.

  1. syscall
    syscall은 함수이다. 필요한 기능,인자에 대한 정보를 레지스터로 전달하면, 커널에서 이를 읽어서 요청을 처리한다.

필요한 인자는 rdi → rsi → rdx → rcx → r8 → r9 → stack 순으로 전달한다
기능과 인자에 대한 더 자세한 정보는 구글짱을 이용하자.

[Register]
rax = 0x1   
rdi = 0x1   
rsi = 0x401000  
rdx = 0xb   
[Memory]
0x401000 | "Hello Wo"   
0x401008 | "rld"    
[Code]  
syscall 

결과

Hello World

피연산자

피연산자는 총 3가지 종류가 있는데

  • 상수
  • 레지스터
  • 메모리

그중에서 메모리 피연산자는 []의 형태로 표현되며, 앞에 크기 지정자가 올 수 있다 크기 지정자는

  • BYTE (1바이트)
  • WORD (2바이트)
  • DWORD (4바이트)
  • QWORD (8바이트)
profile
보안 공부하는 사람

0개의 댓글