뇌
바이너리 분석 본문
//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명
바이너리에 있는 함수 보기
pwndbg> info functions ( = i fu )
main 함수의 어셈블리 코드를 보기
pwndbg> disassemble main ( = disass main )
main 함수에 breakpoint 를 걸어보자.
pwndbg> b* main
이제 분석 ㄱ;
1. Stack Frame - 함수 프롤로그
main 함수의 시작 부분으로 왔다.
ni 또는 si 를 사용하면 다음 rip 로 넘어갈 수 있다.
+ ) ni 와 si 의 차이점
ni 는 call 명령어를 만났을 때 call 로 들어가는 함수를 모든 끝내고 main 함수로 돌아왔을 때의 첫 번째 코드의 주소로 넘어간다.
반면에 si 는 call 명령어를 만났을 때 call 로 들어가는 함수의 첫 번째 코드의 주소로 넘어간다.
ni 를 쳐보자.
rsp 가 8 감소하고, rbp 가 rsp 위치에 push 된다 .. ( ret 의 인자값은 call 할 때 미리 넣어주는 것 같음 )
이것은 함수가 종료 될 때 에필로그 부분에서 leave 에서 수행하는 pop rbp 의 인자값으로 사용돼 main 함수에 들어오기 전 rbp 가 들어있다.
ni
rbp 를 rsp 로 덮는다. rbp 가 rsp 로 올 것이고, 즉 같은 위치에 있다.
이 부분은 rbp, base 위치를 정하는 부분이다.
2. setup 서브 함수
setup 함수에서 setvbuf()
함수는 버퍼링 방식을 변경하는 함수인데,
표준입력, 표준출력, 표준에러에 사용될 버퍼를 직접 지정하지 않고( 0 ), 버퍼를 사용하지 않고 ( 2 ), 버퍼 크기를 설정하지 않는다 ( 0 ).
즉, 표준입력, 표준출력, 표준에러를 할 때마다 해당 버퍼가 없어 바로바로 상호작용하게 해 준다. ( CTF 에서 많이 나옴 )
+ ) 첫 번째 인자 : rdi, 두 번째 인자 : rsi, 세 번째 인자 : rdx, 네 번째 인자 : rcx, 다섯 번째 인자 : r8, 여섯 번째 인자 : r9, 일곱 번째 인자 : stack 내부 ...
3. plt & got
printf()
함수의 plt, got 로 설명하겠다.
printf@plt 코드이다.
0x400540 에서 got 로 jmp 한다.
got 에는 printf@plt+6 주소가 들어있다.
예상대로 printf@plt+6 으로 간다.
그리고 _dl_runtime_resolve_xsavec 함수로 jmp 한다.
그 후 _dl_fixup 함수를 호출한다.
_dl_lookup_symbol_x 함수 호출
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 함수가 끝난다.
'Layer 7' 카테고리의 다른 글
운영체제 메모리 할당 알고리즘 (0) | 2020.09.02 |
---|---|
[Pwnable] Codegate 2018 - BaskinRobins31 (0) | 2020.09.01 |
바이너리 분석 2 - RTL ( NX-bit bypass ) (0) | 2020.08.25 |
BOF 예제 분석 (0) | 2020.08.16 |
DreamHack - string ( pwnable ) write up (0) | 2020.08.07 |