디버거란
컴퓨터과학에서는 실수로 발생한 프로그램의 결함을 버그(bug)라고 한다. 이러한 버그를 찾는 어려움을 해소하고자 디버거(Debugger)라는 도구가 개발되었다.
디버거는 문자 그대로 버그를 없애기 위해 사용하는 도구이다.
프로그램을 어셈블리 코드 단위로 실행하면서, 실행결과를 보여주기 때문에 개발자는 코드의 문제점을 명확하게 찾을 수 있다.
gdb
gdb (GNU debugger)는 리눅스의 대표적인 디버거이다.
우리가 사용하는 Ubuntu 22.04에는 기본적으로 설치되어 있고, 이 로드맵에서는 pwndbg 기준으로 설명되어 있어 pwndbg 를 설치하면 된다.
https://github.com/pwndbg/pwndbg
GitHub - pwndbg/pwndbg: Exploit Development and Reverse Engineering with GDB Made Easy
Exploit Development and Reverse Engineering with GDB Made Easy - pwndbg/pwndbg
github.com
gdb를 입력했을 때, 위와 같은 결과가 나오면 설치에 성공한 것이다.
실습 예제
간단한 코드를 작성하고, gdb 사용법을 익힌다.
// 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;
}
debugee.c 코드를 작성한다.
컴파일하고, 'gdb debugee'로 디버깅을 시작한다.
entry
리눅스는 실행파일의 형식으로 ELF를 규정하고 있다.
ELF는 크게 헤더와 여러 섹션들로 구성되어 있다.
헤더에는 실행에 필요한 여러 정보가 적혀있고, 섹션들에는 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터가 포함되어 있다.
ELF의 헤더 중에 진입점(EP)이라는 필드는 ELF를 실행할 때, 진입점의 값부터 프로그램을 실행한다.
reafelf로 확인해 보면, debugee의 진입점은 0x401050이다.
gdb의 entry 명령어는 진입점부터 프로그램을 분석할 수 있게 해주는 gdb의 명령어이다.
DISASM영역의 화살표(►)가 가리키는 주소는 현재 rip의 값인데, entry 명령어를 실행하고 보면 0x401050을 가리키고 있다.
context
pwndbg는 주요 메모리들의 상태를 프로그램이 실행되고 있는 맥락(Context)이라고 부른다.
context는 크게 4개의 영역으로 구분된다.
- REGISTERS: 레지스터의 상태
- DISASM: rip부터 여러 줄에 걸쳐 디스어셈블된 결과
- STACK: rsp부터 여러 줄에 걸쳐 스택의 값들
- BACKTRACE: 현재 rip에 도달할 때까지 어떤 함수들이 중첩되어 호출됐는지
break&continue / run
break : 특정 주소에 중단점(breakpoint)을 설정하는 기능, 프로그램을 계속 실행하면 해당 함수까지 멈추지 않고 실행한 다음 중단된다.
continue : 중단된 프로그램을 계속 실행시키는 기능
현재 중단한 start 함수부터 main 함수까지 실행시킨다.
run
앞의 start가 진입점부터 프로그램을 분석할 수 있도록 자동으로 중단점을 설정해줬다면, run 은 단순히 실행만 시킨다.
따라서 중단점을 설정해놓지 않았다면 프로그램이 끝까지 멈추지 않고 실행된다.
disassembly
gdb는 프로그램을 어셈블리 코드 단위로 실행하고, 기계어를 디스어셈블(Disassemble)하는 기능을 기본적으로 탑재하고 있다.
disassemble은 gdb가 기본적으로 제공하는 디스어셈블 명령어이다.
함수 이름을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여준다.
u, nearpc, pdisass는 pwndbg에서 제공하는 디스어셈블 명령어이다.
디스어셈블된 코드를 가독성 좋게 출력한다.
navigate - ni, si, finish
관찰하고자 하는 함수의 중단점에 도달했을 때, 그 지점부터 명령어를 한 줄씩 분석할 때 ni와 si 명령어를 사용한다.
ni와 si는 모두 어셈블리 명령어를 한 줄 실행한다는 공통점이 있다. 하지만 call 등을 통해 서브루틴을 호출하는 경우 차이가 존재한다.
ni는 서브루틴의 내부로 들어가지 않지만, si는 서브루틴의 내부로 들어간다.
확인을 위해 main함수에서 printf 함수를 호출하는 지점까지 실행한다.
ni 명령어를 입력하면, printf 함수 바로 다음으로 rip가 이동한 것을 확인할 수 있다.
printf 함수를 호출하는 지점까지 다시 프로그램을 실행시킨 뒤, si를 입력하면 printf 함수 내부로 rip가 이동한 것을 확인할 수 있다.
또한 backtrace를 보면, main 함수에서 printf를 호출했으므로 main함수 위에 printf 함수가 쌓인 것을 볼 수 있다.
finish라는 명령어를 사용하여 함수의 끝까지 한 번에 실행할 수 있다.
examine
프로그램을 분석하다 보면 가상 메모리에 존재하는 임의 주소의 값을 관찰해야할 때가 있다.
x라는 명령어를 이용하면 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 볼 수 있다.
예시
1. rsp부터 80바이트를 8바이트씩 hex형식으로 출력
2. rip부터 5줄의 어셈블리 명령어 출력
3. 특정 주소의 문자열 출력
telescope
특정 주소의 메모리 값들을 보여주는 것에서 그치지 않고, 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 값을 보여준다.
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[1]!\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 debugee2로 디버깅을 시작
gdb / python argv
run 명령어의 인자로 $()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있다.
gdb / python input
위와 같이 $()와 함께 파이썬 코드를 입력하면 값을 입력할 수 있고, 입력값으로 전달하기 위해서는 <<<를 사용한다.
'Dreamhack > System Hacking' 카테고리의 다른 글
Shellcode (0) | 2024.03.27 |
---|---|
Tool Installation - pwntools (0) | 2024.03.20 |
Background - Computer Science - x86 Assembly (0) | 2024.03.20 |
Background - Computer Science - Linux Memory Layout (0) | 2024.03.19 |
Background - Computer Science - Computer Architecture (0) | 2024.03.19 |