Basic of Computer Skills

스택 프레임 훑어보기 (gdb 사용) (完) 본문

보안/Pwnable

스택 프레임 훑어보기 (gdb 사용) (完)

cdecl 2018. 8. 3. 13:56



이전 포스팅에서 위와 같이 메인 함수의 esp와 ebp를 열람했고, 해당 사이즈에 따라 스택에 어떤 값들이 들어갔는지 알 수 있었다.


이제 다음 함수인 func1 함수의 스택 프레임을 열람할 것이다.


우리가 메인 함수의 스택 프레임을 살펴보기 위해 중단점을 걸은 것과 같이, func1 함수에도 중단점을 걸어주고


그 구조를 파악해보자.






메인 함수와 구조가 동일하다. (변수에 저장되는 값만 다른 걸 알 수 있다. 4, 5, 6)


그도 그럴 것이, 예제 코드에 나오는 함수들은 모두 동일한 매커니즘을 가졌기 때문이다.






이 상태에서 func1 함수의 esp 및 ebp의 값을 확인하자. 



esp의 값은 0xbffffb48이고, ebp는 0xbffffb54 라는 걸 알 수 있다.


ebp에서 esp를 빼면 12가 나온다. 이 역시 메인 함수와 스택 프레임 크기가 동일하다.




이전 포스팅과 같이, 중단점이 걸린 상태에서 r을 누르고 변수가 k까지 초기화 될 때까지 n을 눌러 16번줄 라인까지 가보자.






그리고 x/5wx 명령어를 입력하여, 5워드 (20바이트) 길이의 메모리 구조를 살피도록 한다.




 

여기까지 왔으면 별도의 설명 없이도 func1의 스택 프레임 크기가 12이며 


ebp가 0xbffffb68 부분이고 esp는 0x00000006 라는 사실을 파악할 수 있어야 한다.



그런데 이 func1 함수는 메인 함수가 호출해서 나온 것이다. 이전 포스팅에 그림에 따르면,


호출되는 함수는, 호출하는 함수의 윗 부분에 스택 프레임이 생긴다고 했다. 이게 정말 맞는 말인지 확인해보자.


func1 함수의 맨 꼭대기인 esp 부분에서부터, 아래까지 좀 더 넓게 보자.



(gdb) x/10wx $esp





func1의 ebp는 1번째 줄 맨 오른쪽인 0xbffffb68 부분인 걸 알 수 있다. 그리고 그 다음 줄의 0x00000009는 


저번 포스팅에서, 메인 함수의 esp라는 것도 확인했다. (메인 함수의 ebp는 아랫 부분의 0xbffffb88이다.)


해당 사진을 통해서 저 그림이 맞다는 걸 알 수 있다. 호출되는 함수는 호출하는 함수의 스택 프레임 윗 부분에 생성된다.



그리고 여기서 눈썰미가 있는 분들은, 사진에 보이는 어떤 메모리 주소에 대해 궁금해 할 것이라고 생각한다.


func1 함수의 ebp인 0xbffffb68과 메인 함수의 esp인 0x00000009 사이에 있는 0x08048451 부분이다.


이 부분은 어떤 함수의 ebp와 어떤 함수의 esp 사이에 있는데, 이 주소의 정체는 


이전의 포스팅을 마치면서 언급한 RET 영역이란 것이다.



먼저 말하자면, 이 RET 부분은 윗 부분의 func1 함수가 갖는 영역이다.  RET은 Return의 약자이다.


말 그대로 돌아간다는 뜻인데, 어디로 돌아가는 건지 약간 생각을 해보자.




모든 함수는 자기 할 일을 마치면 스택 프레임을 반납함에 따라 메모리 상에서 해제된다.


그렇다면 어떤 함수에 의해 호출된 함수 역시, 할 일이 끝나게 되면 스택 프레임이 반납되고 해제될 것이다.


이에 대한 예시가 저 위에 있는 메인 함수와 func1 함수의 관계다.


func1 함수가 끝나고 스택 프레임이 반납되면, 프로그램의 흐름은 다시 메인 함수로 넘어간다.



가령 메인 함수의 내용을 다음과 같이 수정한다고 가정해보자.


============================

printf("main has called\n");

func1();

printf("Test\n");

============================


그리고 해당 결과는 이와 같다.


============================

main has called

func1 has called

Test

============================



중간에 func1이 호출되고나서 printf 문을 뱉고, 종료된 뒤  다시 메인 함수로 돌아오는 방법이다.


이 말은 func1의 스택 프레임이 해제되고 나면, 끝이 아니라


다시 메인 함수의 스택 프레임으로 흐름이 바뀐다는 것이다.




이를 조금 더 자세하게 말하면 스택 프레임이 해제됨과 동시에 돌아갈 주소로 이동한다는 뜻이 되겠다.


이 "돌아갈 주소" 라는 게 바로 RET을 의미한다.





사진을 다시 보자.


RET 영역은 ebp 영역의 바로 다음에 위치한다. 그래서 func1의 RET 영역은 0x08048451이 되는 것이다.


이 0x8048451를 통해 메인 함수로 복귀해야하니, 이 주소의 위치는 메인 함수 어딘가에 있을 것이다.





메인 함수의 어셈블리 코드이다. 이번엔 아랫 부분을 바라보자.



call 0x80483f4 <func1>



func1 함수를 호출한다는 부분이다. 이 코드가 수행되면 프로그램의 제어권은 func1으로 넘어갈 것이다. 


그리고 그 다음줄을 보자. 주소 값이 0x8048451이다. (맨 왼쪽 부분)



어떤 함수를 call 하면 그 호출된 함수는 자신이 일을 끝 마치고 돌아갈 곳을 지정하는데, 


바로 자신을 부르는 코드 (call  0x80483f4다음 줄에 RET을 설정하게 되는 것이다.


그래야만 다시 원래대로 돌아와서 프로그램이 계속 진행되니까..



즉, 다시 정리하자면 func1 함수는 메인 함수가 호출한다.


func1 함수는 자신의 일이 끝나면 종료되고 프로그램의 제어권은 다시 메인 함수로 돌아오게 된다.


이 과정을 스택 프레임으로 살피면, func1의 스택 프레임이 종료되면서 RET 영역에 지정된 주소값을 통해


해당 부분으로 복귀하는 것이다. (main → func1 → main)



func2 역시 마찬가지이다. 이 함수는 또 func1이 호출한다.


이에 대한 스택 프레임은, 이젠 알 수 있듯이 메인 함수가 맨 아래에 있고, 그 위에 func1이, 또 그 위에 func2이 있는 것이다.


func2에 중단점을 걸고 실행하여, 메모리 주소를 보도록 하자.





주황색 점은 esp,  노란색 점은 ebp, 그리고 초록색 점은 RET을 나타낸 것이다.


윗 부분에서 func1의 RET이 메인 함수에서 func1을 호출하고 난 바로 다음의 코드이니,


func2의 RET은, func1의 자신을 호출한 코드 바로 다음 부분에 있을 것이다.



맞음



이런 식으로 스택 프레임이 정리되고 프로그램의 제어권이 왔다갔다 하면서, 최종적으로 메인 함수의 스택 프레임까지


정리되고 나서야 프로그램이 꺼지는 것이다.



그런데 이 부분에서, 옛날 사람들은 다음과 같은 생각을 했다.



"RET 영역의 주소값을 엉뚱한 걸로 덮어버리면 프로그램이 망가질까요?"



이 생각을 통해 버퍼오버플로우 라는 공격 기법이 만들어졌다.


RET 영역을 맘대로 변조해버리면 해당 함수는 엉뚱한 곳으로 리턴하게 될 것이고, 그렇게 되면 프로그램에 오류가 나거나


좀 더 최악의 상황으로, 해커가 유도하는 동작을 실행할 수도 있다.



가령, 0x12345678 라는 메모리 주소가 있는데 이것이 프로그램에 안 좋은 영향을 끼치는 동작을 수행한다고 가정하자. (예시로..) 


여기서 나쁜 마음을 먹고 함수의 RET 영역을 0x12345678로 덮어버린다면


함수는 RET 영역을 0x12345678로 인식하여, 동작을 마치고 복귀하는 과정에서 그 메모리 주소를 실행할 것이다.


이렇게 스택 공간의 특성을 이용하여 RET 영역을 덮어버리는 걸, 


정확히 "스택버퍼오버플로우(Stack BufferOverflow)" 라고 칭한다. 





이제 ebp에 대한 설명을 간단히 하겠다.


ebp의 경우, 호출된 함수의 ebp는 호출한 함수의 ebp 영역을 가리키게 된다.


함수 프롤로그 과정에서 먼저 push $ebp를 하게 되는데,


이때의 ebp란 바로 호출시킨 이전 함수의 ebp를 의미하기 때문이다. 


위에 올려놨던 사진들을 보면 이해할 수 있을 것이라고 생각한다.


ebp는 다른 말로 sfp 라고도 불린다. (saved frame pointer)




버퍼 오버플로우에 관한 문제를 풀 땐,  스택 공간을 buffer 라고 하고, ebp를 sfp로, RET은 동일하게 ret라고 부른다.


즉, buffer + sfp + ret  이런 식으로 표현한다.


sfp와 ret 영역은 둘 다 동일하게 4바이트의 공간을 차지한다. 




만약 어떤 함수의 지역변수가 char str[8]; 이 하나 뿐이라서 스택 프레임의 크기가 8바이트라면


해당 프로그램의 RET 영역을 변조하려면 어떤 시도를 해야할까.



buffer(8) + sfp(4) + ret(4) 의 사이즈를 가지고 있으므로, 앞의 12칸을 아무 값으로 채워넣고


뒤의 4바이트만을 원하는 값으로 넣어주면 되는 것이다.



이것은 추후에 예시로 다시 포스팅하겠다. 











Comments