[모각코] Reversing(2~3강)

5 minute read

08-04 수요일

x64 기초

Intro

어셈블리 코드는 기계 코드와 대응되기 때문에, CPU가 어떤 역할을 하고
어떻게 동작하는지를 알아보는 것이 어셈블리 코드를 이해하는데 도움이 됨

CPU는 기본적으로 다음 실행할 명령어를 읽어오고(Fetch)->읽어온 명령어를 해석한 다음(Decode)
->해석한 결과를 실행하는(Excute) 과정을 반복하는 장치

Instruction Cycle - 기계 코드가 실행되는 한 번의 과정

CPU는 Instruction Cycle을 수행하기 위해 각종 명렁어를 저장할 임시 공간
레지스터(Register) 를 사용함

레지스터

레지스터는 특별히 쓰임새가 정해지지 않았지만, 관행적으로 용도를 정해놓고 쓰는 레지스터도 있음

범용 레지스터

CPU는 범용 레지스터를 연습장처럼 사용하며, x64의 볌용 레지스터는 총 16개

rax - 리턴값을 저장하기 위해 쓰임 rcx, rdx, r8, r9 .. - Windows 64bit에서 함수를 호출할 때 필요한 인자들을 순서대로 저장 rsp - 용도가 정해져 있어 스택 포인터로 스택의 가장 위쪽 주소를 가리킴 rip - 다음에 실행될 명령어가 위치한 주소를 가리킴

Data Size

16bit - ax, cx, dx, bx 32bit - eax, ecx, edx, ebx

64bit CPU에서 꼭 8byte 단위로만 값을 저장하는 것이 아니라 어셈블리 코드를 통해 하위 16bit,
하위 8bit만 사용하는 것도 가능하며 이는 어셈블리 코드에서 레지스터 이름으로 ecx, cx를 이용하면 됨

FLAGS

FLAGS는 상태 레지스터로 현재 상태나 조건을 0과 1로 나타내는 레지스터 64개의 비트들 각각이 서로 다른 의미를 지니며, 즉 0번째, 1번째, … 서로 다른 상태를 나타냄

Flag Abbreviation 설명
CF(Carry Flag) 산술 연산 혹은 bit shift/rotate 등의 연산이 일어났을 때, 자리 올림이 생기는 경우 CF의 값이 1
ZF(Zero Flag) 연산의 결과가 0일때 ZF는 1
SF(Sign Flag) 부호가 있는 값의 연산에 쓰이며, 최상위 비트가 0이면 SF=0, 음수가 되면 최상위 비트가 1이고 똑같이 SF=1
OF(Overflow Flag) 부호가 있는 값의 연산에서 CF의 역할

Instruction Format

Opcode (Operation Code)

명령 코드(Opcode) - 명령어에서 실제로 어떤 동작을 할지를 나타내는 부분

  • 기계 코드(Machine Code) 또는 명령 코드(Opcode)

    컴파일러가 만드는 결과물인 바이너리를 구성, CPU가 실제로 수행할 작업을 나타내는 숫자 명령 코드에 따라 피연산자가 필요하기도 함

  • 어셈블리 코드(Assembly Code)

    숫자로 이뤄진 명령 코드를 사람이 이해할 수 있도록 작성된 코드 명령 코드와 1:1로 대응되며, 연산할때 사용할 피연산자도 알아보기 쉬움

    명령코드              어셈블리 코드
    55                   push  rbp
    48 89 e5             mov   rbp,rsp
    48 8d 3d 9f 00 00 00 lea   rdi,[rip+0x9f]
    e8 c6 fe ff ff       call  510 <puts@plt>
    b8 00 00 00 00       mov   eax,0x0
    5d                   pop   rbp
    c3                   ret
    
    • Operand

      명령 코드가 연산할 대상, 명령 코드에 따라 조금씩 다르지만, Intel 방식의 어셈블리를
      읽을 때에는 명령 코드에 따라 연산한 결과를 왼쪽 피연산자에 저장된다 고 이해하는 것이 일반적

    mov   rbp,rsp      ; rbp = rsp
    mov   eax,0x0      ; eax = 0x0
    add   rcx,0x8      ; rcx = rcx+0x8
    dec   rcx          ; rcx = rcx-1
    
    • Operand Types

      상수, 레지스터, 혹은 레지스터가 가리키고 있는 메모리의 어떤 주소

      • Addressing Modes

        레지스터에 있는 값이 피연산자가 되는 것이 아니라 레지스터에 저장된 메모리 주소를
        참조하는 값 이 피연산자가 되는 경우

        즉, 레지스터에 들어있는 값은 메모리 주소로 실제로는 해당 메모리 주소를 참조한 값이
        피연산자로 사용됨

Instructions

  • Data Movement

    • mov

      movsrc에 들어있는 값을 dst로 옮김

    • lea

      lea는 Load Effective Address로, dst에 주소를 저장

  • Arithmetic Operations

    • Unary Instructions

      Instructions 설명
      inc, dec dst의 값을 1증감
      neg dst에 들어있는 값의 부호를 바꿈
      not dst에 들어있는 값의 비트를 반전
    • Binary Instructions

      Instructions 설명
      add dst의 값에 src를 더함
      sub dst의 값에서 src를 뺌
      imul dst의 값에 src를 곱함
      and dst의 값과 src간에 AND 논리연산 결과를 dst에 저장
      or dst의 값과 src간 OR 논리연산 결과를 dst에 저장
      xor dst의 값과 src간 XOR 논리연산 결과를 dst에 저장
    • Shift Instructions

      Instructions 설명
      shl, shr dst의 값을 k만큼 왼쪽이나 오른쪽으로 shift shr의 경우 오른쪽으로 shift 할때 빈 bit에 0이 채워짐
      sal, sar dst의 값을 k만큼 shift하는 것 같지만, 부호가 보전됨.
  • Conditional Operations

    조건문과 같이 코드의 실행 흐름을 제어하는 것과 연관

    • test

      논리연산을 하지만, 결과값을 피연산자에 저장하지 않음 FLAGS 레지스터에 영향을 미침

    • cmp

      마찬가지로 FLAGS 레지스터의 ZF와 CF 플래그에만 영향을 미침

    • jmp, jcc

      jmp - 피연산자가 가리키는 곳으로 점프 jcc - 조건에 따라 점프

  • Stack Operations

    • rsp revisit

      rsp - 스택의 가장 위쪽을 가리키므로, 마지막으로 데이터가 추가된 위치를 저장하는 레지스터

  • Function Prologue/Epilogue

    함수가 시작할 때 에는 rsp 레지스터에 들어있는 주소에서 충분한 값을 뺌(pop)

    함수가 끝날 떄 에는 프롤로그에서 빼준 값만큼 다시 rsp에 더해줌(push)

  • Procedure Call Instructions

    • call

      함수를 실행할 때 쓰임, 피연산자로 실행할 함수의 주소를 받음 call로 호출한 함수고 종료되고 나면 다음 명령어를 실행할 장소로 돌아와야함

    • ret

      호출된 함수가 마지막으로 사용하는 명령어
      함수를 종료한 뒤 Return Address로 돌아가는 역할


puts(“hello world!\n”); -> x86_64 asm

#include <stdio.h>
int main(){
    puts("hello world!\n");
    return 0;
}

x64dbg를 통한 main 함수 일부분

  1. 주소

해당 어셈블리 코드의 시작 주소가 표시

  1. 기계 코드

: 앞에 있는 값은 prefix이고 띄어쓰기 다음에 있는 부분은 어셈블리 코드의 인자 부분

  1. 어셈블리어

    사람이 읽기 쉬운 형태로 표시해 줌

  2. 코멘트

프로그램을 분석하여 알게된 추가적인 정보 표시 2번째 줄에 7FF6ED802220에 있는 문자열에 대한 정보가 표시되는 것을 확인할 수 있음

7FF6ED801000 | 48:83EC 28       | sub rsp,28                          |
7FF6ED801004 | 48:8D0D 15120000 | lea rcx,qword ptr ds:[7FF6ED802220] | 00007FF6ED802220:"hello world!\n"
7FF6ED80100B | FF15 5F110000    | call qword ptr ds:[<&puts>]         |
7FF6ED801011 | 33C0             | xor eax,eax                         |
7FF6ED801013 | 48:83C4 28       | add rsp,28                          |
7FF6ED801017 | C3               | ret                                 |

sub rsp, 28

rsp에서 0x28 만큼 빼 함수 내부에서 사용할 스택의 용량을 확보하는 명령어

lea rcx,qword ptr ds:[7FF6ED802220]

rcx에 0x7FF6ED80220 값을 저장, 이는 주소값이며 x64dbg가 생성한
디스어셈블 결과의 코멘트를 확인하면 puts의 첫째 인자 hello world!\n가 위치한 주소임을 알 수 있음

call qword ptr ds:[<&puts>]

puts를 호출하는 명렁어

xor eax,eax

eax를 0으로 만들어주는 명령어, main함수의 리턴값을 0으로 설정해놨기 때문에
함수의 리턴값을 의미하는 eax 레지스터를 0으로 설정하는 것

add rsp,28

함수 시작시 확보해두었던 스택을 정리하는 명령어

ret

함수의 실행을 마치고 리턴하기 위해 사용하는 명령어

64비트 windows의 함수 호출 규약

windows의 함수 호출 규약은 다음과 같은 순서로 첫 4개의 인자를 받음

  • rcx(ecx, cx, …)
  • rdx(edx, dx, …)
  • r8(r8d, r8w, …)
  • r9(r9d, r9w, …)

이후 5번째 인자부터는 스택에 넣게 되어 함수의 리턴값은 rax(eax, ax, …)에 저장

8개의 인자를 받는 함수의 디스어셈블 결과

7FF611801040 | sub rsp,48                            |
7FF611801044 | mov dword ptr ss:[rsp+38],8           |
7FF61180104C | mov dword ptr ss:[rsp+30],7           |
7FF611801054 | mov dword ptr ss:[rsp+28],6           |
7FF61180105C | mov dword ptr ss:[rsp+20],5           |
7FF611801064 | mov r9d,4                             |
7FF61180106A | mov r8d,3                             |
7FF611801070 | mov edx,2                             |
7FF611801075 | mov ecx,1                             |
7FF61180107A | call consoleapplication1.7FF611801000 |
7FF61180107F | xor eax,eax                           |
7FF611801081 | add rsp,48                            |
7FF611801085 | ret                                   |

참고자료

dreamHack Reverse Engineering

https://dreamhack.io/lecture