Study/System Hacking

[dreamhack] Tool: gdb

haesa_s 2022. 3. 26. 16:21

gdb & pwndbg

예제 코드

// Name: debugee.c
// Compile: gcc -o debugee debugee.c -no-pie
#include <stdio.h>
int main(void) {
  int sum = 0;
  int val1 = 1;
  int val2 = 2;
  sum = val1 + val2;
  printf("1 + 2 = %d\\n", sum);
  return 0;
}

 

리눅스는 실행파일의 형식으로 ELF(Executable and Linkable Format)를 규정하고 있다.

ELF는 크게 헤더와 여러 섹션들로 구성되어 있는데 헤더에는 실행에 필요한 여러 정보가 적혀 있고 섹션들에는 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터가 포함되어 있다.

 

ELF의 헤더 중에 진입점(Entry Point, EP)이라는 필드가 있는데 운영체제는 ELF를 실행할 때 진입점의 값부터 프로그램을 실행한다. readelf로 확인해본 결과, debugee의 진입점은 0x400400이다.

 

 

 

 

 

start

진입점부터 프로그램을 분석할 수 있게 해주는 gdb의 명령어다.

DISASM영역의 화살표(►)가 가리키는 주소는 현재 rip의 값이다.

 

 

 

 

 

 

context

pwndbg는 주요 메모리들의 상태를 프로그램이 실행되고 있는 맥락(Context)이라고 부르며, 이를 가독성 있게 표현할 수 있는 인터페이스를 갖추고 있다.

 

context는 크게 4개의 영역으로 구분됩니다.

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

 

 

 

 

 

break & continue

break는 특정 주소에 중단점(breakpoint)을 설정하는 기능이고, continue는 중단된 프로그램을 계속 실행시키는 기능이다.

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

이후 continue를 사용하면 중단된 지점부터 다시 세밀하게 분석할 수 있다.

 

 

 

 

 

 

run

run은 start와 달리 단순히 실행만 시키는 명령어다.

따라서 중단점을 설정해놓지 않았다면 프로그램이 끝까지 멈추지 않고 실행된다.

 

현재는 main에 중단점을 걸어놔서 main에서 멈췄다.

 

 

 

 

 

 

 

disassembly

어셈블(assemble): 어셈블리어를 기계어로 바꾸는 작업.

디스어셈블(disassemble): 기계어를 어셈블리어로 바꾸는 작업.

 

프로그램의 코드는 기계어로 이루어져 있기 때문에 gdb는 기계어를 디스어셈블(Disassemble)하는 기능을 기본적으로 탑재하고 있다. 추가적으로 pwndbg에는 디스어셈블된 결과를 가독성 좋게 출력해주는 기능이 있다.

 

 

 

disassemble

gdb가 기본적으로 제공하는 디스어셈블 명령어다.

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

 

 

 

 

u, nearpc, pdisassemble

pwndbg에서 제공하는 디스어셈블 명령어다.

디스어셈블된 코드를 가독성 좋게 출력해준다.

 

 

 

 

 

 

navigate

관찰하고자 하는 함수의 중단점에 도달했으면 그 지점부터는 명령어를 한 줄씩 자세히 분석해야 한다.

이때 사용하는 명령어로 ni와 si가 있다.

ni와 si는 모두 어셈블리 명령어를 한 줄 실행한다는 공통점이 있다.

그러나 만약 call 등을 통해 서브루틴을 호출하는 경우 ni는 서브루틴의 내부로 들어가지 않지만, si는 서브루틴의 내부로 들어간다는 차이점이 있다.

 

ni와 si의 차이점을 확인하기 위해 printf 함수를 호출하는 지점까지 가봤다.

 

 

 

next instruction

ni를 사용했을 경우 printf 바로 다음으로 rip가 이동했다.

 

 

 

step into

si를 사용했을 경우 printf 함수 내부로 rip가 이동했다.

 

 

 

 

 

finish

step into로 함수 내부에 들어가서 분석한 후 함수의 규모가 커서 ni로는 원래 실행 흐름으로 돌아가기 어려울 때 finish를 사용하면 함수의 끝까지 한 번에 실행할 수 있다.

 

 

 

 

 

 

examine

gdb에서는 기본적으로 x라는 명령어를 제공한다.

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

 

ex)

 

1. rsp부터 80바이트를 8바이트씩 hex형식으로 출력

 

 

2. rip부터 10줄의 어셈블리 명령어 출력

 

 

3. 특정 주소의 문자열 출력

 

 

 

 

 

 

telescope

telescope은 pwndbg가 제공하는 강력한 메모리 덤프 기능이다.

특정 주소의 메모리 값들을 보여주며 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 값을 보여준다.

 

 

 

 

 

 

vmmap

vmmap은 가상 메모리의 레이아웃을 보여준다.

어떤 파일이 매핑된 영역일 경우 해당 파일의 경로까지 보여준다.

 

 

 

 

 

 

gdb / python

gdb를 통해 디버깅할 때 파이썬으로 입력값을 생성하고 사용해야 할 수도 있다.

파이썬을 이용하는 방법을 살펴보기에 앞서 아래 예제 코드를 작성하고 컴파일해보자.

프로그램의 인자로 전달된 값과 이용자로부터 입력받은 값을 출력하는 예제다.

 

예제 코드

// Name: debugee2.c
// Compile: gcc -o debugee2 debugee2.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	char name[20];
	if( argc < 2 ) {
		printf("Give me the argv[2]!\n");
		exit(0);
	}
	memset(name, 0, sizeof(name));
	printf("argv[1] %s\n", argv[1]);
	read(0, name, sizeof(name)-1);
	printf("Name: %s\n", name);
	return 0;
}

 

 

 

 

 

gdb / python argv

run 명령어의 인자로 $()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있다.

 

 

 

gdb / python input

$()와 함께 파이썬 코드를 입력하면 값을 입력할 수 있다.

입력값으로 전달하기 위해서는 '<<<' 문자를 사용한다.