※본 글은 드림핵 강좌를 공부, 실습한 내용입니다 ※
gdb(GNU debugger): 리눅스 내의 대표적인 디버거
gdb 플러그인 중 바이너리 분석 용도로 사용되는 플러그인인 pwndbg 설치 필요함
https://github.com/pwndbg/pwndbg
예제코드 1
// 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; }
$ gcc -o debugee debugee.c
$ gdb debugee
실습 예제를 vim을 통해서 작성한 후 gcc를 통해서 c파일을 실행파일로 만들었다. 그 후 gdb debugee라는 명령어를 통해서 이전에 설치한 pwndbg를 실행한다.
registers, disasm, stack, backtrace 순
보통 프로그램을 분석할 경우에 일부분의 동작을 분석하는 경우엔 일부분의 동작을 집중적으로 분석함
우리 예제에서는 main 함수가 분석의 대상이라고 가정됨
이 상황에서 진입점 to main 이라고 했을경우에 한줄씩 실행을 해야 한다면 너무 번거로운 일이 되어버림
진입점 to main을 하려면 그 main이 위치한 메모리 주소에 break point를 설정해야 한다.
내 환경에서는 start 명령어를 실행했을때, 바로 main함수로 들어가기에 진입점 부분과 main함수 둘다 위치를 breakpoint를 통해 파악해본다.
breakpoint를 설정하기 위해서는 b *_start를 입력하면 된다.
_start와 main에 Breakpoint가 설정됨을 볼 수 있다 (이것을 출력하기 위해서는 info b를 입력)
breakpoint를 설정하고 c 나 continue를 입력하면 설정된 중단점 전까지 출력된다.
이 명령어는 단순히 실행시키는 명령어로 중단점까지 실행된다. run을 돌리고 c를 누르면 다음 중단점까지 진행되며 분석할 수 있다.
프로그램의 코드는 기계어로 이루어져 있는데, gdb는 이 기계어를 디스어셈블하는 기능을 기본적으로 탑재하고 있다.
그리고 우리가 dbg 플러그인을 활용한 pwn도구인 pwndbg에는 이것을 보기 좋게 하는 기능을 지원한다.
위의 사진은 main함수를 어셈블리 언어로 보기 좋게 출력한 것을 볼 수 있다.
가독성을 좋게 하는 명령어로 u, nearpc이 더 있다.
printf를 출력하는 지점 까지 breakpoint 설정
b *main+61
ni를 입력하면 printf다음으로 rip가 넘어갈 수 있다.
si는 함수 내부로 들어가는 명령어로 현재 위치에서 다음 명령어로 rip가 넘어가는 ni와 다르게 si는 함수의 내부까지 분석할 수 있다.
printf에 대해서 si를 실행한경우 Backtrace 영역을 보면 main 위 main+66위에 printf 함수가 쌓인것을 알 수 있다.
프로그램을 분석하다 보면 가상 메모리에 존재하는 임의 주소의 값이 필요할 때가 있다고 한다.
이를 위해서 gdb에서는 기본적으로 x 명령어를 통해 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 볼 수 있다.
o: octal (8진법)
x: hex (16진법)
u: unsigned decimal (부호화되지 않은 10진법)
t: binary (2진법)
f: float (float형 실수 표현)
a: address (주소)
i: instruction (어셈블리 명령어)
c: char (문자형)
s: string(문자열)
z: hex and zero padded on the left(zero padding 적용)
b: byte
h: halfword
w: word
g: giant 8byte
메모리 덤프 기능
프로세스의 메모리를 정해진 덤프 포맷에 따라 기록한 파일
컴퓨터 프로그램이 특정 시점에 작업중이던 메모리 상태를 기록한것, 보통 프로그램이 비정상적으로 종료되었을때 만들어짐
추가적인 검사를 위해서 많은 양의 메모리 데이터를 저장함
vmmap은 가상 메모리의 레이아웃을 보여주는 기능
어떤 파일이 매핑된 영역일 경우 해당 파일의 경로까지 보여줌
어떤 파일을 메모리에 적재하는 것
.so(Shared Object)라고 끝나는 파일들이 해당 메모리에 매핑된 내용이다.
ELF가 실행될때, 먼저 ELF의 코드와 여러 데이터를 가상 메모리에 매핑하고 해당 ELF에 링크된 공유 오브젝트를 추가로 메모리로 매핑한다.
so는 윈도우의 dll과 대조되는 개념으로 자주 사용되는 함수들을 미리 컴파일 해둔것이다.
C언어의 printf, scanf등이 리눅스에선 libary C인 libc에 구현되었다. 공유 오브젝트에 이미 구현된 함수를 호출할 때는 매핑된 메모리(.so)에 존재하는 함수를 대신 호출된다.
숫자와 알파벳이 아닌 값을 입력하는 경우
이러한 값은 이용자가 직접 입력할 수 없는 값이기 때문에 파이썬코드를 통해 입력값을 생성하고 사용할 수 있어야 한다.
예제코드 2
// 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; }
프로그램 기능: 프로그램 인자로 전달된 값과 이용자로부터 입력받은 값을 출력
pwndbg> r $(python -c 'print "\xff"*100') <<< $(python -c 'print "dreamhack"') # argv[1]에 임의의 값 전달후 name변수 메모리에 값 입력