imnyang's workspace

뒤로

Reverse Engineering#

역설계는 이미 만들어진 장치가 어떻게 작동하고 동작하는지 분석을 통해 원리를 이해하는 과정을 뜻해요.

컴파일 과정#

Compile이란?#

컴파일은 인간이 이해하기 쉬운 언어인 소스코드를 컴퓨터가 이해하기 쉬운 언어인 기계어로 변환하는 과정을 말해요.

보통 소스 파일에서 실행 파일로 변환돼요.

Screenshot_20260514_224317.png

Screenshot_20260514_224345.png

옛날 옛적에 main.c라는 소스파일이 있었어요.

#include <stdio.h>

int main() {
    printf("Hello, World!");
    return 0;
}
c

이 소스파일을 컴퓨터는 알리가 없기에 일단 전처리를 통해 컴퓨터가 이해할 수 있는 형태로 변환해요.
이 과정에선 전처리기는 소스 파일에서 #으로 시작하는 모든 전처리기 지시자를 해석합니다. 이 과정에서 실제 논리적인 연산이 수행되는 것이 아니라, 단순한 텍스트의 치환과 확장이 일어나요.

imnyang@mizuki:workspaces/temp/layer7 > gcc -E main.c -o main.i
imnyang@mizuki:workspaces/temp/layer7 > cat main.i
# 0 "main.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/nix/store/fbbw928argckfii0j322346ihmllg7a7-glibc-2.42-61-dev/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "main.c"
# 1 "/nix/store/fbbw928argckfii0j322346ihmllg7a7-glibc-2.42-61-dev/include/stdio.h" 1 3 4
# 28 "/nix/store/fbbw928argckfii0j322346ihmllg7a7-glibc-2.42-61-dev/include/stdio.h" 3 4
# 1 "/nix/store/fbbw928argckfii0j322346ihmllg7a7-glibc-2.42-61-dev/include/bits/libc-header-start.h" 1 3 4
# 33 "/nix/store/fbbw928argckfii0j322346ihmllg7a7-glibc-2.42-61-dev/include/bits/libc-header-start.h" 3 4
...
fish

좋아요 멋져보여요. 그리고 이것을 어셈블리어로 컴파일해요.
이 과정에선 컴파일러는 언어 문법에 맞게 코드가 작성되었는지 확인해요.
컴파일러는 곧바로 어셈블리어로 바꾸기 전에, 기계 독립적인 중간 코드를 생성하여 최적화를 수행해요.
최적화된 코드를 바탕으로 특정 CPU 아키텍처가 이해할 수 있는 어셈블리어 문장으로 변환해요.

imnyang@mizuki:workspaces/temp/layer7 > gcc -S main.i -o main.s
imnyang@mizuki:workspaces/temp/layer7 > bat main.s
─────┬─────────────────────────────────────────────────────────────────────────────────────────────────────
File: main.s
─────┼─────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │     .file   "main.c"
   2.text
   3 │     .section    .rodata
   4.LC0:
   5 │     .string "Hello, World!"
   6.text
   7 │     .globl  main
   8 │     .type   main, @function
   9main:
  10.LFB0:
  11 │     .cfi_startproc
  12 │     pushq   %rbp
  13 │     .cfi_def_cfa_offset 16
  14 │     .cfi_offset 6, -16
  15movq    %rsp, %rbp
  16 │     .cfi_def_cfa_register 6
  17 │     leaq    .LC0(%rip), %rax
  18movq    %rax, %rdi
  19 │     movl    $0, %eax
  20call    printf@PLT
  21 │     movl    $0, %eax
  22 │     popq    %rbp
  23 │     .cfi_def_cfa 7, 8
  24ret
  25 │     .cfi_endproc
  26.LFE0:
  27 │     .size   main, .-main
  28 │     .ident  "GCC: (GNU) 15.2.0"
  29 │     .section    .note.GNU-stack,"",@progbits
─────┴─────────────────────────────────────────────────────────────────────────────────────────────────────
asm

좋아요. 하지만 아직도 컴퓨터가 이해하기엔 어려운 형태에요.
이것을 기계어로 변환합니다. 이 과정을 어셈블 단계라고 불러요.
이 과정에선 어셈블러가 사람이 읽을 수 있는 텍스트 형태의 어셈블리어 코드를 CPU가 직접 해독할 수 있는 0과 1의 이진 바이너리 형태로 번역해요.
이 단계의 결과물로 오브젝트 파일이 만들어집니다. 이 파일은 기계어로 작성되어 있지만, 그 자체로는 아직 완전한 실행 파일이 아니에요.

imnyang@mizuki:workspaces/temp/layer7 > gcc -c main.s -o main.o
imnyang@mizuki:workspaces/temp/layer7 > bat main.o | head -n10
UH��H�H�Ǹ��]�Hello, World!GCC: (GNU) 15.2.0 GNU��zRx
Z                                                    C
main.cmainprintf���������������� .symtab.strtab.shstrtab.rela.text.data.bss.rodata.comment.note.GNU-stack.note.gnu.property.rela.eh_frame @�0
                                    &__1_90mB�R�j�e@�
                                                               ��
                                                                       x�t
fish

오 좋아요. 제가 이해할 순 없게 되었지만 컴퓨터가 이해할 수 있으면 상관 없어요.
그리고 나서 링킹 과정을 거쳐 실행 파일을 만들어 볼거에요.
이 과정에선 링커가 각 오브젝트 파일에는 함수나 변수의 이름인 심볼이 들어 있는데, 이 심볼을 해석하여 실행 파일의 메모리 레이아웃을 구성해요.

imnyang@mizuki:workspaces/temp/layer7 > gcc main.o -o main
imnyang@mizuki:workspaces/temp/layer7 > bat main | head -n10
@@@@�SS��ee   88�-�=�=hp�-�=�� � � @!!!  S�td� � � @P�td   ,,Q�tdR�td�-�=�=PP/nix/store/fjkx1l5cnskzrqacf08z7i8z17256w0j-glibc-2.42-61/lib/ld-linux-x86-64.so.2��e�m� "� � "__libc_start_main__cxa_finalizeprintflibc.so.6GLIBC_2.2.5GLIBC_2.34/nix/store/fjkx1l5cnskzrqacf08z7i8z17256w0j-glibc-2.42-61/lib:/nix/store/si4q3zks5mn5jhzzyri9hhd3cv789vlm-gcc-15.2.0-lib/lib_ITM_deregisterTMCloneTable__gmon_start___ITM_registerTMCloneTable)ui    3���?�0��@�?????@��H�H��/H��t��H���5�/�%�/@�%�/h������%�/f���1�I��^H��H���PTE1�1�H�=��K/�f.�H�=�/H��/H9�tH�./H��t   �����H�=a/H�5Z/H)�H��H��?H��H�H��tH��.H����fD�����=/u+UH�=�.H��t
                    H�=�.�)����d�����.]������w���UH��H��H�Ǹ������]���H�H��Hello, World!(
                                                                                            ���\,����<���D%����zRx
        J
Z        �?;*3$"\���t����A�C
0GNU����GNU
0�)J
X��=p���o�p�


9�@� �"�� ����=� ��?� T @2@9X?R@_ n@{ � @X��
Scrt1.o__abi_tagcrtbeginS.oderegister_tm_clones__do_global_dtors_auxcompleted.0__do_global_dtors_aux_fini_array_entryframe_dummy__frame_dummy_init_array_entrymain.ccrtendS.o__FRAME_END___DYNAMIC__GNU_EH_FRAME_HDR_GLOBAL_OFFSET_TABLE___libc_start_main@GLIBC_2.34_ITM_deregisterTMCloneTable_edata_finiprintf@GLIBC_2.2.5__data_start__gmon_start____dso_handle_IO_stdin_used_end__bss_startmain__TMC_END___ITM_registerTMCloneTable__cxa_finalize@GLIBC_2.2.5_init.symtab.strtab.shstrtab.interp.gnu.hash.dynsym.dynstr.gnu.version.gnu.version_r.rela.dyn.rela.plt.init.plt.got.text.fini.rodata.eh_frame_hdr.eh_frame.note.gnu.property.note.ABI-tag.init_array.fini_array.dynamic.got.plt.data.bss.commentS'pp#���o��-
                                                              ��5pp
                                                                    =���o||J���o��Y��cB��m   s|PP�  �  ,�@ @ �� � �!! ������=�w�?���?�0@�0000`       �3�w5
fish

Assembly Language#

기계어와 1:1 대응되는 저급 프로그래밍 언어에요.

아래와 같이 Hello, World!를 출력할 수 있어요.

section .data
    msg db 'Hello, World!', 0

section .text
    global _start

_start:
    mov rax, 4
    mov rdi, 1
    mov rsi, msg
    mov rdx, 14
    syscall

    mov rax, 60
    mov rdi, 0
    syscall
asm
mov rax, 3
asm

는 직역하면 “rax에 3을 mov한다.”이고 정리하면 rax 레지스터에 3을 저장하는 명령어에요.

여기에서 mov는 Opcode(연산자)이고 rax는 Register, 3은 Operand(피연산자)입니다.

Assembly에서 System Call을 하는 방법은 다음과 같아요.

레지스터 인잘들 저장한 다음 syscall을 호출하면 커널에 접근하여 시스템 콜을 수행할 수 있어요.

rax에 호출할 기능을 저장하고, rdi, rsi, rdx 등의 레지스터에 인자값을 저장한 다음 syscall을 호출하면 해당 기능을 수행할 수 있어요.

System Call은 x86_64 Linux 기준으로 332가지가 넘게 있기에 찾아서 참고하는 편이 좋습니다.

데이터 이동 Opcode#

명령어설명
mov rax, rbxrax에서 rbx로 데이터 복사
lea rax, [rbp-8]rax에 rbp - 8의 주소 복사
push rax스택에 rax 값 복사
pop rax스택에서 꺼낸 값 rax에 저장

산술 및 논리 연산 Opcode#

명령어설명
add rax, 1rax에 1 더하기
sub rax, 1rax에 1 빼기
imul rax, rbx, 10rax에 rbx * 10을 저장
idiv rbxrax / rbx를 몫은 rax, 나머지는 rdx에 저장

비교 및 흐름 제어 Opcode#

명령어설명
cmp rax, 10rax - 10, 결과에 따라 FLAG 설정
jmp <주소>해당 주소로 이동
jz / je <주소>연산 결과가 0이면 해당 주소로 이동
jnz / jne <주소>연산 결과가 0이 아니면 해당 주소로 이동
jg <주소>연산 결과가 0 초과면 해당 주소로 이동
jge <주소>연산 결과가 0 이상이면 해당 주소로 이동
jl <주소>연산 결과가 0 미만이면 해당 주소로 이동
jle <주소>연산 결과가 0 이하이면 해당 주소로 이동

System Call#

System Call은 운영 체제의 커널이 제공하는 서비스에 대해, 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스에요.
컴퓨터의 자원(CPU, 메모리, 하드디스크 등)은 매우 중요하므로, 사용자 프로그램이 직접 접근하지 못하도록 막아야해요.

System Call은 다음과 같이 작동해요.

  1. 프로그램이 파일을 읽거나 네트워크 데이터를 보내야 할 때 시스템 콜을 호출합니다.
  2. CPU에 소프트웨어 인터럽트를 발생시켜 제어권을 커널로 넘깁니다.
  3. 커널은 시스템 콜 번호를 확인하고, 요청한 작업이 안전한지 검사한 뒤 하드웨어를 제어하여 작업을 수행합니다.
  4. 작업이 완료되면 결과값을 반환하며 다시 유저 모드로 돌아갑니다.

Register#

Register는 CPU의 레지스터로, 연산에 사용되는 데이터를 임시로 저장하는 공간에요.

우리가 주로 빠르다고 알고 있는 메모리보다 속도가 빠르기에 메모리에서 값을 가져와서 레지스터에서 사용해요.

Screenshot_20260514_230543.png

같은 레지스터도 사용할 크기에 따라 부르는 명칭이 달라요.

Screenshot_20260514_231504.png

예시로 mov ah, 3은 rax의 하위 16비트 중 상위 8비트에 3을 저장하는 명령어에요.

필수로 알아야하는 레지스터의 역할은 다음과 같아요.

레지스터설명
rax함수 리턴 값
rbp스택 프레임의 최상위 주소를 가리키는 포인터
rsp스택 프레임의 최하위 주소를 가리키는 포인터
rip현재 실행 중인 코드를 가리키는 포인터

Flag 레지스터는 수행한 연산의 결과를 저장하는 레지스터에요.

레지스터설명
ZFZero Flag는 연산 결과가 0이면 1로 설정
SFSign Flag는 연산 결과가 음수면 1로 설정
CFCarry Flag는 빌림/올림이 발생하면 1로 설정
OFOverflow Flag는 오버플로우가 발생하면 1로 설정

범용 레지스터#

레지스터설명
rax함수 리턴 값
rbx데이터를 저장
rcx반복 횟수를 저장 (4번쨰 인자)
rdx데이터를 저장 (3번째 인자)
rdiSource Index (2번째 인자)
rsiDestination Index (1번째 인자)
rsp스택 포인터
rbp베이스 포인터

Opcode#

Opcode는 CPU가 실행할 작업을 지정하는 기계어의 일부분이에요.
CPU에게 어떤 작업을 수행해야 하는지 알려주는 역할을 해요.

우리가 흔히 보는 mov, push, add같은 명령어들은 인간이 보기 편하게 만든 명령어일 뿐입니다.
어셈블러가 이 단어들을 CPU가 이해할 수 있는 이진수로 변역하면, 그 결과물 안에 Opcode가 들어가게 되어요.

Machine Language를 참조하면 좋아요.

Machine Language#

48 C7 C0 03 00 00 00
plaintext

컴퓨터가 이해할 수 있는 언어이고 보기 편하게 16진수로 표현해요.

앞에 있는 48은 64비트 연산 접두어이고
C7은 mov 명령어를 나타내고
C0은 rax 레지스터에
03은 3을 의미해요.

정리하면 rax 레지스터에 3을 저장하는 명령어에요.

어셈블리로 사칙 연산 프로그램 만들기#

section .data
    newline db 10

section .bss
    num1 resb 2
    num2 resb 2
    result resb 2

section .text
    global _start

_start:
    mov rax, 0
    mov rdi, 0
    mov rsi, num1
    mov rdx, 2
    syscall ; 첫번째 입력

    mov rax, 0
    mov rdi, 0
    mov rsi, num2
    mov rdx, 2
    syscall; 두번째 숫자 입력

    mov r8b, [num1]
    sub r8b, '0'      ; r8b(num1)를 숫자로 변환
    mov r9b, [num2]
    sub r9b, '0'      ; r9b(num2)를 숫자로 변환

    mov al, r8b       ; r8b(num1)를 al로 복사
    add al, r9b       ; r8b(num1) + r9b(num2)
    call _print

    mov al, r8b       ; r8b(num1)를 al로 복사
    sub al, r9b       ; r8b(num1) - r9b(num2)
    call _print

    mov al, r8b       ; r8b(num1)를 al로 복사
    mul r9b           ; r8b * r9b
    call _print

    movzx ax, r8b     ; r8b(num1)를 ax로 확장 (ah를 0으로 자동 초기화)
    div r9b           ; ax / r9b 수행
    call _print

    ; 종료
    mov rax, 60
    xor rdi, rdi
    syscall

_print:
    add al, '0'
    mov [result], al

    mov rax, 1
    mov rdi, 1
    mov rsi, result
    mov rdx, 1
    syscall         ; 결과 출력

    mov rax, 1
    mov rdi, 1
    mov rsi, newline
    mov rdx, 1
    syscall         ; 줄바꿈 출력

    ret             ; 함수 종료
asm

선택 과제#

Dreamhack 문제 풀이는 별도 글로 옮겼어요.

-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg4c/dn4BitGH1/xNjKoKEp97I2b
eU57QXvkDBEdNNrEMAAAATYmxvZy5pbW55YS5uZy9wb3N0cwAAAAAAAAAGc2hhNTEyAAAA
UwAAAAtzc2gtZWQyNTUxOQAAAED6qwouK27iuGDFXY9T1XJ9WrtO2dz73eqDhUCJPttt8H
E41x/OiSZdFBz8wdPfebRW7wz1ypGPoFMY50sUOwkO
-----END SSH SIGNATURE-----
[Layer7] 2026년 5월 13일 리버싱 1차시 과제
http://blog.imnya.ng/layer7/08
저자 imnyang
게시일 2026년 05월 13일