버그(bug) : 실수로 발생한 프로그램의 결함
디버거(Debugger) : 버그를 찾아주는 도구
- 프로그램을 어셈블리 코드 단위로 실행하면서, 실행결과를 사용자에게 보여줌
- 추상적으로 생각한 아이디어의 결과를 직관적으로 확인 -> 작성한 코드의 문제점을 명확하게 찾을 수 있다.
- 즉, 버그 탐색의 효율을 높임.
플러그인
- 기본 소프트웨어를 지원해서 특수한 기능을 확장할 수 있도록 설계된 부속 프로그램.
바이너리 분석
- 0과 1로 이루어진 이진 값의 의미를 분석하는 것
- 바이너리 코드를 역어셈블(Disassemble)하여 값으로만 존재하는 데이터가 어떤 명령어(Instruction)이며 어떤 피연산자(Operand)를 의미하는지 알 수 있다.
실습예제
여기 사용된 -no-pie flag는 pie를 해제해주는 것이다.
만약 pie를 사용한다면, 보호기법이 적용되어, 실행할 때마다 바이너리 주소가 랜덤화된다.
코드 설명
- gcc -o debugee debugee.c : debugee.c라는 파일로 debugee라는 명을 가진 실행 파일을 만든다.
- gdb debugee : debugee 를 실행했을 때, 프로그램 내부에서 어떤 일이 일어나고 있는지 보여줌
- gdq에서 나오려면 q 작성 후 Enter 입력하면 됨.
리눅스는 실행 파일의 형식으로 ELF(Executable and Linkable Format) 를 규정
ELF : 헤더와 섹션들로 구성
헤더 중 진입점(Entry Point, EP) 라는 필드가 있는데, 운영체제는 ELF를 실행할 때,
진입점의 값부터 프로그램을 실행
아래는 readelf -h 로 확인한 결과다.
debugee라는 파일의 진입점은 0x401050 이다.
진입점부터 프로그램을 분석할 수 있게 해주는 gdb 의 명령어
우선 그러기 위해 debugee 파일을 gdb를 이용해 실행하자.
1.
start를 해보자
이 결과를 보니, Dreamhack에서 표시한 내용과 달랐다.
그 이유를 찾아보니, 우리가 실행한 결과에서 breakpoint는 in main() 이며,
Dreamhack은 start()다. 그래서 Dreamhack 처럼 결과를 보기위해 구글링...
b _start : break를 _start로 지정 -> r : run, break 지점을 변경 후 실행
< b _start 에 대한 결과 >
< r 결과 >
제일 마지막 부분을 통해, main 함수로 넘어감을 알 수 있다.
자세한 내용은 _start() 에서 확인. 아직 완전히 이해하지 못하여 작성 X
프로그램은 실행되면서 레지스터를 비롯한 여러 메모리에 접근
따라서, 디버거를 이용하여 프로그램의 실행 과정을 자세히 관찰하려면 컴퓨터의 각종 메모리를 한눈에 파악할 수 있는 것이 좋다.
맥락(Context) : pwndbg 에서 주요 메모리들의 상태를 프로그램이 실행되고 있는 "맥락".
이를 가독성 있게 표현할 수 있는 인터페이를 가지고 있는 것이 pwndbg
어셈블리를 실행할 때마다 갱신 -> 어셈블리 명령어가 메모리에 어떤 영향을 주는지 쉽게 파악
디스어셈블된 결과 = 기계어를 어셈블리로 바꾼 결과
gdb 이용하여 프로그램 분석할 때, 일반적으로 전체 프로그램 중 아주 일부분의 동작에만 관심
=> 비효율적인 분석 방법이 될 수 있다.
위와 같은 문제를 해결하기 위해 디버거에는 break 와 continue 라는 기능을 제공
즉, break로 원하는 함수에 중단점 설정 -> 프로그램 계속 실행
= 해당 함수까지 멈추지 않고 실행 후 중단.
= 중단된 지점부터 다시 세밀하게 분석 가능
start : 진입점부터 프로그램을 분석할 수 있도록 자동으로 중단점 설정
run : 단순히 실행만 시킴 = 중단점 미설정 시 프로그램이 끝까지 멈추지 않고 실행
gdb 는 프로그램을 어셈블리 코드 단위로 실행하고 결과를 보여줌
프로그램의 코드는 기계어로 구성
gdb 는 기계어를 디스어셈블하는 기능을 기본적으로 탑재
추가로, pwndbg에는 디스어셈블된 결과를 가독성 좋게 출력해주는 기능 탑재
gdb 가 기본적으로 제공하는 디스어셈블 명령어.
함수 이름을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여줌.
예시
gdb debugee -> disassemble main 실행
결과
u
nearpc
pd
관찰하고자 하는 함수의 중단점에 도달했으면, 그 지점부터는 명령어를 한 줄씩 자세히 분석.
이때 사용하는 명령어에는 ni 와 si 가 존재.
예시
main 함수에서 printf 함수를 호출하는 지점까지 실행
printf 함수를 호출하는 지점을 찾는 방법은 disassemble main 을 이용한다.
Dreamhack 의 예제 같은 경우는 main+57 에 위치함을 disassemble main 을 통해 확인
위의 사진을 통해 나의 printf 는 main+61 에 위치함을 알 수 있다.
결과
b *main+61 이후 ni 로 실행해본 결과, rip 가 printf 바로 다음으로 넘어갔음을 확인.
printf 가 출력하고자 하는 문자열은 stdout 의 버퍼에서 대기 후 출력
버퍼 : 데이터가 목적지로 이동하기 전에 잠시 저장되는 장소
stdout 버퍼는 특정 조건이 만족됐을 때만 데이터를 목적지로 이동.
fflush 함수 : 시스템이 지정된 출력 stream 과 연관된 버퍼를 비우게 함.
stream : 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름.
즉, 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할
한 방향으로만 통신이 가능하다는 특징이 존재.
printf 함수 내부로 rip 가 이동했음을 확인
프로그램을 분석하다가, 어떤 함수의 내부까지 궁금할 때는 si, 그렇지 않으면 ni 사용
위 결과를 통해 main 함수에서 printf 를 호출한 것이므로
main 함수 위에 printf 가 쌓인 것을 확인 할 수 있다.
si 이용해 함수 내부까지 필요한 부분 모두 분석했는데, 함수의 규모가 커서
ni 로는 원래 실행 흐름으로 돌아가기 어려울 때 사용.
rip 가 printf 바로 다음인 것으로 보아 원래 흐름으로 돌아왔다고 볼 수 있음.
가상 메모리에 존재하는 임의의 주소의 값을 관측해야할 때 사용
x 라는 명령어를 사용한다.
사용 시,
예시
1. rsp 부터 80바이트를 8바이트씩 hex 형식으로 출력
2. rip 부터 10줄의 어셈블리 명령어 출력
ㅇ
3. 특정 주소의 문자열 출력
pwndbg가 제공하는 강력한 메모리 덤프 기능
특정 주소의 메모리 값들을 보여주는 것
메모리 덤프 : System 의 물리 Memory 를 File 형태로 저장하는 방법
해당 File 의 구조는 실제 Memory 구조와 동일
예시
가상 메모리의 레이아웃을 보여줌
어떤 파일이 매핑된 영역 -> 해당 파일의 경로까지 보여줌
파일 매핑 : 어떤 파일을 메모리에 적재하는 것
아래 메모리 레이아웃에서 아래 3개가 매핑된 파일
- /home/uniasus/debugee
- /usr/lib/x86_64-linux-gnu/lib-2.31.so
- /usr/lib/x86_64-linux-gnu/ld-2.31.so
예시
리눅스에서 ELF 실행할 때,
ELF의 코드와 여러 데이터를 가상 메모리에 매핑
-> 해당 ELF 에 링크된 공유 오브젝트를 추가 메모리에 매핑
공유 오브젝트(Shared Object, so)
- 자주 사용되는 함수들을 미리 컴파일해둔 것
- 윈도우의 DLL(Dynamic Link Library) 과 대응되는 개념
- 라이브러리 : 표준화된 함수 및 데이터 타입을 만들어서 모아놓은 것- C언어의 printf, scanf 등이 리눅스에서는 libc 에 구현
- so 에 이미 구현된 함수를 호출할 때는 매핑된 메모리에 존재하는 함수를 대신 호출
gdb를 통해 디버깅할 때 직접 입력할 수 없는 경우가 존재.
예시
숫자와 알파벳이 아닌 값을 입력하는 경우.
이러한 값은 이용자가 직접 입력할 수 없는 값 -> 파이썬으로 입력 값 생성하여 사용
아래 실습은 프로그램의 인자로 전달된 값과 이용자로부터 입력받은 값을 출력하는 예제
#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;
}
추가 작성해야할 내용
- main 의 괄호에 들어가는 것들이 무엇을 의미하는가
- memset 이 무엇인가
- read 가 무엇인가
run 명령어의 인자로 $()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있다.
예시
파이썬에서 print 함수를 통해 출력한 값을 run 명령어의 인자로 전달하는 명령어
추가 작성해야할 내용
- run 명령어의 인자 역할
- \xff*100 이 무엇인지?
입력값으로 전달하기 위해서 <<< 문자 사용
예시
argv[1]에 임의의 값을 전달하고, 값을 입력하는 명령어