바이너리 분석 본문

Layer 7

바이너리 분석

disso1p1 2020. 7. 12. 18:29

 

//gcc -o ex ex.c -fno-stack-protector -z norelro -z execstack –no-pie
#include<stdio.h>

void setup()
{
	setvbuf(stdin, 0, 2, 0);
	setvbuf(stdout, 0, 2, 0);
	setvbuf(stderr, 0, 2, 0);
}

int main(void)
{
	setup();

	char buf[0x100];

	printf("What's your name? : ");
	gets(buf);			// Buffer Overflow

	printf("Hello, ");
	printf(buf);			// Format String Bug
	printf("!!!\n");

	printf("Last greeting : ");
	gets(buf);			// Buffer Overflow
	
	return 0;
}

 

gdb 를 켜보자.

$ gdb binary명

 

gdb ex

 

바이너리에 있는 함수 보기

pwndbg> info functions ( = i fu )

info functions

 

main 함수의 어셈블리 코드를 보기

pwndbg> disassemble main ( = disass main )

disassemble main

 

main 함수에 breakpoint 를 걸어보자.

pwndbg> b* main

 

이제 분석 ㄱ;

 

1. Stack Frame - 함수 프롤로그

run ( r )

main 함수의 시작 부분으로 왔다.

ni 또는 si 를 사용하면 다음 rip 로 넘어갈 수 있다.

 

 

 

+ ) ni 와 si 의 차이점

ni 는 call 명령어를 만났을 때 call 로 들어가는 함수를 모든 끝내고 main 함수로 돌아왔을 때의 첫 번째 코드의 주소로 넘어간다.

반면에 si 는 call 명령어를 만났을 때 call 로 들어가는 함수의 첫 번째 코드의 주소로 넘어간다.

 

ni 를 쳐보자.

ni

rsp 가 8 감소하고, rbp 가 rsp 위치에 push 된다 .. ( ret 의 인자값은 call 할 때 미리 넣어주는 것 같음 )

이것은 함수가 종료 될 때 에필로그 부분에서 leave 에서 수행하는 pop rbp 의 인자값으로 사용돼 main 함수에 들어오기 전 rbp 가 들어있다. 

 

ni

ni

rbp 를 rsp 로 덮는다. rbp 가 rsp 로 올 것이고, 즉 같은 위치에 있다.

이 부분은 rbp, base 위치를 정하는 부분이다.

 

 

2. setup 서브 함수

disassemble setup

setup 함수에서 setvbuf() 함수는 버퍼링 방식을 변경하는 함수인데,

표준입력, 표준출력, 표준에러에 사용될 버퍼를 직접 지정하지 않고( 0 ), 버퍼를 사용하지 않고 ( 2 ), 버퍼 크기를 설정하지 않는다 ( 0 ).

 

즉, 표준입력, 표준출력, 표준에러를 할 때마다 해당 버퍼가 없어 바로바로 상호작용하게 해 준다. ( CTF 에서 많이 나옴 )

 

+ ) 첫 번째 인자 : rdi, 두 번째 인자 : rsi, 세 번째 인자 : rdx, 네 번째 인자 : rcx, 다섯 번째 인자 : r8, 여섯 번째 인자 : r9, 일곱 번째 인자 : stack 내부 ...

 

 

3. plt & got

 

printf() 함수의 plt, got 로 설명하겠다. 

 

x/3gi 0x400540

printf@plt 코드이다. 

0x400540 에서 got 로 jmp 한다. 

x/x 0x600ba0

got 에는 printf@plt+6 주소가 들어있다.

ni

예상대로 printf@plt+6 으로 간다.

 

그리고 _dl_runtime_resolve_xsavec 함수로 jmp 한다.

 

si

그 후 _dl_fixup 함수를 호출한다.

 

si

_dl_lookup_symbol_x 함수 호출

 

si

do_lookup_x 함수 호출

 

 

_dl_name_match_p 함수 호출

 

strcmp 함수 호출

 

 

check_match 함수 호출

 

_dl_runtime_resolve_xsavec 까지 return

 

printf 함수 호출

 

 

vfprintf 함수 호출

 

 

buffered_vfprintf 함수 호출

 

 

다시 vfprintf 함수 호출

 

 

strchrnul 함수 호출

 

strchrnul 함수 return

 

 

_IO_default+xsputn 함수 호출

 

_IO_default+xsputn 함수 return

 

vfprintf 함수 return

 

 

_IO_file_xsputn 함수 다시 호출

 

_IO_file_overflow 함수 호출

 

 

_IO_do_write 함수로 jmp

 

_IO_do_write 함수 return

 

_IO_file_write 함수 호출

 

write 함수 호출

 

write 함수 return

 

_IO_file_xsputn 함수 return

 

buffered_vfprintf 함수 return

 

vfprintf 함수 return

 

printf() 함수 return 

 

이렇게 된다 ...

 

 

이 과정이 모두 끝나면 그 다음에 다시 printf() 함수를 사용할 때는 이 과정을 거치지 않고, got 에 printf 실제 주소를 적어놓았기 때문에

plt -> got -> printf() 이런 식으로 작동한다.

 

이 과정이 끝난 후 got 의 값을 보면, 

printf() 함수의 실제 주소가 들어있다.

 

 

4. Stack Frame - 함수 에필로그

 

 

main 함수의 에필로그 과정을 통해 __libc_start_main 으로 return 된다.

 

eax ( return 주소 넣은 자리 ) 에 0 을 대입한다.

 

 

leave는

mov rsp, rbp

pop rbp

를 수행한다고 보면 된다.

 

64bit 라 그런진 모르겠는데 이 바이너리는 sub 나 add 할 상황이 없어서 rbp 와 rsp 가 원래 같다.

 

어쨌든 leave 를 실행하면,

rbp 는 이전 rbp 로 바뀌었고, rsp 가 8 만큼 증가해 __libc_start_main 함수의 다음 코드의 주소를 가르키게 한다.

즉, 0x7ffff7a2d840 가 ret 의 인자값으로 사용돼 main 함수에 들어오기 전( __libc_start_main ) 함수로 돌아갈 수 있게 한다.

 

이렇게 main 함수가 끝난다.

Comments