돌아온 개강
오랜만입니다. 그동안 공부는 조금씩 했는데(진짜 조금씩함;;) 내용 정리를 노션에 먼저 정리하고 블로그에 옮겨적으려고 했습니다. 복습 겸 재정리하려고 했는데, 어쩌다보니 분량이 좀 쌓여버렸네요...
이 놈의 날씨는 처서가 지나고 8월이 다 끝나가는데, 시원해질 생각을 안하는 것 같습니다. 여전히 밖에 나가면 녹아버릴 것 같아요.
그리고 어느덧 개강이 일주일 남았습니다. 항상 개강 시즌에는 돈도 많이 나가고 너무 바쁜 것 같습니다.

하지만 저란 인간은 학기 중에 그나마 사람답게 살기 때문에, 저의 발전에는 오히려 좋다고 볼 수 있습니다. 열심히 달려보자구요.
각설하고 본론으로 들어가보겠습니다.
GDB란 무엇인가?
GDB(GNU debugger)는 리눅스에서 사용되는 표준 디버거이며, GNU 프로젝트에서 제작된 소프트웨어 시스템을 위한 표준 디버거이다.
자유소프트웨어를 만들고 보급하기 위한 GNU에서 제작된 다양한 소프트웨어들이 연결된 구조에서 사용되는 디버거라고 합니다.
리버싱 과정에서 동적 분석이 필요할 때 많이 사용됩니다. 프로그램을 조금씩 실행시키면서 레지스터 값의 변화를 추적하는 데 용이하기 때문이죠.
GDB 명령어를 알아보자
1. file(example)
example 파일을 디버깅 대상으로 지정하는 명령어입니다.

gdb에 진입한 이후 file 명령어로 relativity를 지정한 모습입니다. 그러나 gdb 진입 전에 gdb ./relativity를 입력하면 gdb에 진입함과 동시에 디버깅 대상으로 지정하므로 file 명령어를 잘 쓸 일은 없을 것 같습니다.
참고로 리눅스 자체 명령어인 file과 헷갈리지 않도록 합시다.

file 명령어를 통해 해당 파일의 정보를 볼 수 있습니다. 일반적으로 확인할 것은 x86-64와 stripped입니다. x86-64는 해당 프로그램이 x86 아키텍처로 만들어진 바이너리라는 뜻이고, stripped는 ELF에 심볼 테이블이 존재하지 않는다고 보시면 되겠습니다. ELF와 심볼 등에 관한 내용은 포스트 마지막에 정리하겠습니다.
2. run <프로그램 인자>

프로그램을 실행하는 명령어입니다. 이미지는 debugee라는 파일을 실행하는 과정입니다. 이 명령어 또한 이미 디버깅 대상으로 지정한 파일에 대해서는 run만 입력해도 됩니다.
3. info file
현재 로드된 바이너리의 섹션 정보를 보여주는 명령어입니다.

- 대상 바이너리 파일의 경로
- 파일에 적용된 아키텍처의 종류
- 엔트리 포인트
- 각 섹션의 메모리 주소 범위
일반적으로 프로그램을 시작하는 과정은 다음과 같습니다.
main에 브레이크 포인트를 걸게되면, 사전 작업을 건너뛰고 바로 main에 멈추게되지만, 엔트리 포인트에 브레이크 포인트를 걸면 위와 같은 과정을 시작하기 직전에 멈추게 됩니다.
4. break
특정 주소에 중단점을 설정하는 명령어입니다. 이후 프로그램을 실행하면 지정한 포인트에서 멈추게됩니다.

레지스터의 변화를 관찰하고 싶은 부분의 시작점에 break point를 걸어주고 continue, ni 등의 명령어로 자세하게 변화를 관찰할 수 있습니다.
5. continue
다음 브레이크 포인트나 프로그램 종료까지 중단된 프로그램을 계속 실행시킵니다. 만약 멈춰야할 어떤 이유가 있다면 그 지점에서 다시 멈추게됩니다.

여러 BP(Break Point)를 건너뛰고 싶을 때는 continue/c <숫자>를 이용해 여러번 건너뛸 수 있습니다.

bp를 설정한 후 run을 실행한 화면입니다. 여기서 레지스터들의 상태, 실행 중인 어셈블리 명령어와 그 주변 명령어들을 확인할 수 있습니다. 이를 통해 레지스터의 값을 수정하거나 프로세스의 정보를 얻어낼 수 있습니다.
6. entry
RIP 레지스터에 EP를 설정하는 명령어입니다.
RIP 레지스터란 x86 아키텍처의 레지스터로, CPU가 다음으로 실행할 명령어인 PC가 저장되는 레지스터입니다.
entry 명령어는 RIP에 EP가 들어올 때까지 프로그램을 실행시키는 명령어입니다. 이후 RIP에 EP가 들어오면 멈춥니다.

만약 프로그램 실행 도중에 entry를 실행하면, 현재 실행 상태를 모두 버리고 처음으로 돌아가 EP에 멈추는 메커니즘입니다.
7. start
main의 시작점에 임시 bp를 걸고 프로그램을 실행하여 임시 bp에서 프로그램을 멈춥니다.
참고로 바이너리가 stripped 상태여서 심볼 정보가 존재하지 않는다면 main을 찾을 수 없기 때문에, 이 경우에는 entry와 동일하게 동작합니다.

stripped 바이너리에서 start를 실행한 모습입니다. 정상적으로 작동한 모습을 볼 수 있는데 왜 그럴까요?
이는 GDB가 __libc_start_main의 호출 규약을 따르기 때문입니다.
__libc_start_main은 앞서 언급한 프로그램 시작 과정에 실행되는 함수입니다. 이 함수는 ELF의 dysym이라는 섹션에 저장되는 동적 심볼이기 때문에, strip되어도 남아있게 되는데, 이 함수의 첫번째 인자가 main의 주소입니다.
즉, 내부적으로 __libc_start_main에 bp를 걸고 run하여 멈춘 시점의 첫번째 인자(RDI)의 값을 확인하여 main의 주소를 유추해내도록 작동하게됩니다.
8. 명령어를 분석할 때 사용하는 명령어

1. ni(next instruction): 함수 호출(call)을 만났을 때, 해당 함수 전체를 실행하고 다음 줄로 넘어갑니다.(=step over) 함수 내부의 동작을 살펴볼 때는 적절하지 않습니다.

2. si(step instruction): call을 만났을 때, 함수 안으로 들어갑니다(step in) 함수 내부의 동작을 살펴볼 때 적절합니다.

3. finish: 함수의 끝까지 한번에 실행하는 명령어입니다. si를 통해 함수 내부를 살펴보다가 더 이상 살펴볼 필요가 없다고 판단되면, finish로 함수를 끝낼 수 있습니다.

참고로 PIE 바이너리를 IDA와 gdb를 모두 사용하여 정적/동적 분석을 동시에 수행하는 경우, IDA의 주소를 기반으로 gdb에서 bp를 걸려고 한다면, 제대로 걸리지 않습니다.
이유는 ASLR이 런타임에 베이스 주소를 랜덤으로 재배치하는데, IDA는 ELF에 저장된 0x0 베이스 주소를 기반의 오프셋으로 보여주기 때문입니다. 그리하여 gdb에서 IDA의 오프셋을 활용하려면 ASLR이 배치한 베이스 주소에 IDA 오프셋을 더해주어야합니다.
9. info registers(i r)
축약형 i r로 사용할 수 있습니다. 실행 중 현 시점의 모든 범용 레지스터의 값을 한번에 보여주는 명령어입니다.

10. b *$(레지스터 명)
레지스터 내부의 값을 주소로 해석하여 그 주소에 bp를 거는 명령어입니다.

11. bp 관련 명령어들
1. info break points: 현재 걸려있는 모든 bp의 정보를 알려주는 명령어입니다. 참고로 bp의 번호는 순서대로 매겨지며, 앞 번호의 bp를 지워도 bp 번호가 갱신되지는 않습니다.

2. disable(bp 번호): bp를 제거하지 않고 일시적으로 비활성화하는 명령어입니다. enable로 다시 활성화 할 수 있습니다.

enb 항목을 보면 3번 bp가 n으로 표기되어 비활성화 됐음을 알 수 있습니다.
3. delete(bp 번호): bp를 영구적으로 삭제합니다.

12. disassemble
함수 전체 또는 특정 주소 범위의 기계어를 어셈블리 코드로 변환해서 보여주는 명령어입니다. 축약형은 disas입니다.

u/nearpc/pdisas: 현재 위치 주변의 몇 줄만 보여주는 명령어입니다. 또는 주소를 인자로 넘겨주면 해당 주소 주변의 몇 줄을 디스어셈블하여 보여줍니다. 다른 작업 중에 특정 주소 영역을 확인하고 싶을 때 유용합니다.
13. x(examine)
특정 메모리 주소의 내용을 원하는 형식으로 출력하는 명령어입니다.
| 포맷 종류 | 크기 종류 |
| x -> 16 진수 | b -> 1byte |
| d -> 10진수 | h -> 2byte(half word) |
| s -> 문자열 | w -> 4byte(word) |
| i -> 명령어(디스어셈블) | g -> 8byte(giant) |
| c -> 문자(char) | |
| t -> 2진수 | |
| f -> float | |
| o -> 8진수 | |
| a -> 주소 |
기본 형식은 x/[개수][포맷][크기]입니다.

rsp 레지스터의 주소로부터 8 * 5 = 40바이트를 16진수로 8바이트씩 보여주는 모습입니다.

두 케이스는 각각 rsp의 주소부터 문자열 10개를 출력, 5개의 어셈블리 코드를 출력하는 명령어입니다. 문자열이나 명령어와 같이 가변적인 포맷들은 크기 지정자가 무시됩니다.
14. telescope<주소>[개수]
tele로 축약할 수 있습니다. 이 명령어는 지정한 주소에서 지정한 개수만큼의 메모리 값을 보여주고, 값이 포인터면 자동으로 따라가서 최종값까지 체인으로 보여주는 명령어입니다.

화살표(->)가 존재하는 값들은 포인터 값으로, 해당 포인터가 가르키는 값을 체인으로 보여주는 모습입니다.
Q. 어떻게 포인터인지 구분할까요?
A. 실제 주소로 존재하는 값으로, 접근이 가능하며 포인터로 판단하고, 접근이 불가능하다면 일반 정수로 판단합니다. 그래서 간혹 정수로 판단해야하는데, 포인터로 해석되어 잘못 표현될 때도 있다고 합니다. 개인적으로 포인터 체인 값이 이상하게 나온다면 의심해볼만 한 것 같습니다.
15. bt(k)
backtrace의 약자로, 현재 시점까지의 함수 호출 스택(call stack)을 보여주는 명령어입니다.
call stack이란 함수가 호출된 순서를 스택으로 쌓아놓은 자료구조 입니다.
bt를 사용하면 낯선 주소에서 멈췄을 때, 어떤 흐름으로 호출됐는지 판단할 때, 특정 함수에 전달된 인자에 문제가 발생해서 버그가 유발됐다면, 이 인자가 어떤 함수로부터 왔는지 확인할 때 유용하게 사용됩니다.

스택 자료구조이므로 아래에서 위로 읽으면 함수가 호출된 순서입니다.
16.f(프레임 숫자)
콜 스택의 특정 프레임으로 이동해서 해당 시점의 컨텍스트를 볼 수 있는 명령어입니다. 인자를 넘기지 않으면 현재 어느 프레임에 있는지를 보여줍니다.

2번 프레임의 함수 실행 시점의 레지스터, 로컬 변수, 소스 위치 등을 확인할 수 있습니다.
단, 해당 시점의 값만 확인하는 것이기 때문에, 실행 위치까지 2번 프레임으로 바뀌는 것이 아닙니다.
telescope와 조합하여 사용하면, 문제가 발생한 함수 부근에서 구체적인 값들을 확인할 수 있습니다.
17. 추가적인 명령어들
1. dump memory[파일명][시작주소][끝주소]: 특정 메모리 범위의 내용을 저장합니다.
2.vmmap: 현재 실행 중인 프로세스의 셋녀들이 가상 메모리에 어떤 위치에 어떤 권한으로 매핑되어있는지를 보여주는 명령어입니다.

vmmap을 이용하여 데이터 영역의 주소 범위를 계산하여 데이터 영역을 파일로 저장한 모습입니다.
3. context(ctx): 주요 메모리들의 상태를 가독성 있는 인터페이스로 보여줍니다. 프로그램이 실행되다가 bp에 도달하면 자동으로 실행되며, 별도 실행 또한 가능합니다.

context 실행 시 보여지는 4 개의 영역입니다
| REGISTERS | 레지스터의 상태 표시 |
| DISASM | rip부터 여러 줄에 걸쳐 디스어셈블된 결과 표시 |
| STACK | rsp로부터 여러 줄에 걸쳐 스택 값을 표시 |
| BACKTRACE | 현재 rip에 도달할 때까지 어떤 함수들이 중첩되어 호출되었는지 표시 |
4. set<주소/레지스터> = <변경할 값>: 특정 주소나 레지스터의 값을 원하는 값으로 변경하는 명령어입니다. 값을 조작하여 결과를 관찰할 때 유용합니다.
추가적인 몇 가지 팁과 정보
일단 저는 M1 Mac 환경에서 실습 중입니다. 많이 경험해보진 않았지만, 제가 겪은 드림핵 리버싱 문제들은 모두 stripped 바이너리였어요. 그리고 gdb를 처음 실행하고 나면, 매 명령어 실행마다 디버깅 심볼 다운로드 실행을 실행하기 때문에 실습에 큰 지장이 가는 문제가 있었습니다. 그래서 저는 디버깅 심볼 다운로드를 하지 않도록 하는 명령어를 입력하고 리버싱을 시작합니다.

디버깅 심볼에 대해서 알아보며 알게된 ELF 관련 정보입니다. 먼저 ELF란 OS의 로더와 링커가 읽고 해석할 수 있도록, 바이너리 파일을 구조화하는 포맷입니다.
간략하게 표현하자면 ELF의 구조는 아래와 같습니다.
stripped 바이너리는 심볼 테이블에 .symtab 세션이 제거된 바이너리 입니다. 해당 세션에 사용자 정의 함수명이 존재하기 때문에 stripped 바이너리에서 b main이 통하지 않게됩니다.
아래는 ELF 바이너리의 주요 섹션입니다.
| .text | 실제 실행되는 기계어 코드가 존재하는 섹션 |
| .init/fini | 프로그램의 시작과 끝에 실행되는 초기화 및 정리 코드가 존재하는 섹션 |
| .data | 초기값이 있는 정적/전역 변수 |
| .bss | 초기값이 없거나 0으로 초기화된 정적/전역 변수 -> 파일에는 크기 정보만 존재하고 실제 데이터는 없어서 바이너리 크기를 줄여줌 |
| .rodata | 읽기 전용 데이터, 문자열 리터럴이나 const 상수가 저장됨 |
| .plt/.got | 외부 라이브러리 함수를 호출할 때, 사용하는 동적 링킹 테이블 |
| .dynsym/.dynstr | 동적 심볼 이름과 문자열 정보 |
| .interp | 동적 링커의 경로를 지정 |
| .symtab/.strtab | 심볼 테이블과 문자열 테이블. stripped되면 이 섹션이 제거됨 |
| .debug | 디버그 정보 |
리버싱을 할 때 주로 .text /.data/.rodata, .plt/.got 등을 참고하게됩니다.
몇 주전에 정리한 내용인데, 어쩌다보니 이제 글을 작성하게 되었네요.. 스압이 좀 있지만, 끝까지 읽어주셔서 감사합니다! 개강하고 나면 더 열심히 달려보겠습니다.
'개념 정리 > 리버싱' 카테고리의 다른 글
| x86-64 Architecture (0) | 2026.09.01 |
|---|