gdb & pwndbg

신우빈·2022년 7월 5일
0

Dreamhack

목록 보기
6/7
post-thumbnail

Intro.

버그(bug) : 실수로 발생한 프로그램의 결함
디버거(Debugger) : 버그를 찾아주는 도구
- 프로그램을 어셈블리 코드 단위로 실행하면서, 실행결과를 사용자에게 보여줌
- 추상적으로 생각한 아이디어의 결과를 직관적으로 확인 -> 작성한 코드의 문제점을 명확하게 찾을 수 있다.
- 즉, 버그 탐색의 효율을 높임.

GDB

  • 리눅스의 대표 Debugger 중 하나
  • 여러 plugin이 존재하는데 그 중 바이너리 분석 용도로 사용되는 Plugin 들은 아래와 같다.
    - gef
    • peda
    • pwngdb
    • pwndbg
  • 우리는 pwndbg를 사용할 것

    플러그인
    - 기본 소프트웨어를 지원해서 특수한 기능을 확장할 수 있도록 설계된 부속 프로그램.
    바이너리 분석
    - 0과 1로 이루어진 이진 값의 의미를 분석하는 것
    - 바이너리 코드를 역어셈블(Disassemble)하여 값으로만 존재하는 데이터가 어떤 명령어(Instruction)이며 어떤 피연산자(Operand)를 의미하는지 알 수 있다.

실습예제

여기 사용된 -no-pie flag는 pie를 해제해주는 것이다.
만약 pie를 사용한다면, 보호기법이 적용되어, 실행할 때마다 바이너리 주소가 랜덤화된다.

코드 설명

  • gcc -o debugee debugee.c : debugee.c라는 파일로 debugee라는 명을 가진 실행 파일을 만든다.
  • gdb debugee : debugee 를 실행했을 때, 프로그램 내부에서 어떤 일이 일어나고 있는지 보여줌
  • gdq에서 나오려면 q 작성 후 Enter 입력하면 됨.

About ELF

리눅스는 실행 파일의 형식으로 ELF(Executable and Linkable Format) 를 규정
ELF : 헤더와 섹션들로 구성

  • 헤더 : 실행에 필요한 여러 정보
  • 섹션 : 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터

    헤더 중 진입점(Entry Point, EP) 라는 필드가 있는데, 운영체제는 ELF를 실행할 때,
    진입점의 값부터 프로그램을 실행

아래는 readelf -h 로 확인한 결과다.

debugee라는 파일의 진입점은 0x401050 이다.

명령어 - Start

진입점부터 프로그램을 분석할 수 있게 해주는 gdb 의 명령어
우선 그러기 위해 debugee 파일을 gdb를 이용해 실행하자.
1.

  1. start를 해보자

    이 결과를 보니, Dreamhack에서 표시한 내용과 달랐다.
    그 이유를 찾아보니, 우리가 실행한 결과에서 breakpoint는 in main() 이며,
    Dreamhack은 start()다. 그래서 Dreamhack 처럼 결과를 보기위해 구글링...

  2. b _start : break를 _start로 지정 -> r : run, break 지점을 변경 후 실행

< b _start 에 대한 결과 >

< r 결과 >

제일 마지막 부분을 통해, main 함수로 넘어감을 알 수 있다.
자세한 내용은 _start() 에서 확인. 아직 완전히 이해하지 못하여 작성 X

결론

  • gdb start를 통해 진입점부터 프로그램을 분석할 수 있다.
    - b _start 미사용 시 breakpoint 가 main 으로 설정됨.
    • 이에, readelf 로 확인한 진입점과 값이 다름.
    • 그 이유는 _start 가 main 이전에 실행되기 때문
  • start 에 대한 결과로 나오는 영역들은 Context 에서 공부.
    - DISASM 영역은 rip 부터 여러 줄에 걸쳐 Disassembled 된 결과를 보여줌.
    • DISASM 영역에 있는 ► 가 가리키는 주소는 현재 rip의 값이다.
    • 중단점을 _start 로 지정한 경우, ► 의 주소와 진입점의 주소는 일치한다.

Context

프로그램은 실행되면서 레지스터를 비롯한 여러 메모리에 접근

따라서, 디버거를 이용하여 프로그램의 실행 과정을 자세히 관찰하려면 컴퓨터의 각종 메모리를 한눈에 파악할 수 있는 것이 좋다.

맥락(Context) : pwndbg 에서 주요 메모리들의 상태를 프로그램이 실행되고 있는 "맥락".
이를 가독성 있게 표현할 수 있는 인터페이를 가지고 있는 것이 pwndbg

Context의 4가지 영역

어셈블리를 실행할 때마다 갱신 -> 어셈블리 명령어가 메모리에 어떤 영향을 주는지 쉽게 파악

  1. Registers : 레지스터의 상태를 보여줌
  2. Disasm : rip 부터 여러 줄에 걸쳐 디스어셈블된 결과를 보여줌
  3. Stack : rsp 부터 여러 줄에 걸쳐 스택의 값들을 보여줌
  4. Backtrace : 현재 rip 에 도달할 때까지 어떤 함수들이 중첨되어 호출됐는지 보여줌

    디스어셈블된 결과 = 기계어를 어셈블리로 바꾼 결과

Break & Continue

gdb 이용하여 프로그램 분석할 때, 일반적으로 전체 프로그램 중 아주 일부분의 동작에만 관심
=> 비효율적인 분석 방법이 될 수 있다.

위와 같은 문제를 해결하기 위해 디버거에는 breakcontinue 라는 기능을 제공

  • break : 특정 주소에 중단점(breakpoint)를 설정
  • continue : 중단된 프로그램을 계속 실행시키는 기능

    즉, break로 원하는 함수에 중단점 설정 -> 프로그램 계속 실행
    = 해당 함수까지 멈추지 않고 실행 후 중단.
    = 중단된 지점부터 다시 세밀하게 분석 가능

Run

start : 진입점부터 프로그램을 분석할 수 있도록 자동으로 중단점 설정
run : 단순히 실행만 시킴 = 중단점 미설정 시 프로그램이 끝까지 멈추지 않고 실행

Etc.

Disassembly

gdb 는 프로그램을 어셈블리 코드 단위로 실행하고 결과를 보여줌
프로그램의 코드는 기계어로 구성
gdb 는 기계어를 디스어셈블하는 기능을 기본적으로 탑재
추가로, pwndbg에는 디스어셈블된 결과를 가독성 좋게 출력해주는 기능 탑재

disassemble 함수명

gdb 가 기본적으로 제공하는 디스어셈블 명령어.
함수 이름을 인자로 전달하면 해당 함수가 반환될 때 까지 전부 디스어셈블하여 보여줌.

예시
gdb debugee -> disassemble main 실행

결과

가독성 향상 명령어

u

nearpc

pd

관찰하고자 하는 함수의 중단점에 도달했으면, 그 지점부터는 명령어를 한 줄씩 자세히 분석.
이때 사용하는 명령어에는 ni 와 si 가 존재.

  • 공통점
    - 두 명령어 모두 어셈블리 명령어를 한 줄 실행한다
  • 만약 call 등을 통해 서브루틴을 호출하는 경우
    - ni : 서브루틴의 내부로 들어가지 않는다.
    • si : 서브루틴의 내부로 들어간다.

예시
main 함수에서 printf 함수를 호출하는 지점까지 실행

printf 함수를 호출하는 지점을 찾는 방법은 disassemble main 을 이용한다.
Dreamhack 의 예제 같은 경우는 main+57 에 위치함을 disassemble main 을 통해 확인

위의 사진을 통해 나의 printf 는 main+61 에 위치함을 알 수 있다.

결과

ni 입력 결과


b *main+61 이후 ni 로 실행해본 결과, rip 가 printf 바로 다음으로 넘어갔음을 확인.

printf를 실행했는데 아무 문자열도 출력되지 않는 이유

printf 가 출력하고자 하는 문자열은 stdout 의 버퍼에서 대기 후 출력

버퍼 : 데이터가 목적지로 이동하기 전에 잠시 저장되는 장소

stdout 버퍼는 특정 조건이 만족됐을 때만 데이터를 목적지로 이동.

  • 프로그램이 종료될 때
  • 버퍼가 가득 찼을 때
  • fflush 와 같은 함수로 버퍼를 비우도록 명시했을 때
  • 개행문자가 버퍼에 들어왔을 때

    fflush 함수 : 시스템이 지정된 출력 stream 과 연관된 버퍼를 비우게 함.
    stream : 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름.
    즉, 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할
    한 방향으로만 통신이 가능하다는 특징이 존재.

         

si 입력 결과


printf 함수 내부로 rip 가 이동했음을 확인

프로그램을 분석하다가, 어떤 함수의 내부까지 궁금할 때는 si, 그렇지 않으면 ni 사용

위 결과를 통해 main 함수에서 printf 를 호출한 것이므로
main 함수 위에 printf 가 쌓인 것을 확인 할 수 있다.

finish

si 이용해 함수 내부까지 필요한 부분 모두 분석했는데, 함수의 규모가 커서
ni 로는 원래 실행 흐름으로 돌아가기 어려울 때 사용.

rip 가 printf 바로 다음인 것으로 보아 원래 흐름으로 돌아왔다고 볼 수 있음.

examine

가상 메모리에 존재하는 임의의 주소의 값을 관측해야할 때 사용
x 라는 명령어를 사용한다.
사용 시,

  • 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩하여 확인
  • Format letter
    - o(octal)
    • x(hex)
    • d(decimal)
    • u(unsigned decimal)
    • t(binary)
    • f(float)
    • a(address)
    • i(instruction)
    • c(char)
    • s(string)
    • z(hex, zero padded on the left)
    • b(Size letters)(byte)
    • h(halfword)
    • w(word)
    • g(giant, 8 bytes)

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

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

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

Telescope

pwndbg가 제공하는 강력한 메모리 덤프 기능
특정 주소의 메모리 값들을 보여주는 것

  • 메모리가 참조하고 있는 주소를 재귀적으로 탐색하여 값을 보여줌.

    메모리 덤프 : System 의 물리 Memory 를 File 형태로 저장하는 방법
    해당 File 의 구조는 실제 Memory 구조와 동일

예시

Vmmap

가상 메모리의 레이아웃을 보여줌
어떤 파일이 매핑된 영역 -> 해당 파일의 경로까지 보여줌

파일 매핑 : 어떤 파일을 메모리에 적재하는 것
아래 메모리 레이아웃에서 아래 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 / python

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 가 무엇인가

gdb / python argv

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

예시
파이썬에서 print 함수를 통해 출력한 값을 run 명령어의 인자로 전달하는 명령어

추가 작성해야할 내용

  • run 명령어의 인자 역할
  • \xff*100 이 무엇인지?

gdb / python input

입력값으로 전달하기 위해서 <<< 문자 사용

예시
argv[1]에 임의의 값을 전달하고, 값을 입력하는 명령어

0개의 댓글