ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • dreamhack stage 2 - x86 Assembly
    보안/SYSTEM HACKING 2024. 3. 15. 12:46

    STAGE 2

    x86 Assembly

    x86 Assembly:Essential Part(1)

    0. 서론

    기계어(Machine Code)라는 언어가 있다.

    시스템 해커는 컴퓨터의 언어로 작성된 소프트웨어에서 취약점을 발견해야 한다. 그런데 기계어는 0과 1로만 구성되어있어 이해하기 어렵다.

    --> David Wheeler 가 EDSAC을 개발하면서 어셈블리 언어와 어셈믈러를 고안했다.

    어셈블러는 어셈블리어로 코드를 작성하면 컴퓨터가 이해할 수 있는 기계어로 코드를 치환해준다.

    --> 기계어를 어셈블리 언어로 번역하는 역어셈블러를 개발했다.

    기계어로 구성된 소프트웨어를 역어셈블러에 넣으면 어셈블리 코드로 번역된다.


    1. 어셈블리어와 x86-64

    • 어셈블리 언어

    : 컴퓨터의 기계어와 치환되는 언어

    - 기계어가 여러 종류라면 어셈블리어도 여러 종류여야 한다.

    - CPU에 사용되는 ISA는 종류가 매우 다양하다.

    (여기에서는 x64 어셈블리어만 소개한다.)

    • x64 어셈블리 언어

    - 기본 구조: 명령어(Operation Code, Opcode) - 동사에 해당

    피연산자(Operand) - 목적어에 해당

    으로 구성된다.

    기본 구조에 대해 자세하게 알아보자.

    - 명령어

    - 피연산자

    종류) 1. 상수(Immediate Value)

    2. 레지스터(Register)

    3. 메모리(Memory)

    - 메모리 피연산자는 []로 둘러싸인 것으로 표현된다.

    - 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.

    타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있고 각각 1, 2, 4, 8 바이트의

    크기를 지정한다.

     

     

    + 자료형 WORD의 크기가 2바이트인 이유

    인텔은 WORD 자료형의 크기를 16비트로 유지했다. WORD 자료형의 크기를 변경하면 기존의 프로그램들이 새로운 아키텍처와 호환되지 않을 수 있기 때문이다. 그래서 인텔은 기존에 사용하던 WORD의 크기를 그대로 유지하고, DWORD(Double Word, 32bit)와 QWORD(Quad Word, 64bit)자료형을 추가로 만들었다.


    2. x86-64 어셈블리 명령어

    • 데이터 이동

    데이터 이동 명령어: 어떤 값을 레지스터/메모리에 옮기도록 지시한다.

    • 산술 연산

    산술 연산 명령어: 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다.

     
     

    • 논리 연산

    논리 연산 명령어: and, or, xor, neg 등의 비트 연산을 지시한다.

    연산은 비트 단위로 이루어진다.

    - and dst, src: dst 와 src의 비트가 모두 1이면 1, 아니면 0

    - or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0

    - xor dst, src: dst와 src의 비트가 서로 다르면 1, 아니면 0

    - not op: op의 비트 전부 반전

     

    • 비교

    비교 명령어: 두 피연산자의 값을 비교하고 플래그를 설정한다.

    - cmp op1, op2: 두 피연산자를 빼서 op1과 op2의 대소를 비교, 연산의 결과는 대입하지 않음

    - test op1, op2: 두 피연산자에 AND 비트연산을 취해 op1과 op2를 비교, 연산의 결과는 대입하지 않음

    • 분기

    분기 명령어: rip 를 이동시켜 실행 흐름을 바꾼다.

    jmp addr: addr 로 rip를 이동 시킨다.

    je addr: 직전에 비교한 두 피연산자가 같으면 점프한다. (jump if equal)

    jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)


    3. 결론

    1. 데이터 이동 연산자

    - mov dst, src: src의 값을 dst에 대입

    - lea dst, src: src의 유효 주소를 dst에 대입

    2. 산술 연산

    - add dst, src: src의 값을 dst에 더함

    - sub dst, src: src의 값을 dst에서 뺌

    - inc op: op의 값을 1 더함

    - dec op: op의 값을 1 뺌

    3. 논리 연산

    - and dst, src: dst와 src가 모두 1이면 1, 아니면 0

    - or dst, src: dst와 src중 한 쪽이라도 1이면 1, 아니면 0

    - xor dst, src: dst와 src가 다르면 1, 같으면 0

    - not op: op의 비트를 모두 반전

    4. 비교

    - cmp op1, op2: op1에서 op2를 빼고 플래그를 설정

    - test op1, op2: op1과 op2에 AND 연산을 하고, 플래그를 설정

    5. 분기

    - jmp addr: addr로 rip 이동

    - je addr: 직전 비교에서 두 피연산자의 값이 같을 경우 addr로 rip 이동

    - jg addr: 직전 비교에서 두 피연산자 중 전자의 값이 더 클 경우 addr로 rip 이동


    STAGE 2

    x86 Assembly

    x86 Assembly:Essential Part(2)

    0. 서론

    • 스택, 프로시저, 시스템콜 관련 어셈블리를 소개할 것이다.
    • 스택: push, pop
    • 프로시저: call, leave, ret
    • 시스템콜:syscall

    1. x86-64 어셈블리 명령어 Pt.2

    • Opcode: 스택

    x64 아키텍쳐에서는 명령어를 사용해 스택을 조작할 수 있다.

    - push val: val을 스택 최상단에 쌓음

    - pop reg: 스택 최상단의 값을 꺼내서 reg에 대입

     

    • Opcode: 프로시저

    - 프로시저(Procedure): 특정 기능을 수행하는 코드 조각

    프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체 가능

    -> 전체 코드를 줄일 수 있음

    기능별로 이름을 붙일 수 있음 -> 코드의 가독성 크게 높임

    - 호출(call): 프로시저를 부르는 행위

    프로시저 호출: call 다음의 명령어 주소(반환 주소)를 스택에 저장하고 프로시저로 rip 이동

    - 반환(return): 프로시저에서 돌아오는 것

     

    - x64 어셈블리언어에서는 프로시저의 호출과 반환을 위한 call, leave ret 명령어가 있음

    - call addr: addr에 위치한 프로시져 호출 / 연산: push return_address, jmp addr

    - leave: 스택프레임 정리 / 연산: mov rsp/rbp, pop rbp

    - ret: return address로 반환 / 연산: pop rip

     

     
     

    call / leave/ret

    • 스택 프레임의 할당과 해제
    1. func 함수를 호출한다. 다음 명령어의 주소인 0x400005 는 스택에 push된다.
    2. 기존의 스택 프레임을 저장하기 위해 rbp를 스택에 push 한다.
    3. 새로운 스택 프레임을 만들기 위해 rbp를 rsp 로 옮긴다.
    4. 새로 만든 스택 프레임의 공간을 확장하기 위해 rsp를 0x30만큼 뺀다.
    5. 할당한 스택 프레임에 지역변수를 할당한다.
    6. 스택 프레임 위에서 여러 연산을 수행한다.
    7. 저장해뒀던 rbp를 꺼내 원래의 스택 프레임으로 돌아간다.
    8. 저장해뒀던 반환 주솔르 꺼내어 원래의 실행 흐름으로 돌아간다.
    9. 기존의 스택프레임과 함께 원래의 실행 흐름을 이어간다.

    • 운영체제의 권한

    현대 운영체제는 컴퓨터 자원의 효율적인 사용, 사용자에게의 편리한 경험 제공을 위해 복잡한 동작을 한다.

    운영체제는 연결된 모든 하드웨어/ 소프트웨어에 접근, 제어할 수 있고 해킹으로부터 보호하기 위해 커널모드와

    유저모드로 권한을 나눈다.

    - 커널 모드

    : 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어를 부여하는 권한

    - 진행되는 작업: 파일시스템 입력/출력, 네트워크 통신 등 저수준의 작업

    - 시스템의 모든 부분 제어 가능 -> 해커가 진입하면 무방비 상태

    - 유저 모드

    : 운영체제가 사용자에게 부여하는 권한

    - 진행되는 작업: 브라우저 이용해서 시청, 게임, 리눅스에서 루트 권한으로 사용자 추가 ..

    - 유저 모드에서는 해킹이 발생해도 권한 보호 가능

    • Opcode: 시스템 콜(System call, syscall)

    : 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용

    - 소프트웨어 대부분은 커널의 도움이 필요하다. 도움이 필요하다는 요청 == 시스템 콜

    - 유저 모드의 소프트웨어가 필요한 도움을 요청-> 커널이 요청한 동작 수행 -> 유저에게 결과 반환

    - x64 아키텍쳐: 시스템콜을 위한 syscall 명령어 존재

     

    시스템 콜은 함수이다.

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

    리눅스에서는 rax로 요청을 나타내고 필요한 인자를 전달한다.

     

    - 시스템 콜(syscall)의 요청과 인자순서

    - 요청: rax

    - 인자 순서: rdi - rsi - rdx- rcx - r8 - r9 - stack

    • x64 syscall 테이블

     

    1. rax가 0x1일 때, 커널에 write 시스템콜을 요청

    2. rdi, rsi, rdx가 0x1, 0x401000, 0xb 이므로 커널은 write(0x1, 0x401000, 0xb)를 수행

    3. write함수의 각 인자는 출력 스트림, 출력 버퍼, 출력 길이를 나타냄

    4. 여기서 0x1은 stdout이며, 이는 일반적으로 화면을 의미

    5. 0x401000에는 Hello World가 저장되어 있고, 길이는 0xb로 지정되어 있음

    6. 화면에 Hello World가


    2. 에필로그

    스택

    push val : rsp를 8만큼 빼고, 스택의 최상단에 val을 쌓습니다.

    pop reg: 스택 최상단의 값을 reg에 넣고, rsp를 8만큼 더합니다.

    프로시저

    call addr: addr의 프로시저를 호출합니다.

    leave: 스택 프레임을 정리합니다.

    ret: 호출자의 실행 흐름으로 돌아갑니다.

    시스템 콜:

    syscall: 커널에게 필요한 동작을 요청합니다.


    STAGE 2

    x86 Assembly

    Quiz: x86 Assembly 1

    1. mov dl, BYTE PTR[rsi+rcx] = dl에 rsi+rcx의 값이 들어간다.

    rsi+rcx = 0x400000 -> dl = 0x400000

    2. xor dl, 0x30 = 0x30과 dl 을 xor 한다.

    dl=0x400000 = 0x67 0x55 0x5c 0x53 0x5f 0x5d 0x55 0x10

    각각 xor 하면 welcome 이 나온다.

    3. mov BYTE PTR[rsi+rcx], dl = dl에 있는 값이 rcx+rsi의 주소값으로 전달된다.

    4. inc rcx = rcx값을 1만큼 증가시킨다.

    rcx=0 -> rcx= 0x01

    5. cmp rcx, 0x19 = rcx와 0x19를 비교한다. 처음 비교결과 rcx가 더 작다.

    6. operand1이 크면 jmp 1을 한다. 0x19보다 커질 때까지 code1로 이동해 계속 진행한다.

    7. 1로 이동한다.

     


    STAGE 2

    x86 Assembly

    Quiz: x86 Assembly 2

    1. main 함수에서

    - push rbp: rbp를 스택에 push한다.

    - mov rbp, rsp: rsp를 rbp로 옮긴다.

    - mov esi, 0xf: esi의 값을 0xf 로 한다.

    - mov rdi, 0x400500: rdi의 값을 0x400500으로 한다.

    - call 0x400497<write_n>: 함수 write_n을 호출한다.

    - mov eax, 0x0: eax의 값을 0x0으로 한다.

    - pop rbp: rbp를 스택에서 pop한다.

    - ret: 호출 주소로 돌아간다.

    2. write_n 함수에서

    - push rbp: rbp를 스택에 push한다.

    - mov rbp, rsp: rsp를 rbp로 옮긴다.

    - mov QWORD PTR [rbp-0x8], rdi: rdi를 rbp-0x8 주소에서 8바이트만큼 대입한다.

    - mov DWORD PTR [rbp-0xc], esi: esi를 rbp-0xc주소에서 4바이트만큼 대입한다.

    - xor rdx, rdx: rdx를 자기자신과 xor하여 rdx에 저장한다.

    - mov edx, DWORD PTR [rbp-0xc]: edx에 rbp-0xc 주소를 4바이트만큼 대입한다.

    - mov, rsi, QWORD PTR [rbp-0x8]: rsi에 rbp-0x8 주소를 8바이트만큼 대입한다.

    - mov rdi, 0x1: rdi의 값을 0x1로 저장한다.

    - mov rax, 0x1: rax의 값을 0x1로 저장한다.

    - syscall: 시스템 콜

    - pop rbp: rbp를 스택에서 pop한다.

    - ret: 호출 주소로 돌아간다.

Designed by Tistory.