일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- hackctf
- lob
- SWEA
- CSS
- PHP 웹페이지 만들기
- BOJ
- siss
- 백준
- Sookmyung Information Security Study
- 파이썬
- XSS Game
- 숙명여자대학교 정보보안동아리
- Python
- 드림핵
- hackerrank
- C언어
- 풀이
- c++
- 숙명여자대학교 정보보안 동아리
- 머신러닝
- 자료구조 복습
- 생활코딩
- BOJ Python
- 기계학습
- WarGame
- c
- Javascript
- The Loard of BOF
- HTML
- 웹페이지 만들기
- Today
- Total
혜랑's STORY
[System Hacking STAGE 3] Tool: gdb 본문
디버거란
컴퓨터과학에서는 실수로 발생한 프로그램의 결함을 버그(bug)라고 한다. 완성된 코드에서 버그를 찾는 것은 어렵다. 이런 어려움을 해소하고자 디버거(Debugger)라는 도구가 개발되었다.
디버거는 문자 그대로 버그를 없애기 위해 사용하는 도구이다. 프로그램을 어셈블맄 코드 단위로 실행하면서, 실행결과를 사용자에세 보여준다. 이번에는 리눅스의 대표적인 디버거 중 하나인 gdb의 기능들에 대해 배우고, 실습을 통해 사용법을 익혀볼 것이다.
gdb & pwndbg
실습 예제
간단한 코드를 작성하고, 이를 분석하면서 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;
}
그 뒤, gdb debugee로 디버깅을 시작한다.
start
리눅스는 실행파일의 형식으로 ELF를 규정하고 있다. ELF는 크게 헤더와 여러 섹션들로 구성되어 있다. 헤더에는 실행에 필요한 여러 정보가 적혀있고, 섹션들에는 컴파일된 기계어 코드, 프로그램 문자열을 비록한 여러 데이터가 포함되어 있다.
ELF의 헤더 중에 진입접(Entry Point, EP)이라는 필드가 있는데, 운영체제는 ELF를 실행할 때, 진입점의 값부터 프로그램을 실행한다. readelf로 확인해본 결과, debugee의 진입점은 0x400400이다.
gdb의 start 명령어는 진입점부터 프로그램을 분석할 수 있게 해주는 gdb의 명령어이다. DISASM 영역의 화살표가 가리키는 주소는 현재 rop의 값인데, start 명령어를 실행하고 보면 0x4004eb을 가리키고 있다. 이는 앞서 살펴본 프로그램의 진입점의 주소와 다르다. 찾아보니 어셈블리로 작성된 경우 엔트리 포인트는 _start가 된다고 한다. 따라서 _start를 찾아보니 역시 0x400400을 가리키고 있었다.
context
프로그램은 실행되면서 레지스터를 비롯한 여러 메모리에 접근한다. 따라서 디버거를 이용하여 프로그램의 실행 과정을 자세히 관찰하려면 컴퓨터의 각종 메모리를 한눈에 파악할 수 있는 것이 좋다. pwndbg는 주요 메모리들의 상태를 프로그램이 실행되고 있는 맥락(context)이라 부르며, 이를 가독성 있게 표현할 수 있는 인터페이스를 갖추고 있다.
context는 크게 4개의 영역으로 구분된다.
- registers: 레지스터의 상태를 보여준다.
- disasm: rip부터 여러 줄에 걸쳐 디스어셈블된 결과를 보여준다.
- stack: rsp부터 여러 줄에 걸쳐 스택의 값들을 보여준다.
- backtrace: 현재 rip에 도달할 때까지 어떤 함수들이 중첩되어 호출됐는지 보여준다.
어셈블리를 실행할 때마다 갱신되어 방금 실행한 어셈블리 명령어가 메모리에 어떤 영향을 줬는지 쉽게 파악할 수 있게 돕는다.
break & continue
break는 특정 주소에 중단점(breakpoint)을 설정하는 기능이고, continue는 중단된 프로그램을 계속 실행시키는 기능이다. break로 원하는 함수에 중단점을 설정하고, 프로그램을 계쏙 실행하면 해당 함수까지 멈추지 않고 실행한 다음 중단된다. 그러면 중단된 지점부터 다시 세밀하게 분석할 수 있다.
현재 중단된 start 함수부터 main 함수까지 실행시켜 보자.
run
앞의 start가 진입점부터 프로그램을 분석할 수 있도록 자동으로 중단점을 설정해줬다면, run은 단순히 실행만 시킨다. 따라서 중단점을 설정해놓지 않았다면 프로그램이 끝까지 멈추지 않고 실행된다. 지금은 main 함수에 중단점을 설정해놨기 때문에 run 명령어를 실행해도, main 함수에서 실행이 멈춘다.
+) gdb의 명령어 축약
b: break
c: continue
r: run
si: step into
ni: next instruction
i: info
k: kill
pd: pdisas
disassembly
gdb는 기계어를 디스어셈블(Disassemble)하는 기능을 기본적으로 탑재하고 있다. disassemble은 gdb가 기본적으로 제공하는 디스어셈블 명령어이다. 아래와 같이 함수 이림을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여준다.
u, nearpc, pdisassemble는 pwndbg에서 제공하는 디스어셈블 명령어 이다. 디스어셈블된 코드를 가독성 좋게 출력해준다.
navigate
함수의 중단점에 도달하면 그 지점부터는 명령어를 한 줄씩 자세히 분석해야 한다. 이때 사용하는 명령어로 ni와 si가 있다. 두 명령어 모두 어셈블리 명령어를 한 줄 실행한다는 공통점이 있다. 그러나 만약 call 등을 통해 서브루틴을 호출하는 경우 ni는 서브루틴의 내부로 들어가지 않지만, si는 서브루틴의 내부로 들어간다는 차이점이 있다. 이를 확인하기 위해 일단 main 함수에서 printf 함수를 호출하는 지점까지 실행해보자.
next instruction
ni를 입력하면, 아래와 같이 printf 함수 바로 다음으로 rip가 이동한 것을 확인할 수 있다.
step into
printf 함수를 호출하는 지점까지 다시 프로그램을 실행시킨 뒤, si를 입력하면 아래와 같이 printf 함수 내부로 rip가 이동한 것을 확인할 수 있다.
즉, 프로그램을 분석하다 함수의 내부까지 궁금할 때는 si를 사용하고 그렇지 않을 때는 ni를 사용한다. Backtrace를 보면, main 함수에서 printf를 호출했으므로 main 함수 위에 printf 함수가 쌓인 것을 볼 수 있다.
finish
step into로 함수 내부에 들어가서 필요한 부분을 모두 분석했는데, 함수의 규모가 커서 ni로는 원래 실행 흐름으로 돌아가기 어려울 수 있다. 이럴 때는 finish라는 명령어를 사용하여 함수의 끝까지 한 번에 실행할 수 있다.
examine
프로그램을 분석하다 보면 가상 메모리에 존재하는 임의 주소의 값을 관찰해야할 때가 있다. 이를 위해 gdb에서는 기본적으로 x라는 명령어를 제공한다. x를 이용하면 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 볼 수 있다.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string) and z(hex, zero padded on the left). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
예를 통해 살펴보자.
- rsp부터 80바이트를 8바이트씩 hex형식으로 출력
- rip부터 5줄의 어셈블리 명령어 출력
- 특정 주소의 문자열 출력
telescope
telescope은 pwndbg가 제공하는 메모리 덤프 기능이다. 특정 주소의 메모리 값들을 보여주는 것에서 그치지 않고, 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 값을 보여준다.
vmmap
vmmap은 가상 메모리의 레이아웃을 보여준다. 어떤 파일이 매핑된 영역일 경우, 해당 파일의 경로까지 보여준다.
gdb
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 debugee2로 디버깅 해보자.
python argv
run 명령어의 인자로 $()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있다. 다음은 파이썬에서 print 함수를 통해 출력한 값을 run 명령어의 인자로 전달하는 명령어이다.
python input
이전과 같이 $()와 함께 파이썬 코드를 입력하면 값을 입력할 수 있다. 입력값으로 전달하기 위해서는 '<<<' 문자를 사용한다. 다음은 앞서 배운 argv[1]에 임의의 값을 전달하고, 값을 입력하는 명령어이다.
'무지성 공부방 > Dreamhack SystemHacking' 카테고리의 다른 글
[System Hacking STAGE 4] Background: Calling Convention (0) | 2022.01.26 |
---|---|
[System Hacking STAGE 3] Tool: pwntools (0) | 2022.01.26 |
[System Hacking STAGE 2] shell_basic (0) | 2022.01.21 |
[System Hacking STAGE 2] Exploit Tech: Shellcode (0) | 2022.01.21 |
[System Hacking STAGE 2] Quiz: x86 Assembly 2 (0) | 2022.01.21 |