gdb 할 때 기본적인 disp 항목
disp $esp
disp $ebp
disp $eip
disp /i $eip
disp /4xw $esp
--
저번차시에 하다가 급하게 끝냈던 내용을 다시 되짚으며 처음부터 시작
소스코드 : int a=1, int b=2;
0x080484b0 <+3>: sub $0x8,%esp
→ 변수 2개를 선언하기 위해 공간을 만들었다 (int니까 4byte 2개)
확인해보니 %ebp 0xffffd458 / %esp 0xffffd450 로 8만큼 %esp가 낮아짐
( Centos7에서는 printf까지 다섯개 분량의 공간을 미리 만들고 mov로 복사 sub $0x14,%esp
실습환경인 Rocky8에서는 두 개만 만들고 나중에 push로 넣는다 )
0x080484b3 <+6>: movl $0x1,-0x4(%ebp)
0x080484ba <+13>: movl $0x2,-0x8(%ebp)
→ $ebp 기준으로 -4 위치에 1 대입, -8 위치에 2 대입
소스코드 : printf("%d, %d \n", a, b);
0x080484c1 <+20>: pushl -0x8(%ebp)
0x080484c4 <+23>: pushl -0x4(%ebp)
0x080484c7 <+26>: push $0x804856c
→ printf 함수의 세번째(b), 두번째(a), 첫번째(문자열) 인수를 스택에 저장한다.
( 마지막부터 거꾸로 실행한다. 후입선출. a, b 순으로 선언되었음. 따라서 b, a 순으로 저장하고 마지막에 문자열 저장 )
0x080484c1 <+20>: pushl -0x8(%ebp) 실행후
(gdb) x/4xw $esp
0xffffd43c: 0x00000002 0x00000002 0x00000001 0x00000000
0x080484c4 <+23>: pushl -0x4(%ebp) 실행후
(gdb) x/4xw $esp
0xffffd438: 0x00000001 0x00000002 0x00000002 0x00000001
=> 순서대로 b인 2의 값, a인 1의 값이 추가된 것
0x080484c7 <+26>: push $0x804856c
(gdb) x/4xw $esp
0xffffd434: 0x0804856c 0x00000001 0x00000002 0x00000002
=> 알아볼 순 없지만 문자열이 스택에 추가됨
( mov는 메모리에서 메모리로 이동할 수 없으므로, push를 사용한다 )
(gdb) x/s 0x0804856c
0x804856c: "a: %d, b: %d\n"
=> x/s로 확인하면 문자열 확인 가능
(gdb) x/4xw 0x0804856c
0x804856c: 0x25203a61 0x62202c64 0x6425203a 0x0000000a
=> x/4xw로 확인한 결과
이를 아스키코드표와 함께 대응해 볼 때 0x 25 20 3a 61 0x 62 20 2c 64 0x 64 25 20 3a 0x0000000a % 공백 : a b 공백 , d d % 공백 : \n <- 개행이라고 하여 한 문자로 취급 LF ( NULL문자는 00 이다 ) |
0x080484cc <+31>: call 0x8048350 <printf@plt>
→ printf 함수 호출. 이 때 printf함수 실행하고 종료되면 돌아올 RET(Return address)를 스택에 저장함
call은 복귀주소를 스택에 저장하고, 함수를 호출하고 점프한다. ( 복귀주소 : 0x080484d1 )
(gdb) si 0x08048350 in printf@plt () 1: $esp = (void *) 0xffffd430 2: $ebp = (void *) 0xffffd448 3: $eip = (void (*)()) 0x8048350 <printf@plt> 4: x/i $eip => 0x8048350 <printf@plt>: jmp *0x804a00c ← printf는 동적 메모리이기 때문에 아직 이 주소에 없음. 채워주는 과정을 실행할 예정이다. 평소에는 n 또는 ni를 사용했지만 step into 즉 si로 printf 함수내로 들어가야 복귀주소를 확인할 수 있다 (gdb) x/8xw $esp 0xffffd430: 0x080484d1 0x0804856c 0x00000001 0x00000002 0xffffd440: 0x00000002 0x00000001 0x00000000 0xf7e34f36 (gdb) p printf $1 = {<text variable, no debug info>} 0xf7e6e7e0 <printf> => printf의 위치 step into로 함수 안에 들어오면 disas main으로 위치를 파악할 수 없고 n이나 ni를 쓸 수 없다. c로 쭉 이어서 실행하다가 main으로 돌아올 수 있지만, 함수의 끝까지 실행하므로 계속 분석하려면 bp 브레이크 포인트를 잡아주어야 한다 (gdb) b *main+36 (gdb) c Breakpoint 3, 0x080484d1 in main () at test-att-sub.c:11 ... => 브레이크 포인트에 멈췄다 (gdb) x/8xw $esp 0xffffd434: 0x0804856c 0x00000001 0x00000002 0x00000002 0xffffd444: 0x00000001 0x00000000 0xf7e34f36 0x00000001 => 복귀주소 사라짐 |
0x080484d1 <+36>: add $0xc,%esp
→ printf 함수 호출이 끝나서 스택 복원 a=10, b=11, c=12. 12만큼 %esp의 위치를 옮긴다.
(gdb) x/8xw $esp 0xffffd440: 0x00000002 0x00000001 0x00000000 0xf7e34f36 0xffffd450: 0x00000001 0xffffd4d4 0xffffd4dc 0xffffd474 => 0xffffd434 에서 0xffffd440로. 0x0804856c 0x00000001 0x00000002 12만큼의 데이터가 보이지 않게 되고, a=1, b=2 했을때로 되돌아왔다. |
0x080484d4 <+39>: mov $0x0,%eax
→ 종료. %eax에는 종료 값으로 0이 저장된다 eax 레지스터는 리턴값이 저장됨
disas /m main
=> C언어 문법을 어셈블리어와 같이 출력해준다. 컴파일시 -g 옵션 필요
-4(%esp)
=> $esp가 가리키는 위치 -4의 메모리 주소(값)
%esp-4
=> $esp가 가리키는 위치에서 -4
* 만약 centos7에서 진행했다면? push 대신 movl로 대체되어 인수값을 복사한다.
movl -0x8(%ebp), %eax
movl %eax, (%esp)
movl -0x4(%ebp), %eax
movl %eax, (%esp)
movl $0x804856c,(%esp)
c언어에서는 무조건 1개의 값만 리턴가능하므로
구조체나 배열의 경우 첫 번째 주소값을 넘김.
함수의 에필로그
leave <-- movl %ebp, %esp + pop %ebp
ret <-- pop %eip + jmp %eip
leave
=> 낮아진 %esp에 %ebp를 넣어 다시 높아지도록 만든다. (esp: 440 -> 448)
pop으로 %esp에 들어있는 값(0)을 %ebp에 넣는다. 그래서 %ebp의 값이 0이 되고 pop때문에 %esp는 더 높아짐.
* pop 명령어 : %esp 레지스터의 값을 피연산자에 넣고 한 단계 높인다
(gdb) x/xw 0xffffd448 0xffffd448: 0x00000000 => movl %ebp, %esp로 인해 %esp의 주소값 0xffffd448에 들어있는 값 0 (gdb) ni 1: x/i $eip => 0x80484d9 <main+44>: leave (gdb) ni (gdb) i r ebp esp ebp 0x0 0x0 esp 0xffffd44c 0xffffd44c ebp : 0xffffd448 -> 0x0 esp : 0xffffd448 -> 0xffffd44c |
ret
=> pop으로 %eip에 복귀주소를 저장하면서 %esp가 한 칸 높아진다
(gdb) ni 0xf7e34f36 in __libc_start_main () from /lib/libc.so.6 1: x/i $eip => 0xf7e34f36 <__libc_start_main+246>: add $0x10,%esp (gdb) ni (gdb) i r eip eip 0xf7e34f36 0xf7e34f36 <__libc_start_main+246> 이 때 disas main 하면 화살표가 없다. main은 끝났으므로. disas __libc_start_main 하면 화살표가 있음. => ebp에 pop할 때는 0이었지만, 한 칸 높아지면서 0xffffd44c에 복귀주소가 저장되어 있다 0xffffd44c의 복귀주소를 %eip에 저장. %esp는 한 칸 높아진다. |
SFP 세이브 프레임 포인터
=> 함수종료시에 %esp가 가리키는 주소의 값을 %ebp에 집어넣은 후 새 프로그램을 시작한다
실습)
임의의 함수 A를 만들지만, 호출하지 않는(void myShell(); 이렇게 선언만 하고 호출X) c언어 코드를 만든다.
gdb로 함수 A의 주소를 확인한다.
(gdb) p myShell
$1 = {void ()} 0x80484f4 <myShell>
(gdb) x/5wx $ebp
0xffffd458: 0x00000000 0xf7e34f36 0x00000001 0xffffd4e4
ebp ret argc argv
0xffffd468: 0xffffd4ec
envp
( int main(int argc, char *argv[], char *envp[]) c언어 기본 함수 )
ret 주소를 함수 A의 주소로 바꿔치기한다.
(gdb) set {int}($ebp+4)=0x80484f4
(gdb) x/8wx $ebp
0xffffd458: 0x00000000 0x080484f4 0x00000001 0xffffd4e4
0xffffd468: 0xffffd4ec 0xffffd484 0x0804a014 0x00000010
=> 변조된 상태
(gdb) ni
3: $eip = (void (*)()) 0x80484f4 <myShell>
=> ni를 여러번 하여 끝에 도달하자 다음에 실행할 명령어, eip에 함수A의 주소가 담긴다
(gdb) ni
0x080484fc 16 system("/bin/sh");
1: $ebp = (void *) 0xffffd45c
2: $esp = (void *) 0xffffd458
3: $eip = (void (*)()) 0x80484fc <myShell+8>
4: x/i $pc
=> 0x80484fc <myShell+8>: call 0x8048380 <system@plt>
5: x/4xw $esp
0xffffd458: 0x080485ab 0x00000000 0x00000001 0xffffd4e4
(gdb) ni
[Detaching after vfork from child process 1902]
sh-4.4#
=> 함수A 실행이 종료되자 함수A에서 실행한 /bin/sh 쉘 탈취 명령어가 실행되었다.
실습) 함수의 분석
(gdb) p main
$1 = {int ()} 0x80484ad <main>
=> main의 주소값도 확인할 수 있다
Dump of assembler code for function main:
0x080484ad <+0>: push %ebp
0x080484ae <+1>: mov %esp,%ebp
(gdb) i r $esp $ebp
esp 0xffffd458 0xffffd458
ebp 0x0 0x0
=> 처음에 시작할 때 찍어보면 ebp값이 0으로 되어 있다
(gdb) i r $esp $ebp
esp 0xffffd458 0xffffd458
ebp 0xffffd458 0xffffd458
=> mov 명령어가 실행된 후 $esp와 $ebp의 값이 같아진다. 이제 소스코드 시작.
0x80484b0 <main+3>: sub $0x4,%esp
(gdb) i r $esp $ebp
esp 0xffffd454 0xffffd454
ebp 0xffffd458 0xffffd458
=> 변수를 위해 sub로 공간 확보, $esp의 위치가 4byte 낮아진다
0x080484b3 <+6>: movl $0x1,-0x4(%ebp)
=> ebp에서 -4위치에 1 넣기
0x080484ba <+13>: pushl -0x4(%ebp)
0x080484bd <+16>: push $0x804858c
=> printf 함수의 인수를 저장. i와 문자열.
0x080484c2 <+21>: call 0x8048350 <printf@plt>
=> printf 함수 call
0x080484f0 <+26>: add $0x8,%esp
=> 두 칸(4바이트 x 2)을 다시 복귀시켜서 $esp 복귀. 0xffffd44c -> 0xffffd454로 복귀됨
0x080484ca <+29>: call 0x80484d6 <func1>
=> finc1 함수 call
이 때는 si로 step into.
(gdb) disas func1
0x080484d6 <+0>: push %ebp
0x080484d7 <+1>: mov %esp,%ebp
0x080484d9 <+3>: sub $0x4,%esp
0x080484dc <+6>: movl $0x2,-0x4(%ebp)
0x080484e3 <+13>: pushl -0x4(%ebp)
0x080484e6 <+16>: push $0x8048593
0x080484eb <+21>: call 0x8048350 <printf@plt>
0x080484f0 <+26>: add $0x8,%esp
0x080484f3 <+29>: nop
0x080484f4 <+30>: leave
0x080484f5 <+31>: ret
함수 내용이 차례대로 실행된다.
(gdb) i r $ebp $esp
ebp 0xffffd458 0xffffd458
esp 0xffffd44c 0xffffd44c
0x080484d7 <+1>: mov %esp,%ebp 실행 후
(gdb) i r $ebp $esp
ebp 0xffffd44c 0xffffd44c
esp 0xffffd44c 0xffffd44c
=> 함수 내에서도 처음에 $ebp와 $esp의 위치를 맞추어주는 작업이 있다
(gdb) x/4xw $ebp
0xffffd44c: 0xffffd458 0x080484cf 0x00000001 0x00000000
=> 이 때 $ebp를 조사해보면
0xffffd458은 함수 호출전의 ebp값이고 0x080484cf는 함수를 끝내고 돌아갈 복귀주소이다
0x080484f3 <+29>: nop
=> nop는 공백을 처리를 위한 함수이다. cpu가 쉬어감
nop를 왜 사용하는가? ( https://shinluckyarchive.tistory.com/113 )
0x080484f4 <+30>: leave
0x080484f5 <+31>: ret
(gdb) i r $ebp $esp
ebp 0xffffd458 0xffffd458
esp 0xffffd450 0xffffd450
=> leave 명령어를 통해 ebp의 위치가 변경되었다
(gdb) ni
(gdb) i r $ebp $esp
ebp 0xffffd458 0xffffd458
esp 0xffffd454 0xffffd454
=> ret 명령어 후, main으로 돌아온 esp 레지스터의 위치
0x80484d4 <main+39>: leave
(gdb) i r $ebp $esp
ebp 0x0 0x0
esp 0xffffd45c 0xffffd45c
=> 다시 main의 종료과정. leave 명령어로 인해
esp와 ebp가 같아지고, ebp 0으로 초기화되고, esp 한 바이트 높아짐
0x80484d5 <main+40>: ret
(gdb) ni
x/i $eip
=> 0xf7e34f36 <__libc_start_main+246>: add $0x10,%esp
(gdb) i r $ebp $esp
ebp 0x0 0x0
esp 0xffffd460 0xffffd460
=> eip에 셋팅해주고 esp 한 바이트 높아짐
종료 ~