pwndbg로 ELF 바이너리 분석해보기

·2022년 11월 9일
0
post-thumbnail

Background Knowledge

1. Stack Frame

스택 영역에 차례대로 저장되는 함수의 호출 정보를 Stack Frame이라고 한다.
Stack Frame은 지역 변수, 인수 매개 변수, 함수의 반환 주소값 등과 같이 구성된다.
Stack FrameBP, SP 레지스터를 사용하여 Stack Frame에 구성된 스택을 접근한다.

BP 레지스터: 스택프레임의 바닥을 가르킨다. 지역 변수나 인자에 참조할 때 사용된다.
SP 레지스터: 스택의 크기를 조정할 때 사용한다.

이러한 BP, SP 레지스터를 이용한 함수 프롤로그, 에필로그가 있다.

함수 프롤로그:
베이스 포인터를 스택에 저장하고 현재 스택 포인터를 베이스 포인터에 저장한다.
함수 에필로그:
현재 스택 포인터를 베이스 포인터로 복귀, 베이스 포인터를 복귀한 뒤 처음 호출한 지점으로 돌아간다.

2. PLT & GOT

PLT: 외부 프로시저를 연결해주는 테이블이다.
(프로시저: 함수와 비슷하지만, 함수와 달리 리턴 값을 날리지 않는다.)
PLT를 통해 다른 라이브러리에 있는 프로시저를 호출 할 수 있다.

GOT: PLT가 참조하는 테이블
프로시저들의 주소가 들어있다.

printf() 같은 함수는 우리가 만들거나 구현하지 않아도 쓸 수 있다.
printf() 선언이 들어 있는 stdio.h#include 했기 때문이다.

그러면, printf() 의 구현 코드는 어디에 있는 걸까? printf()를 구현한 오브젝트 파일에 있을 것이다.
이러한 오브젝트 파일이 모여 있는 곳을 라이브러리 라고 한다.
우리가 이러한 함수들을 사용하는 것은 외부 라이브러리에서 정의되고 만들어진 함수들을 사용하는 것이다.

우리가 만든 코드는 컴파일 후 링킹을 한다.
이 때, 우리가 만든 코드와 라이브러리와 연결을 하는 작업을 링킹이라고 한다.

링킹 방식 중에서 Static Link 방식과 Dynamic Link 방식이 있는데,Dynamic Link 방식이 PLT, GOT를 사용한다.

Static Link 방식:
gcc에 -static 옵션을 줌으로서 사용할 수 있다.

정적 링크방식으로, 실행 파일 안에 라이브러리 동작 코드가 포함된다.
이 때문에, 파일 실행 만으로 라이브러리 함수를 사용할 수 있다.

그러나, 라이브러리 동작 코드가 실행 파일에 포함되면 실행 파일에 크기는 커질 수 밖에 없고, 메모리 관리도 비효율적이게 된다.

Dynamic Link 방식:
gcc에 아무런 옵션이 없으면 기본으로 Dynamic Link 방식이 사용된다.

동적 링크방식으로, 실행 파일 안에 라이브러리 동작 코드가 포함되지 않는다.
해당 프로그램을 실행하기 위한 실행파일 뿐 아니라 라이브러리 파일(.dll .so)도 필수로 있어야 한다.

하지만 라이브러리 내용이 코드에 포함하지 않기 때문에
파일의 크기가 작고 메모리를 보다 더 적게 먹기 때문에 효율적이다.

따라서 공유 라이브러리 함수을 사용하려면
PLT, GOT 로 라이브러리에 있는 프로시저 주소를 호출해야 한다.

공유 라이브러리 함수 처음 사용 시:
PLT로 함수 호출 -> GOT 참조 -> 다시 PLT 호출 -> _dll_runtime_resolve 함수를 실행하여 해당하는 함수 주소를 GOT에 저장 -> 해당 함수로 점프

2번째 사용 부터: PLT로 함수 호출 -> GOT 참조 -> GOT에 쓰여진 그 함수의 주소가 이미 존재하므로 해당 함수로 점프.

3. asm 코드 흐름 간단하게 보기

0x00000000004006e7 <+0>: push rbp 
0x00000000004006e8 <+1>: mov rbp,rsp
main 함수 프롤로그

0x00000000004006eb <+4>: sub rsp,0x100
rsp에 0x100을 뺌 (160바이트의 메모리 공간을 확보)

0x00000000004006f2 <+11>: mov eax,0x0
eax를 0으로 초기화

0x00000000004006f7 <+16>: call 0x400686 <setup>
setup 함수 call

0x0000000000400686 <+0>: push rbp 
0x0000000000400687 <+1>: mov rbp,rsp
setup 함수 프롤로그

0x000000000040068a <+4>: mov rax,QWORD PTR [rip+0x20055f]        
0x0000000000400691 <+11>: mov ecx,0x0 
0x0000000000400696 <+16>: mov edx,0x2 
0x000000000040069b <+21>: mov esi,0x0
0x00000000004006a0 <+26>: mov rdi,rax 
0x00000000004006a3 <+29>: call 0x400570 <setvbuf@plt>

각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call

0x00000000004006a8 <+34>: mov rax,QWORD PTR [rip+0x200531] 
0x00000000004006af <+41>: mov ecx,0x0 
0x00000000004006b4 <+46>: mov edx,0x2 
0x00000000004006b9 <+51>: mov esi,0x0 
0x00000000004006be <+56>: mov rdi,rax 
0x00000000004006c1 <+59>: call 0x400570 <setvbuf@plt> 
각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call

0x00000000004006c6 <+64>: mov rax,QWORD PTR [rip+0x200533]
0x00000000004006cd <+71>: mov ecx,0x0 
0x00000000004006d2 <+76>: mov edx,0x2 
0x00000000004006d7 <+81>: mov esi,0x0 
0x00000000004006dc <+86>: mov rdi,rax 
0x00000000004006df <+89>: call 0x400570 <setvbuf@plt>

각 파라미터에 stdin, 0x0, 0x2, 0x0을 넣고 setvbuf 함수 call

0x00000000004006e4 <+94>: nop
아무것도 안함(no-operation)

0x00000000004006e5 <+95>: pop rbp
함수 복귀

0x00000000004006e6 <+96>: ret
함수 종료 후, call 다음 명령줄로 이동

0x00000000004006fc <+21>: mov edi,0x400804
"What's your name :" 문자열의 주소를 edi에 대입

0x0000000000400701 <+26>: mov eax,0x0
eax를 0으로 초기화

0x0000000000400706 <+31>: call 0x400540 <printf@plt>
printf call - "What's your name :"이 출력됨

0x000000000040070b <+36>: lea rax,[rbp-0x100]
[rbp-0x100]의 주솟값을 rax에 대입, rax는 gets의 입력값이 된다.

0x0000000000400712 <+43>: mov rdi,rax 
rax값을 rdi에 대입

0x0000000000400715 <+46>:    mov    eax,0x0
eax를 0으로 초기화

0x000000000040071a <+51>:    call   0x400560 <gets@plt>
gets 함수 call

0x000000000040071f <+56>:    mov    edi,0x400818
"Hello, " 문자열의 주소를 edi에 대입

0x0000000000400724 <+61>:    mov    eax,0x0
eax를 0으로 초기화

0x0000000000400729 <+66>:    call   0x400540 <printf@plt>
printf 함수 call - "Hello, "이 출력된다.

0x0000000000400742 <+91>:    mov    edi,0x400820
"!!!" 문자열의 주소를 edi에 대입

0x0000000000400747 <+96>:    call   0x400530 <puts@plt>
puts 함수 call - "!!!" 이 출력된다.

0x000000000040074c <+101>:   mov    edi,0x400824
"Last Greeting : " 문자열의 주소를 edi에 대입

0x0000000000400751 <+106>:   mov    eax,0x0
eax를 0으로 초기화

0x0000000000400756 <+111>:   call   0x400540 <printf@plt>
printf 함수 call - "Last Greeting : " 이 출력된다.

0x40075b <main+116>    lea    rax, [rbp - 0x100]
[rbp-0x100]의 주솟값을 rax에 대입, rax는 gets의 입력값이 된다.

0x400765 <main+126>    mov    eax, 0
rax를 0으로 초기화

0x40076a <main+131>    call   gets@plt <gets@plt>
gets 함수 call

0x40076f <main+136>    mov    eax, 0
eax를 0으로 초기화

0x400774 <main+141>    leave
0x400775 <main+142>    ret
main 함수 에필로그

gdb(pwndbg) 사용법

1. gdb ./filename or gdb 실행 후 file "./filename"

"filename" 바이너리를 로드한다.
추가) gdb --args ./filename all is well 식으로 프로그램 인자를 줄 수 있다.

2. disas{s} "함수명"

함수명의 어셈블리 코드를 본다.

3. b*메모리주소값

해당 메모리 주소값에 break를 설정한다.

3-1. b "함수명"

함수 이름을 적어 break를 설정 할 수도 있다.

4. run

프로그램을 실행한다.

breakpoint가 없으면 프로그램이 완전히 실행된 후 종료되고,
breakpoint가 있으면 breakpoint 걸린 코드 직전까지 실행되고, 디버깅 값을 보여준다.

5. si, ni

공통점:

어셈블리어 한 줄을 실행한다.

다른점:

si - call이 실행되면 call 안의 함수 코드 한줄로 넘어간다.
ni - call이 실행되면 call 함수를 끝마치고 다음 코드 한줄로 넘어간다.

6. s, n

공통점:

소스 한 줄을 실행한다.

다른점:

s - call이 실행되면 call 안의 함수 코드 한줄로 넘어간다.
n - call이 실행되면 call 함수를 끝마치고 다음 코드 한줄로 넘어간다.

7. reg

레지스터 값을 출력한다.

7-1. reg info "$regName"

특정 레지스터 값을 출력한다.

8. p[/x or d][value or defined name]

값을 원하는 형식으로 출력해준다.

9. x/[options]

메모리를 확인한다.

옵션:

o: 8진법
x: 16진법
u: 10진법
t: 2진법 으로 보여준다.

b: 1byte
h: 2byte
w: 4byte
g: 8byte 단위로 보여준다.

i: 역어셈블된 명령어의 명령 메모리를 볼 수 있다.
c: ASCII 표의 바이트를 자동으로 볼 수 있다.
s: 문자 데이터의 전체 문자열을 보여 준다.

10. vmmap [주소]

그냥 입력하면 프로그램의 memory map을 보여주고, 주소까지 입력하면 그 주소의 대응하는 위치와 정보도 알려준다.

11. find "시작 주소" "끝 주소 또는 오프셋", "검색할 값"

메모리 구역을 정해 주어진 값을 검색해준다.

0개의 댓글