★fgets(buffer, 256, stdin)
★if(*(buffer+47) == ‘\xbf’) : buffer+47은 ret의 다음 부분이니까 return address 뒤에는 ‘\xbf’로 시작하면 안 된다.
★if(*(buffer+47) == ‘\x08’) : return address 뒤에‘\x08’로 시작하는 주소를 넣을 수 없다.
=> return address에 뒤의 4byte 주소에는 stack 영역의 addr(‘\xbf’로 시작)와 ‘\x08’로 시작하는 addr는 넣을 수 없지만, ‘\x40’으로 시작하는 addr는 아직 넣을 수 있다. Libc 영역의 주소는 넣을 수 있는 것이다.
★ memcpy(&ret_addr, buffer+44,4) : buffer+44부터 4byte를 ret_addr에 복사한다. Buffer+44부터 4byte는 return addr이므로 return addr가 ret_addr에 복사된다.
★ while(memcmp(ret_addr, “\x90x90”, 2) != 0) : 일단 return addr는 NOP으로 채워지면 안 된다. 계속 ret_addr++하면서 조건을 확인하므로 ret 전체가 NOP으로 채워질 수 없다. (아니면 NOP으로 채워서 while문 안에 안 들어가는 방법도 있을 것 같다.)
★ if(*ret_addr == ‘\xc9’) if (*(ret_addr+1) == ‘\xc3’) : ret_addr에 차례로 “\xc9c3”이 오면 안 된다. ret_Addr++을 하므로 뒤의 2byte에도 올 수 없다. “\xc9”는 leave, “\xc3”은 ret을 나타내는 것이다. 따라서 leave와 ret을 하는 libc 함수 영역을 사용할 수 없다.
★ memset(buffer, 0, 44) : buffer 시작주소부터 44byte를 0으로 초기화한다. 따라서 buffer와 sfp 총 44byte가 0으로 초기화된다.
★memset(buffer+408, 0, 0xbfffffff – (int)(buffer+48)) : buffer+48부터, 즉 return addr 뒤부터 0xbfffffff까지를 0으로 초기화한다. 스택은 커질수록 주소가 감소하므로
★ memset(buffer-3000, 0, 3000-40) : 어디를 0으로 초기화하는지는 모르지만 buffer-3000부터 40byte를 0으로 초기화한다. 상관있을지는 해봐야 알 것 같다.
Buffer 40byte(0-39) + sfp 4byte(40-43) + ret 4 byte(44-47)
일단 심볼릭 링크 파일을 만들었다.
일단 fgets는 사용법이 다르다. Stdin을 읽으니까 일단 기존에 입력했던 방식으로 주면 커서가 뜨는데 거기에 그대로 적으로 그때 buffer에 저장된다. 256만큼을 읽으니까 NOP을 많이 줘서 core dumped를 시켜 core 파일을 생성했다.
이번에는 argv[1]이 없고 buffer에만 복사되므로 buffer만 보면 될 것 같다.
Buffer + sfp가 ‘\x41’로 채워졌고, 그뒤에 ret이 “\x42”*4, ret 뒤부터 “\x43”*4 + “\x90”*200이 채워져있다.
Fgets는 stdin에서 읽은 문자열을 포인터 s가 가리키는 buffer에 저장한다. 따라서 코드 상에서는 stdin에서 읽은 문자열을 buffer가 가리키는 곳에 저장한다.
fgets에서 stdin으로 문자열을 읽으므로 buffer 공간을 확보한 후 0x8049a3c로 이동하는 것은 stdin으로 문자열을 읽기 위해서이다. <stdin@@GLIBC_2.0>에서 처음은 0x401068c0 주소가 채워져있다. stdin에서 읽은 것을 buffer에 저장하니까 buffer의 시작주소가 아닐까? 일단 GLIBC는 GNU C Library이고, 2.0은 Version을 뜻한다. 이것은 libio/stdio.c에 있고 stdio.h 헤더 파일을 import하면 쓸 수 있다.
이동해보면 <_IO_2_1_stdin>이다. 여기서 두 번째는 start of ptr, 즉 stdin 입력 메모리에 시작이고, 세 번째는 end of ptr, 즉 입력이 끝난 시점에서 메모리 위치이다. Start of ptr에 키보드로 입력한 문장이 들어가 있다고 한다.
일단 stdin의 주소를 알려면 실행 후에 알 수 있다.
<_IO_2_1_stdin_>+12가 stdin 시작주소이다.
fgets 함수 실행 후 printf 함수에서 breakpoint를 잡고, continue를 한다. 그러면 입력을 받는데 stdin일 것이다. 0x401068c0을 확인해보면 주소가 들어가있는 것을 알 수 있다.
<_IO_2_1_stdin_>+4 : 어디까지 읽었는지를 의미한다. 0x40015005
<_IO_2_1_stdin_>+8 : 얼마나 입력받았는지 0x40015005
<_IO_2_1_stdin_>+12 : stdin 시작 주소가 어딘지 0x40015000
실제로 0x40015000을 string으로 출력해보면 입력한 AAAA가 들어가 있는 것을 확인할 수 있다.
그렇다면 0x40015000부터 stdin에 입력한 문자열이 들어간다. 이 공간을 사용하면 되고, 여기에 쉘코드를 주면 된다.
쉘코드를 stdin에 주는데, ret에 stdin의 시작주소를 준다. Fgets는 기존의 payload와 달리 인자로 주는 것이 아니라 일단 파일이 열리면 표준입력을 받기 때문에 지난번 level3에서처럼 파이썬 명령을 실행하고 cat 명령어를 같이 줘서 xavius를 실행해야 한다.
파이썬 명령을 통해 payload가 전달되는데 쉘은 stdin으로부터 EOF를 전달받는다. 따라서 python 명령으로 stdin를 전달하는 과정에서 쉘에 EOF를 전달하는 문제가 생기므로 이 문제를 해결하기 위해 cat 명령어를 사용하는 것이다. (level 3에서도 다뤘던 내용) cat으로 문제를 해결하는 이유는 cat은 파일을 지정하지 않으면 표준입력 받은 그대로 표준출력하기 때문이다. 따라서 python 명령과 함께 cat 명령어를 줘야 하고, 같이 줄 수 있는 방법을 ;를 사용하는 것이다. 위에서 확인할 수 있듯이 |를 사용하면 core dumped 되었지만 seg fault가 출력되지 않았다. 따라서 seg fault를 출력하지 않기 위해 |를 사용한다.
Stdin의 임시 buffer 시작주소인 0x40015000으로 가보면 쉘코드와 NOP이 들어가 있는 것을 확인할 수 있다.
Stdin의 임시 buffer 주소를 0x40015000으로 안 주고 0x40015001로 준 이유는 fgets 함수는 문자열 끝이 \n으로 \x00으로 끝나는데 \x00은 NULL을 의미하므로 주소로 NULL을 포함하지 않기 위해 주소 끝부분을 변조한 것이다. 실제로 원래 주소로 줬을 때는 계속 표준입력 받지만, 변조한 주소를 ret에 줬을 때는 바로 입력받고 엔터를 치면 끝난다.
왜 안 될지…
NOP은 쉘코드 뒤에 줬을 때만 되고 앞에 주거나 NOP으로 쉘코드를 감싸면 안 된다.
음…됐다. NOP을 테스트해봤을 때는 뒤에 줘야지 되는 것 같았는데 아니었다.
다시 해보니까 앞에줘야지 되는 거였다…계속 입력받아서 잘못된줄 알았는데 아니었다…while문 때문에 앞에 줘야지 NOP으로 while문을 끝내고 쉘을 받을 수 있어서 그런 것 같기도 하고…
어쨌든 성공이다…
“throw me away”
[LOB] death_knight (0) | 2019.06.30 |
---|---|
[LOB] level 20: xavius -> death_knight (0) | 2019.06.30 |
[LOB] level 20 소켓(socket) 생성 간단 정리 (0) | 2019.06.30 |
[LOB] level 18: succubus -> nightmare (0) | 2019.06.28 |
[LOB] level 17: zomebie_assassin -> succubus (0) | 2019.06.28 |