취약점분석/Pwnable

[Pwnable] GDB 기초 사용법

poiri3r 2025. 6. 9. 23:47

GDB(GNU debugger)는 리눅스에서 사용하는 대표적인 디버거 중 하나로 GNU에서 소프트웨어 시스템을 위한 표준 디버거이다.

 

gcc로 텍스트 파일 컴파일 하기.

gcc -o debugee debugee.c -no-pie에서

메모장에 해당 코드를 적고 debugee.c로 저장한 뒤 리눅스에 해당 명령어를 입력하면, debugee.c라는 c 소스파일을 gcc로 컴파일해서, 보안 기능인 PIE를 끈 고정 주소 실행파일 debugee를 만들어 저장한다.
(고정 주소 실행 파일이 디버깅/리버싱할 때 주소 고정으로 편리함)

 

디버깅을 수행하기 위해서 먼저 디버깅 할 바이너리를 GDB에 불러와야 한다.

GDB에서 file 명령어는 바이너리를 GDB에 불러 와 실행하고 디버깅 할 수 있게 만든다.

처음 터미널에서 gdb를 실행할 때 gdb ./debugee와 같이 인자에 디버깅 할 바이너리 경로를 넣어도 똑같이 기능한다.

 

run <프로그램 인자> : 프로그램을 실행하는 명령어이다.

프로그램 인자가 필요 없는 경우 run 명령어만 입력해도 실행된다.

break : 특정 주소에 중단점을 설정하는 기능이다.

continue : 중단된 프로그램을 계속 실행시키는 기능이다.

break로 원하는 함수에 중단점을 설정하고, 프로그램을 계속 실행하면 해당 함수까지 멈추지 않고 실행한 다음 중단된다. 그 뒤 중단된 지점부터 세밀하게 분석한다.

break대신 b 명령어로 동일한 작업을 수행할 수 있으며, 함수명 뿐만 아니라 특정 주소로도 중단점을 설정할 수 있다.

b main으로 중단점에 bp를 건 뒤 run으로 프로그램을 실행하면, 중단점이 걸린 지점에서 실행이 중단되면서 다음과 같은 화면이 출력된다. 

프로그램이 중단점에 도달했을 때 프로그램에 대한 상세한 정보들을 출력해주는데 실행되고 있는 어셈블리 명령어와 그 주변의 명령어들을 보여준다. 여러가지 디버거의 기능들을 통해 프로세스의 정보를 얻거나 변조도 가능하다.

중단점을 통해 프로그램을 중단한 뒤, continue(c)를 통해 다시 실행을 이어나갈 수 있다. (중단점을 여러개 건너 뛰고 싶을때는 continue <숫자> or c <숫자>를 이용해 여러번 건너뛸 수 있다)

리눅스는 실행파일 형식으로 ELF (Executable and Linkable Format) 형식을 따른다. 엘프의 헤더와 섹션은 리눅스에서 readelf 라는 명령어로 확인 가능하다.

(readelf -h 명령어는 ELF file header만 읽어들임)

이 중에서 Entry Point(EP, 진입점)이 있는데, 운영체제에서 ELF를 실행할 때 EP 명령어부터 시작한다.

debugee파일의 EP는 0x401050인데 GDB의 entry 명령어를 사용하면 EP부터 프로그램을 분석할 수 있다.

entry : EP로 rip를 설정하고 분석을 시작할 수 있게 해주는 명령어

=> entry 명령어를 실행했을 때 EP 지점인 0x401050부터 프로그램이 진입함

start : entry와 비슷하지만 main()부터 분석할 수 있게 시작점을 설정함. main()을 찾을 수 없는 경우 entry와 동일하게 동작함

b main + r 과의 차이점

관찰하고자 하는 함수의 중단점에 도달한 뒤, 명령어를 분석할 때 사용하는 명령어들이다

ni : next instruction을 줄인것으로 함수의 내부에 들어가지 않고 한 줄을 실행

si : step info을 줄인것으로 함수의 내부에 들어가서 한 줄을 실행

finish : 함수의 끝까지 한번에 실행하는 명령어

=>함수에서 printf에 해당하는 주소인 401190에 bp를 걸고 실습
printf 부분에 bp가 걸린 상태

 ni명령어 입력시 printf() 다음 주소인 0x401195에 rip가 이동한다.

si 명령어 입력시 printf()내부로 들어간다

si로 printf 내부 진입 후 finish 명령어를 내리면 함수의 끝까지 한번에 실행한다

 

info registers(i r) : 프로그램이 실행 중일 때 레지스터에 저장된 값을 출력

b *$(레지스터) : 레지스터 안에 주소값이 있을 때 그 주소값으로 바로 bp를 걸 수 있다

info breakpoints(i b) : 프로그램의 실행 여부와 상관 없이 bp를 확인 가능

disable 중단점 번호 :  중단점 비활성화(enable로 다시 활성화), 중단점을 다시 활성화 할 때까지 해당 중단점에서 프로그램이 멈추지 않음

delete 중단점 번호(d 중단점 번호) : 중단점 삭제

disassemble 함수이름(disass 함수이름) : 함수 이름을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여준다

u, nearpc, pdisass : pwndbg에서 제공하는 디스어셈블 명령어로, 디스어셈블된 코드를 가독성 좋게 출력해준다

x(examine)

x 명령어를 이용하면 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩해서 볼 수 있다.

원하는 포멧과 크기를 조합하여  x/<포멧 및 크기> 형태의 명령어를 사용한다. 여러개의 값을 보기 위해서는 x/<개수><포멧 및 크기> 형태의 명령어를 사용한다.

 

x/<개수><포멧 및 크기> :

포멧 크기
x: 16진수 b: byte, 1 byte
o: 8진수 h: halfword, 2byte
d: 10진수 w: word, 4byte
t: 2진수 g: giant, 8byte
f: float  
a: 주소  
i: 명령어  
c: 문자  
s: 문자열  

rsp부터 80바이트(g * 10)를 10바이트씩 Hex형식(16진수)로 출력

 

rip부터 5줄의 어셈블리 명령어 출력

가상 메모리는 프로그램이 실행되기 전에는 할당되지 않으므로 x 명령어는 프로그램 실행 도중에만 사용할 수 있다.

 

telescope <주소> [개수](tele <주소>  [개수] : 특정 주소의 메모리 값들을 보여주고 그 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 보여준다.

 

bt (k로도 가능) : 콜 스택을 출력

콜 스택이란 프로그램이 실행되는 동안 함수가 호출되는 순서를 저장하는 스택 자료구조이다. (FILO)

어떤 함수에 전달된 인자에 문제가 발생해 버그가 발생했다면, 이 인자가 어떤 함수로부터 왔는지 확인 할 때 유용하게 사용할 수 있다.

함수의 add가 위치한 0x40113e에 bp를 걸어두고 프로그램을 실행한다
k로도 똑같은 명령어를 내릴 수 있다

f (프레임 숫자): 특정 스택 프레임으로 이동할 때 사용한다. info frame이나 info locals를 더 정확하게 보기 위해 사용한다

bt(k)로 콜스택을 확인한 뒤 f (숫자) 명령어를 이용해서 원하는 프레임으로 이동할 수 있다.

f로 프레임에 접근 한 뒤 context를 통해 해당 함수 부근을 살피면서 문제가 발생한 지점을 확인할 수 있다. telescope를 이용하면 더 강력하게 분석할 수 있다!

dump memory <파일명> <시작주소> <끝주소> : 프로세스의 메모리 상태를 파일로 저장할 때 사용하는 명령어이다

ex)프로그램의 코드영역 덤프 뜨기

1. entry 명령어로 프로그램을 실행한 뒤

2. vmmap 명령어로 프로그램의 코드영역의 시작과 끝 주소를 얻는다

코드 영역은 읽기 권한과 실행권한이 부여되어 있으므로 vmmap에서 r-xp로 저장되어있는 0x401000 부터 0x402000까지가 코드 영역임을 확인할 수 있다.

3.dump memory code_section 0x401000 0x402000(예시)로 코드영역의 시작부터 끝까지 메모리 덤프를 한다.

같은 파일에 code_section이라는 파일이 생성된다.

IDA로 확인한 0x401000부분의 HEX 코드
생성된 code_section파일에 저장된 HEX코드

해당 파일을 확인해봤을 때 메모리 덤프가 정상적으로 일어났음을 확인해볼 수 있다.

 

context(ctx) : 주요 메모리들의 상태를 가독성 있는 인터페이스로 보여준다. 프로그램이 실행되다가 중단점에 도달하면 자동으로 실행되며, context 명령어로 별도 실행 가능하다

context 명령시 보여지는 4개의 영역

REGISTERS 레지스터의 상태를 보여준다
DISASM rip부터 여러 줄에 걸쳐 디스어셈블된 결과를 보여준다
STACT rsp부터 여러 줄에 걸쳐 스택의 값들을 보여준다
BACKTRACE 현재 rip에 도달할 때 까지의 어떤 함수들이 중첩되어 호출됐는지 보여준다

 

set <주소/레지스터> = <변경할 값> : ★프로세스의 메모리 상태를 변경할 수 있는 명령어이다. 주로 레지스터 값을 변경하거나 특정 주소의 메모리 값을 변경하며, 프로그램이 실행 중인 상태에서만 동작한다.

연습해보면서 자세히 알아보자!

debugee의 원본 코드

여기서 printf에서 add(a,b)의 값을 출력하는데 이 add결과값인 5는 출력 전 특정 레지스터에 저장이 된 뒤 출력이 될 것이다.

b printf를 통해 printf에 bp를 걸어둔 뒤 r로 코드를 실행한다.

info register로 레지스터에 들어간 값을 보면 rcx에 add값인 5가, rdx와 rsi에 각각 b,a값이 들어감을 알 수 있다.

IDA로 어셈블리어를 분석해보았을 때에도 ecx에 add함수의 리턴값인 eax가 들어감을 알 수 있다

이 때 set $rcx를 통해 rcx레지스터에 들어간 값을 바꿔주면 함수의 출력값을 바꿀 수 있다.

rcx값이 변경됐음을 확인할 수 있다.

그 다음 continue를 통해 함수 끝까지 실행하면 99999가 출력됨을 알 수 있다.

 

이상으로 GDB의 기본 사용에 대한 블로그 포스팅을 마치겠습니다!

아직 포너블이나 리버싱에 익숙하지 않은 초보지만 꾸준히 포스팅 하면서 공부한 내용들 정리해서 올리겠습니다~ㅎ