스택 조작을 통한 지역변수 파괴 실습
----
실습에 앞서, 해당 게시글의 실습 환경은 LOB 서버를 이용했다는 걸 알아야한다.
만약 다른 실습 환경에서 진행했을 경우, ASLR 및 Stack Canary와 같은 메모리 보호기법이 걸려있을 가능성이 높으므로
원하는 결과를 얻을 수 없다는 것을 유념하자. 해당 LOB 서버는 메모리 보호기법이 다 해제된 상태이다.
----
스택 프레임에 대해 대략적으로나마 이해를 했다면
이번엔 이를 응용하여 지역변수 값을 변조하는 실습을 진행해보자
다음 코드는 기초적인 버퍼오버플로우에 대한 예시이다.
i의 값이 100으로 고정되어있으므로,
i가 123이 되지않는한 이 프로그램은 계속 "Failed"라는 문자열을 뱉고 종료할 것이다.
우리의 목적은 메인 함수 내부에서 100으로 초기화되어 있는 변수 'i'를 123으로 변조하여
func1을 실행시키는 것이다.
프로그램을 임의대로 조작하여 "Overflow Success"라는 문자열을 출력해보도록 하자.
func1 함수는 printf 단 하나를 실행시키는 것이니 딱히 볼 내용은 없다.
우리가 주목해야 할 것은 메인 함수 내부의 strcpy함수이다.
왜냐하면 사용자로부터 "입력"을 받는 함수이기 때문이다.
입력을 받는 함수 중에서, 길이를 검사하지 않는 함수들은 모두 취약한 함수라는 걸 알아두자.
이런 케이스는 scanf, gets, 그리고 위와 같은 strcpy 등이 있다.
요즘은 보안이 잘 되어있어서, gets 와 같은 함수를 사용할 경우 컴파일러에서 경고문을 출력해주거나
심지어 Visual Studio는 위의 함수들이 발견되면 즉시 컴파일 오류를 내뿜는다.
(물론 'SDL'이라는 옵션의 체크를 해제하면 이런 보안성 검사를 하지 않는다.)
메인 함수의 strcpy를 보면, 첫 번째 인자인 argv[1]을 buf 배열에 넣는다고 되어있다.
그리고 이전 스택 프레임의 포스팅에선 지역변수는 선언되는 순서대로 스택에 저장된다고 했다.
i가 먼저 선언되었으므로, i가 스택에 먼저 들어갈 것이다. gdb를 통해 천천히 살펴보자.
변수 2개(i, buf)가 선언되고, 또 strcpy 함수가 동작한 이후의 메인 함수를 살펴봐야하므로,
중단점(break point)를 16번째 라인으로 설정해준다. (b 16)
그리고 프로그램을 시작하되, 테스트할겸 인자값을 aaaaabbbbbccccc 이 15개로 줘보자.
이후 실행 도중 우리가 설정한 중단점인 16번째 라인에서 멈춘 걸 확인할 수 있다.
이 상태에서 지역변수의 값을 살펴본다. (where f)
i는 100의 값을 갖고있고, buf 배열은 우리가 입력한 aaaaabbbbbccccc 값이 잘 들어갔다. (뒤의 의미없는 문자는 쓰레기 값이다.)
그리고 이 상태에서 메인 함수를 디스어셈블 해보자. (disas main)
함수 프롤로그가 실행되고, 스택 프레임의 크기가 24바이트로 할당되었다.
여기서 유심히 봐야할 건 과연 i와 buf가 스택 어디 부분에 지정되었나 보는 것이다. 보기쉽게 노란점으로 표시해뒀다.
0x64는 16진수로 64이므로, 이는 10진수로 변환하면 100이 된다. (16*6 + 4)
따라서 ebp-4 부분에 변수 i가 선언되어 있고, buf는 ebp-24 부분에 있는 걸 확인할 수 있다.
i는 그렇다쳐도, buf가 ebp-24에 있다는 것은 뒤에 따라오는 strcpy를 call 했다는 부분에서 알 수 있다.
call 0x8048340 <strcpy>
이 부분말이다. 이는 리버싱쪽에 가까운 분야이지만, 여기선 strcpy가 call되기 전의 윗 부분 코드들은
그냥 strcpy 함수의 인자들이라고 생각하면 편하다. (그리고 ebp-24 부분에 위치해야할 것은 생각해봐도 buf말곤 없다.)
여기서 정리하자면, i는 ebp-4에 있고, buf는 ebp-24에 있으므로, 총 20바이트 떨어지게 된 셈이다.
그리고 buf의 크기가 20바이트 배열이므로, 이는 Dummy 값 없이 깔끔하게 스택 프레임이 할당된 것이라 할 수 있다.
다시 인자를 aaaaabbbbbccccc로 준 다음, 메모리 주소를 확인해본다.
a는 아스키 코드로 97이다. b는 98이고, c는 99이다.
그리고 이를 16진수로 변환하면, 각각 61, 62, 63이 나온다.
아랫 줄 가운데의 64는 10진수로 변환했을 때 100이 나오므로, 변수 i가 위치한 영역이란 걸 알 수 있다.
다시 명령어를 줘보자.
r aaaaabbbbbcccccdddddee
총 22개를 줘보았다.
e가 아스키 코드로 101이고, 16진수로 65이므로 i를 덮어쓴 이 값은 e라는 걸 알 수 있다.
따라서 인자를 aaaaabbbbbcccccddddd로 주고, 그 다음 21번째 인자를 10진수 123을 16진수로 바꾼 값을 주면 된다.
123은 16진수로 7b이므로
이를 아스키문자열에서 찾아보도록 한다.
7B는 우측 하단에 위치한 '{' 라는 걸 알 수 있다. 즉 레프트 브레이스(left brace)인 것이다.
따라서 최종 공격은 다음과 같다.
r aaaaabbbbbcccccddddd{
'{'가 7B이고, 이것이 10진수로 변환되면 123이므로
메인 함수 내부의 if문 조건에 맞아 떨어져서 func1 함수가 실행될 것이다.
공격이 성공했다.