해야지 해야지 하고 미뤄뒀던 쉘코드 작성법이다.
사실 나도 내가 짠 쉘코드를 사용한다기 보다는 남들이 해놓은거 바이트별로 모아놨다가 써서..
쉘코드 작성법에 대해서는 별로 숙달도 되어있지 않고 무엇보다도 기계적으로 학습한 부분도 있어서 설명이 부족할 수 있다.
우선 쉘코드라는 것은 말그대로 쉘을 실행시켜 주는 코드다... 이렇게 말하면 좀 막연하니 부연하자면
대부분 프로그램을 짤때는 c, java같은 하이레벨 랭귀지를 사용한다... 길어서 쓰기 귀찮은데 한국말이 안떠오른다... 고급 언어라고 부르겠다.
쉘코드는 주로 공격용으로 만들어지는데 공격할 프로그램 어딘가에 이런 고급언어로... 예를들어 system("/bin/sh"); 이런식의 문자열을 넣어놓고 eip를 이쪽으로 점프시킨다고 해도 당연히 컴퓨터는 알아먹지 못한다.
그래서 쉘을 실행시키는 기능을 컴퓨터가 이해가능한 기계어 수준 즉, 바이너리로 작성하는 것이다.
거기에 argv나 gets 함수등 문자열의 형태로 보통 집어넣기 때문에 \x00이 있으면 문자열의 끝으로 인식하여 들어가지 않는 문제를 해결하기 위해서 \x00을 빼주는 작업을 한 것이 바로 쉘코드다.
굳이 쉘코드를 집어서 글을 쓰는 이유는 쉘을 띄우면 해당 권한의 어떤 명령어든 써먹을 수 있어서 가장 많이 쓰이기 때문이다.
ftz나 lob같은 문제에서는 execve("my-pass");이런것을 코드로 만들어 써도 괜찮을듯 하다.
서론이 길어졌는데.. 그래서 기계어를 보려면 당연히 프로그램을 짜서 그 바이너리를 보는게 빠르다.
이렇게 생겨먹은 shell.c라는 코드를 짜서
gcc -o shell shell.c -static 이렇게 컴파일 했다.
-static이 들어간 이유는 공유라이브러리가 아닌 정적 라이브러리를 사용하기 위함이다.
쉽게말하면 execve가 실행되기 전에 execve의 코드를 보기 위해서 필요하다.
그리고 gdb에서 shell 프로그램을 열어서 execve를 disass 해주면 -static 옵션으로 컴파일 했기 때문에 execve 코드가 이렇게 쭉 나온다.
저기에서 mov 하고 ebp+어쩌구라고 되어있는 부분이 인자를 전달하는 부분인데 ebp+8부분이 /bin/sh라는 문자열의 주소, ebp+0xc 부분과 ebp+0x16부분이 아까 c에서 짤때 널로 넣어준 부분이다.
직접 확인해 봐도 좋지만 함수호출시 인자 전달 순서정도는 외워두두는 것도 편하다.
이렇게 인자를 전달하고 나서 eax에 0xb를 집어넣고 int 0x80을 하는데 우리가 필요한건 딱 이 int 0x80까지의 부분이다.
int 0x80에서 끊고 뒷부분은 필요 없는 이유는 저부분에서 실질적으로 필요한 동작이 끝나기 때문인데..
int 0x80명령어가 실행되면 프로세스는 커널에 인터럽트를 발생시키며 eax, 정확히는 al에 들어있는 0xb라는 저 숫자를 커널에 전달한다.
0xb=11, 이 11이 무슨 숫자냐하면 시스템 콜 넘버다.
http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/Assembly/Documents/article_linux_systemcall_quick_reference
여기서 리눅스 시스템 콜 레퍼런스를 보면 11번에 execve가 있는것을 확인할 수 있다.
그래서 커널이 execve를 실행하게 되면 프로그램은 완전히 /bin/sh라는 프로그램으로 덮어지므로 뒷쪽 코드는 필요없다.
저부분은 아마도 에러나 실수 등으로 execve가 실패했을 때의 처리 부분일 것이다. (아마도다. 아마도...)
그럼 이제 shell_asm.s라는 파일을 만들어서 해당부분만 어셈블리어로 작성해보자.
위 분석에서 확인한 바로는
eax로 시스템 콜 넘버 0xb
ebx로 "/bin/sh"의 주소
ecx로 0x00(null)을 가르키는 주소 (const char* 형태라서 주소를 넣어줘야 한다.)
edx로 0x00(null)을 넣어주면 된다.
이런식으로 넣어줬다. 어셈 프로그래밍은 거의 안해봤지만 그럼에도 어려운 수준은 아니다.
strings에서 exeshell을 call 하면 call 명령어에 의해 스텍에 리턴주소가 들어가게 된다.
그 리턴주소는 strings에서 바로 다음 부분이므로 "/bin/sh"의 주소가 된다. 이 리턴주소를 popl ebx 해서 ebx에 집어넣어줬다.
그리고 pop 0x0을 해서 스텍에 널을 집어넣고 그 널을 가르키고 있는 esp를 ecx에 넣어줘서 널을 가르키는 주소를 완성했다.
eax와 edx는 그냥 movl 명령어로 넣어준 것이니 굳이 설명은 필요 없을 것 같다.
굳이 strings를 아래쪽에 둔 이유는 아마도 문자열인 "/bin/sh"를 맨 밑으로 내려놓기 위함이다.
그래야 0x00이 중간에 들어가지 않고 쭉 연결되게 만들수 있기 때문일듯. 나도 약간은 수동적으로 외운 부분이라 확실하지는 않고 추측일 뿐이다.
컴파일 하기 전에도 지금 edx에 집어넣을 0x00이 코드 중간에 박혀있는 문제를 예상할 수 있지만 그게 다가 아니니 일단은 컴파일해서 바이너리를 살펴보자.
objdump의 -d 옵션을 사용하면 바이너리와 코드를 함께 볼 수 있다.
......윗부분은 무시하고...
보아하니 00이 꽤 많이 들어가있다.
edx에 0x0을 넣어줄때 00이 들어갈 것은 예상했지만 movl을 사용해서인지 eax에 0xb를 넣을때도 00이 세바이트나 들어갔다.
이것들을 없애주는 작업을 할건데 방법은 간단하다.
edx는 xor edx edx 이런식으로 0으로 만들어준다.
eax는 똑같이 xor eax eax 해서 4바이트를 00000000으로 만들어준뒤,
eax의 끝바이트를 뜻하는 al 레지스터에다가 movb로 0xb를 넣어준다.
이 방법대로 코드를 다시 짰다.
push 0x0 에서 0x0대신 xor로 0으로 만들어준 eax를 쓰느라 순서가 약간 뒤집혔지만 자세히 들여다보면 위와 같은 코드다.
이제 이것을 objdump로 분석해보면
이렇게 널이 없는 기계 코드를 얻어냈다.
밑에 0x00부터는 상관없는 부분이므로 떼주면
"\xeb\x0c\x31\xc0\x5b\x50\xb0\x0b\x89\xe1\x31\xd2\xcd\x80\xe8\xef\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
이제 테스트를 해봐야 하는데 테스트 프로그램 만들기가 귀찮아서 그냥 ftz level11을 켜서 bof 테스트를 해봤다 ----> 실패했다...
코어덤프들을 보며 분석해본 결과 문제는 ecx로 넘어간 주소가 널을 가르키고 있기 때문이라고 생각했다.
원래 argv[0]에는 프로그램명이 들어가야 하므로 "/bin/sh"의 주소가 들어가는게 맞긴 하다.
그런데 분명히 c언어에서 NULL을 넣고 돌렸을땐 돌아갔는데... 참 이러면 억울하다. 환경에 따라 다르거나 컴파일러가 어떤 예외처리를 해줬거나 둘중 하나인듯.
결론은 이렇게 해결해 줬다. 추가된 부분은 push ebx 부분인데 어떻게 해결이 된거냐하면
||argv[0]의 시작주소 ||argv[1]의 시작주소||
||ebx(/bin/sh의 주소)||\x00000000 ||
^
esp
이렇게 ebx를 스텍에 넣어준뒤 esp를 통해 해당부분의 주소를 전달하여 성공한 것.... 뒤에 널까지 굳이 또 넣어준 이유는 이렇게 안해주면 배열로 인식을 안하는 모양이다... 참 까다롭기 그지없다.
그래서 위 objdump에서 뽑아낸 쉘코드는
"\xeb\x0d\x31\xc0\x5b\x50\x53\xb0\x0b\x89\xe1\x31\xd2\xcd\x80\xe8\xee\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
27바이트짜리 쉘코드다.
중간 풀이과정은 생략하도록 하고... 위 쉘코드를 사용해서 bof 공격에 성공했으므로 코드가 작동한다고 봐도 무방한 것 같다!!
해야지 해야지 하다가 결국 쉘코드 작성도 해보고 포스팅까지 마쳤다.... 이제 다양한 형태의 쉘코드를 요구하는 문제에도 대응할 수 있을듯
끝-
ps.다른 쉘코드를 gdb에 넣고 분석해 보니 /bin/sh를 스텍에 밀어넣고 사용하는듯 하다. 그러면 처음에 main에서 아래로 점프할 필요가 사라져서인지 2바이트 적은 25바이트로 쉘코드를 만들수 있었다. 더 효율적인 방법이 있으므로 실전에서는 저방식대로 만드는게 좋을듯...
ps2.http://satanel001.tistory.com/76 에 25바이트 쉘코드에 대한 정리를 추가했다.