Assembly

  • 기계어로 1대1 대응 가능한 언어로, human readable 한 언어 중 가장 기계어에 가까운 언어이다. 기계어로 컴파일 직전에 어셈블리어로 변환을 거친다.
  • operation code(명령어) 와 operand(피연산자) 로 구성된다.
  • 명령어는 데이터 이동, 산술연산, 논리연산, 비교, 분기, 스택, 프로시저, 시스템콜의 종류가 있다.
  • 피연산자 자리에는 상수(Immediate Value), 레지스터(Register), 메모리(Memory)가 올 수 있다.
    • 숫자를 넣으면 상수이다.
    • [] 로 둘러싸인 숫자는 메모리이다.
    • 메모리 피연산자 앞에는 메모리의 크기를 나타내는 크기 지정자(Size Directive)가 붙을 수 있다.
      • BYTE: 8bit
        • BYTE PTR rax : rax 레지스터의 데이터를 1바이트만큼 참조
      • WORD: 16bit
        • WORD PTR [0x8048000] : 0x8048000의 데이터를 2바이트만큼 참조
      • DWORD: 32bit
      • QWORD: 64bit

명령어

  1. mov
    • “값"을 레지스터리나 메모리에 저장하는 명령
    • mov dst, src : src 값을 dst에 덮어씀
      • mov rdi, rsi : rsi의 값을 rdi에 대입
      • mov QWORD PTR[rdi], rsi : rsi의 값을 rdi가 가리키는 주소에 대입
      • mov QWORD PTR[rdi + 8 * rcx], rsi : rsi의 값을 (rdi + 8 * rcx)가 가리키는 주소에 대입
    • dst = 레지스터, src = 레지스터 : src가 가리키는 주소의 값을 dst가 가리키는 주소의 값에 덮어씀
    • dst = 메모리, src = 레지스터 : src가 가리키는 주소의 값을 dst가 가리키는 주소의 값에 덮어씀
    • dst = 레지스터, src = 메모리 : src가 가리키는 주소의 값을 dst가 가리키는 주소의 값에 덮어씀
    • dst = 메모리, src = 메모리 : 불가능
    • mov dst, [mem + 4] : mem + 4 주소에 저장된 값을 dst에 덮어씀
    • dst 값으로는 주소나 포인터가 올 수 있다.
  2. lea
    • “주소"를 레지스터리나 메모리에 저장하는 명령
    • lea dst, src : src값을 dst에 덮어씀 (src는 주소값)
      • lea rsi, [rbx + 8 * rcx] : (rbx + 8 * rcx) 를 rsi에 대입
    • lea dst, [mem + 4] : mem 값에 4를 더한 값을 dst에 덮어씀
  3. add
    • add dst, src : dst 에 있는 값에 src 값을 더해 dst에 덮어씀
    • dst는 주소, src는 값
  4. sub
    • sub dst, src: : dst 에 있는 값에 src 값을 빼고 dst 주소에 덮어씀
    • dst는 주소, src는 값
  5. inc
    • inc op : op 에 있는 값을 1 증가시킴
    • op는 주소
  6. dec
    • dec op : op 에 있는 값을 1 감소시킴
    • op는 주소
  7. and
    • and dst, src : src와 dst 값을 and 연산한 결과를 dst에 저장
  8. or
    • or dst, src : src와 dst 값을 or 연산한 결과를 dst에 저장
  9. xor
    • xor dst, src : src와 dst 값을 xor 연산한 결과를 dst에 저장
  10. not
  • not op : op 값을 not 연산한 값을 op에 저장
  1. comp
    • cmp rax, rbx : rax 값과 rbx 값을 비교한 후, 결과에 따라 플래그 설정
    • if rax == rbx: ZF = 1
  2. test
    • test rax, rbx : rax 값과 rbx 값을 and 연산 후, 결과에 따라 플래그 설정
  3. jmp
    • jmp addr : addr 주소로 rip를 이동한다.
  4. je
    • je addr : 직전에 비교한 cmp rax rbx 연산에서 rax == rbx 라면 addr로 rip 를 이동한다.
  5. jg
    • jg addr : 직전에 비교한 cmp rax rbx 연산에서 rax > rbx 라면 addr로 rip 를 이동한다.
  6. push
    • push val : 스택의 최상단에 ‘val’ 값을 집어넣는다.
    • rsp 를 한칸 위로 옮기고, 그 위치에 ‘val’을 대입한다.
    • rsp -= 8; [rsp] = val 동작과 동일하다.
    • push val 형태로는 4byte 데이터밖에 주입할 수 없으므로, 4byte를 초과하는 데이터를 주입할 때는 값을 레지스터에 대입하고, 레지스터를 push한다.
         mov rax 0x0102030405060708
         push rax
      
  7. pop
    • pop rax : 스택의 최상단에 있는 값을 ‘rax’ 주소에 대입한다.
    • rsp 위치의 값을 반환하고, rsp 를 한칸 밑으로 옮긴다.
    • rsp += 8; reg = [rsp-8] 동작과 동일하다.
  8. call
    • call addr ‘addr’ 위치의 프로시저를 호출
    • ‘push’ 명령과 ‘jump’ 명령으로 구현할 수 있다.
      • 스택에 다음 실행 주소를 push한다. (push rip + 8)
      • rip를 실행시키고 싶은 명령어가 적힌 주소로 이동한다. (jump)
  9. leave
  • rsp를 rbp + 8 위치로 이동한다.
  • rbp도 갱신한다.
  • mov rsp, rbp; pop rbp 명령과 동일하다.
  1. ret
    • rip를 rsp가 가리키는 스택의 주소에 담긴 값으로 이동한다.
    • pop rip 명령과 동일하다.

시스템콜

  • 운영체제는 하드웨어 및 소프트웨어를 총괄하며, 접근 권한을 제한하여 해킹으로부터 컴퓨터를 보호하기 위해 커널 모드와 유저 모드로 권한을 분리한다.
  • 시스템 콜은 유저모드에서 시스템에게 커널 모드에서 실행할 수 있는 동작들을 요청하는 동작이다.
    • 유저가 시스템 콜을 호출하면 커널은 이를 실행하고, 결과를 유저에게 반환한다.

레지스터

범용 레지스터

  1. rax : (Extended Accumulator Register) 사칙연산에서 자동으로 피연산자의 값이 저장된다.
    • 논리 연산(덧셈, 뺄셈 등)의 결과값이 저장된다.
    • 피연산자와 별개로 데이터가 저장된다.
    • 시스템 콜의 실질적인 번호를 가리킴
    • 시스템 콜의 반환값도 rax에 저장됨
    • x64구조에서 rax 를 사용하고, x86구조에서는 eax 를 사용했다.
    • ax : eax가 사용되기 이전, CPU의 word가 16bit 일 때 사용되던 레지스터
    • 큰 의미는 없지만 관습처럼 사용되며 eax에서 하위 2byte를 자른 값을 나타낸다.
    • ax 는 다시 ah와 al로 한 byte씩 나뉜다.
      • ah : ax에서 상위 1byte
      • al : ax에서 하위 1byte
      byte_8byte_7byte_6byte_5byte_4byte_3byte_2byte_1
      rax_8rax_7rax_6rax_5rax_4rax_3rax_2rax_1
      ----eax_4eax_3eax_2eax_1
      -------ax_2
      -------ah
  2. rbx(ebx) : (Extended Base register)메모리 주소를 저장하는 용도로 사용
  3. rcx(ecx) : (Extended Counter Register)CPU loop counter
  4. rdx(edx) : 시스템 콜 실행 시 세 번째 인자의 주소 / (Extended Data Register)
  5. rsi : 시스템 콜 실행시 두 번째 인자의 주소 / (source index) 데이터 이동시 원본을 가리키는 주소
    • x64구조에서 rsi 를 사용하고, x86구조에서는 esi 를 사용했다.
  6. rdi : 함수 실행시 첫 번째 인자의 주소 / 시스템 콜 실행시 첫 번째 인자의 주소 / (destination index) 데이터 이동시 목적지를 가리키는 주소
    • x64구조에서 rdi 를 사용하고, x86구조에서는 edi 를 사용했다.
  7. rbp : (Base Register Pointer)스택 복귀 주소
    • rbp 주소에는 함수가 종료되고 함수를 호출한 함수(caller) 의 스택 프레임으로 rbp를 이동하기 위한 주소 SFP(Stack Frame Pointer) 가 저장된다. 함수 호출시 호출자(caller)의 SFP를 stack에 넣고, 실행된 함수가 끝날 때 이를 pop하여 함수가 호출된 코드 라인으로 복귀할 수 있다.
    • 즉, 함수 호출 시마다 push rbp 코드를 보게 될 것이다.
    • x64구조에서 rbp 를 사용하고, x86구조에서는 ebp 를 사용했다.
    • ebp : 스택 프레임 최하단의 주소값 (Base pointer register)
      • x86에서 사용하는 값으로, x64에서는 rbp로 대체된다.
      • 새로운 함수가 호출 될 경우, EBP 값이 스택에 push되어, 이전 함수의 EBP값이 스택에 쌓이게 된다.
  8. rsp : 스택의 최상단의 주소
    • x64구조에서 rsp 를 사용하고, x86구조에서는 esp 를 사용했다.
    • esp : 스택 최상단의 주소값 (Stack pointer register)
      • PUSH, POP, SUB, CALL 명령을 수행 할 때 마다 자동으로 변경된다.
      • PUSH, POP 의 기준이 되는 포인터이다.
  • r8 ~ r15까지는 따로 명칭이 없다.
  • 각 레지스터들은 64비트 일때 하위 32비트(=32bit 시스템에서 사용하는 명칭), 하위 16bit, 하위 8bit 를 칭하는 명칭이 각각 존재한다.
    64비트하위32비트하위16비트하위8비트
    raxeaxaxal
    rbxebxbxbl
    rcxecxcxcl
    rdxedxdxdl
    rsiesisisil
    rdiedididil
    rbpebpbpbpl
    rspespspspl
    r8r8dr8wr8b
    r9r9dr9wr9b
    r15r15dr15wr15b

세그먼트 레지스터

  • cs, ss, ds, es, fs, gs
    • cs : code segment
    • ds : data segment
    • es : extra segment
    • fs, gs : 앞선 세 개의 segment를 만들고 여유분 두개를 추가한 것. cs/ds/es는 CPU가 명확한 사용 용도를 가지는 반면 fs/gs는 정해진 용도가 없어 OS가 임의로 사용 가능
      • 리눅스에서는 fs segment register를 Thread Local Storage(TLS) 의 포인터로 사용한다.

명령어 포인터 레지스터

  • Instruction Pointer Register, IP
  • rip : 현재 명령 실행 주소
    • x64구조에서 rip 를 사용하고, x86구조에서는 eip 를 사용했다.

플래그 레지스터

  • CF(Carry Flag) : 부호 없는 수의 연산 결과가 비트의 범위를 넘을 경우 1로 세팅
  • ZF(Zero Flag) : 연산의 결과가 0일 경우 1로 세팅
  • SF(Sign Flag) : 연산의 결과가 음수일 경우 1로 세팅
  • OF(Overflow Flag) : 부호 있는 수의 연산 결과가 비트 범위를 넘을 경우 1로 세팅

프로시저

  • 특정 주소의 명령어를 실행하도록 하는 코드이다.
  • 프로시저를 사용하면 가독성이 높아지고, 반복되는 코드를 절약할 수 있다.

Section

  • object 파일 안에서 재배치 될 수 있는 가장 작은 단위를 섹션(section) 이라 한다.
  • objdump -h 로 목적파일의 Section을 확인할 수 있다.

스택프레임

  • 각 함수들은 실행되면서 지역변수와 임시 값들을 저장해야 하는데, 이 값들은 스택 영역에 저장된다.

  • 하지만 특정 함수가 사용하고 있는 스택 영역을 다른 함수가 침범하여 사용하지 못하게 하기 위해 함수별로 스택 프레임을 두고 스택 영역을 공용으로 사용하지 못하게 관리한다.

  • 함수가 호출될 떄 마다 스택프레임이 형성되며, 스택프레임 형성을 어셈블리어로 표현하면 다음과 같다.

    push EIP  # 함수 완료 후 실행할 코드의 주소를 스택에 저장
    push EBP  #  함수 완료 후 EBP 포인터를 복구시킬 값을 스택에 저장한다. (이를 SFP라 한다.)
    mov EBP ESP # 스택의 top 주소를 EBP에 대입한다. (EBP를 갱신하여 새로운 스택 프레임의 base를 세팅한다.)
    sub ESP, VALUE  # 지역변수가 설정될 영역만큼(VALUE) ESP 주소를 옮긴다. (EBP - ESP 만큼이 지역변수 영역)
    
    • x64라면 EIP 대신 rip, EBP 대신 rbp, ESP 대신 rsp를 사용한다.
  • 구성된 스택 프레임은 아래와 같이 형성된다.

    ----------  <- ESP(rsp)
    지역변수
    ----------  <- EBP(rbp)
    SFP
    ----------  <- EBP(rbp) + 0x04 (x64라면 +0x08)
    return address -> 함수 종료시 EIP(rip) 에 해당 위치의 값 대입
    ----------
    
  • 만약 Stack canary 기법이 적용되었다면 아래와 같이 canary가 추가된다.

    ----------  <- ESP(rsp)
    지역변수
    ----------  <- EBP(rbp) - 0x04 (x64라면 -0x08)
    Canary (4byte / x64라면 8byte)
    ----------  <- EBP(rbp)
    SFP
    ----------  <- EBP(rbp) + 0x04 (x64라면 +0x08)
    return address -> 함수 종료시 EIP(rip) 에 해당 위치의 값 대입
    ----------
    
  • 함수가 종료되면 다음 절차가 수행된다.

    mov ESP, EBP  # 지역변수 공간을 해제
    pop EBP  # SFP에 정보를 가져와 ebp에 대입
    RET  # pop EIP; jmp EIP 동작을 수행한다.
    
  • 리눅스에서 C 언어로 프로그램을 짜면 보통 libc 라이브러리를 호출하게 되고, 이 경우 main 함수는 __libc_start_main 함수에서 호출된다.

    • main 함수의 스택 프레임을 벗어나 return 주소로 가게 되면 __libc_start_main 함수의 스택 프레임으로 이동할 수 있다.
    • main 함수의 스텍 프레임의 return address 가 __libc_start_main + A 라면, “main 함수의 return address” - “libc 라이브러리에서 __libc_start_main 함수의 offset” - “A” = libc_base 가 된다. 이 사실은 exploit 에 사용될 수 있다.

.asm to bin

  • .asm 파일을 바이트 코드로 변경하려면 “nasm” 이라는 모듈을 사용하면 된다.
  • nasm -f elf YOUR_FILE.asm 명령으로 .o 파일을 생성할 수 있다.
    • 만약 구동중인 컴퓨터가 x86-64 구조라면 elf 대신 elf64를 입력한다.
    • 컴퓨터 구조별 명령은 nasm -fh 로 확인이 가능하다.
    • 생성된 .o 파일은 objdump -d YOUR_OBJ.o 명령으로 내용 확인이 가능하다.
    • 만약 assembly 파일 안에 main 함수를 정의하였다면 gcc YOUR_OBJ.o -o YOUR_OUT.out 명령어로 실행 가능한 ELF 파일을 생성할 수도 있다.
  • objcopy --dump-section .YOUR_SECTION=YOUR_BIN.bin YOUR_OBJ.o 명령으로 .o 파일을 .bin 파일로 변환할 수 있다.
    • section .text 로 어셈블리 영역이 시작된다면 YOUR_SECTION=text 가된다.

    • ex) test.asm 파일이 아래와 같은 경우,

         section .text  ; 아래에 text 라는 section을 정의한다.
         global main    ; main 함수를 전역으로 선언한다.
         main:          ; main 함수의 내용을 구현한다.
         push 0x00      ; 구현부
         ...
      
    • nasm -f elf64 test.asm 을 수행한 후, objcopy --dump-section .text=test.bin test.o 을 수행하면 test.bin 파일을 얻어낼 수 있다.

    • 생성한 바이너리 파일을 xxd YOUR_BIN.bin 명령으로 내용을 출력할 수 있다. 이는 objdump -s YOUR_OBJ.o 명령의 출력 형태와 동일하다