ARM Assembly에 대해 공부하던 중 좋은 글이 있어 번역 하였다.

출처: Azeria Labs (https://azeria-labs.com/writing-arm-assembly-part-1/)


ARM Assembly Basics 튜토리얼 시리즈에 오신 여러분을 환영합니다! 이 문서는 ARM Exploit 개발 튜토리얼 전에 알아야 할 내용을 담고 있습니다. ARM 쉘코드를 작성하고 ROP 체인을 만들기 전에 ARM Assembly 기초에 대해서 알아야 합니다.

아래 주제들을 순차적으로 다룰 예정입니다.

ARM Assembly 기본 튜토리얼 시리즈:

  • Part 1. ARM Assembly 기본
  • Part 2. 데이터 타입 레지스터
  • Part 3. ARM 명령어 셋
  • Part 4. 메모리 명령들: 데이터 불러오기 및 저장하기
  • Part 5. 다중 불러오기 및 저장하기
  • Part 6. 조건부 실행 및 분기문
  • Part 7. 스택과 함수

튜토리얼을 이해하기 위해서는 ARM 기반의 테스트 베드(Lab Environment)가 필요합니다. 라즈베리파이와같은 ARM 기반의 기기가 없다면, VM에 QEMU와 라즈베리파이 디스트로를 이용하여 테스트 환경을 구축할 수 있습니다. 또한 GDB가 익숙하지 않다면 다른 튜토리얼을 통해서 미리 공부해 주새요. 이 튜토리얼에서는 ARM 32-bit 환경에 집중할 것이고, 예제들은 모두 ARMv6 기반으로 컴파일 되었습니다.

왜 ARM 인가?

이 튜토리얼은 ARM Assembly의 기본을 가르치기 위해 작성했습니다. 특히 ARM 플랫폼에서 익스플로잇을 작성하고 싶은 사람들에게 유용할 것입니다. 당신은 이미 당신 곁에 많은 ARM 프로세서들이 존재한다는 사실을 알고 있을 것입니다. 내 주변을 돌아보면, 인텔 프로세서보다 훨씬 더 많은 ARM 프로세서를 확인할 수 있습니다. 스마트폰, 라우터, 요즘 잘 팔리는 IoT 기기 등에서 말이죠. 이 말인 즉슨, ARM 프로세서는 전 세계에서 가장 널리 보급된 CPU 코어 중 하나라고 할 수 있다는 것입니다. PC나 IoT 기기들이 부적절한 입력값 검증으로 인한 버퍼 오버플로우 등의 공격에 영향을 받기 쉽다는 사실을 생각해 본다면, 널리 퍼진 ARM 기반의 기기들은 다양한 오용 가능성 혹은 공격의 타겟이 될 가능성이 있으며 그 가능성은 점점 높아질 것입니다.

아직 x86 전문가가 ARM 전문가보다 많지만, ARM 어셈블리 언어는 널리 알려진 어셈블리 언어 중 가장 쉽습니다. 그런데 왜 사람들은 ARM에 관심을 가지지 않을까요? 아마도 인텔 프로세서보다 ARM 프로세서 관련한 참고 문헌 들이 적기 때문일 수 있습니다. x86 프로세서를 생각해 보면, Fuzzy SecurityCorelan Team에서 쓴 매우 훌륭한 튜토리얼들이 떠오릅니다. 이러한 튜토리얼을 기반으로 사람들이 실무적인 지식을 가지게 되고, 나아가 튜토리얼에서 커버하지 못한 내용들도 알아내게 됩니다. 만약 당신이 x86 익스플로잇에 관심이 많다면 위 튜토리얼을 통해 훌륭한 시작을 할 수 있을 것입니다. 이 튜토리얼 에서는 어셈블리 기본과 ARM에서 익스플로잇 작성 방법을 집중적으로 다룰 것입니다.

ARM 프로세서 vs 인텔 프로세서

인텔과 ARM에는 많은 다른점이 있지만, 가장 중요한 다른점은 명령어 셋(instruction set) 입니다. 인텔은 CISC(Complex Instruction Set Computing) 프로세서로서 크고 다양한 기능을 가진 명령어 셋을 가지고 있어서, 복잡한 명령어를 통해 메모리 접근을 하게 됩니다. 그러므로 ARM에 비해 다양한 명령어, 어드레싱 모드를 가지고 있지만 더 적은 레지스터를 가지고 있습니다. CISC 방식은 요즘의 PC, 워크스테이션, 서버 등에서 사용되고 있습니다.

ARM은 RISC(Reduced Instruction Set Computing) 프로세서로서 단순한, 100개 혹은 그보다 적은 명령어 셋을 가지고 있습니다.인텔과 다르게 ARM의 명령어는 단순하게 레지스터를 관리하고 메모리에 기록 혹은 메모리로부터 값을 불러오는 역할 정도만 수행합니다. 이말인 즉슨 메모리에 접근 할 때 Load/Store 명령만 사용할 수 있다는 얘깁니다. 좀 더 구체화 시켜보면, ARM에 올라와 있는 특정 메모리 주소 32-bit 내의 값을 증가 시키기 위해서는 오직 세 가지의 명령어 (불러오기, 증가, 그리고 저장) 만 필요하다는 이야기 입니다. 값을 처음 메모리에서 레지스터로 불러오고, 레지스터 안에서 증가시킨 뒤, 다시 레지스터에서 메모리로 저장시키면 됩니다.

Reduced Instruction Set은 장점과 단점이 있습니다. 장점 중 하나는 명령이 빠르게 실행될 수 있다는 것이며, 전체적으로 더 빠른 속도를 제공할 수 있다는 점입니다(RISC 시스템은 명령 실행 시간 단축을 위해 각 명령 당 clock cycle을 줄입니다). 반면에 단점은 적은 명령셋을 제공하기 때문에 소프트웨어를 만들 때 더욱 효율적으로 설계해야 한다는 점입니다. 그리고 ARM은 두 가지 모드가 있다는 점을 명심해야 합니다. ARM 모드와 Thumb 모드가 있습니다. Thumb 모드는 2바이트 혹은 4바이트가 될 수 있습니다(상세한 내용은 Part 3: ARM Instruction Set 참고).

더 많은 ARM과 x86의 차이점은 아래와 같습니다.

  • ARM은 대부분의 명령들을 조건부 분기 실행에 사용할 수 있습니다.
  • 인텔 x86과 x86-64 시리즈는 리틀 인디언 포맷을 사용합니다.
  • ARM 아키텍쳐는 버전3 이전에는 리틀 인디언 포맷을 사용했습니다. 그 이후로는 ARM 프로세서는 혼용 인디언(BI-Endian)을 지원하며 Endianness를 변경할 수 있는 기능을 탑재하고 있습니다.

이것들이 유일한 인텔과 ARM의 차이는 아니며, ARM 버전 별로의 차이들도 있습니다. 이 튜토리얼은 어떻게 ARM이 동작 하는지를 보여주기 위하여 작성된 것이기 때문에 최대한 일반적(generic)인 내용으로 작성 되었습니다. 기본을 이해하게 된다면, 그 이후로 각각 ARM 버전별 미묘한 차이를 쉽게 배울 수 있을 것입니다. 이 튜토리얼에서는 32-bit ARMv6 (라즈베리파이 1) 기반으로 작성 되었으므로, 설명들도 모두 해당 버전을 기반으로 작성되었습니다.

ARM 버전 별 이름이 헷갈릴 수 있습니다. 아래를 참고하세요:

ARM Family

어셈블리 작성하기

ARM 익스플로잇 개발을 시작하기 전에, 어셈블리 프로그래밍의 기본을 이해해야 합니다. 그 전에, 왜 우리는 ARM 어셈블리가 “일반적인” 프로그래밍 / 스크립팅 언어가 아님에도 불구하고 필요할까요? 우리가 리버스 엔지니어링을 하고 ARM 바이너리의 플로우를 이해하고, 우리만의 ARM 쉘코드를 작성하고, ARM ROP 체인을 만들고, ARM 앱을 디버깅 할 일이 없다면 배울 필요가 없죠.

리버스 엔지니어링과 익스플로잇 개발을 위해서 어셈블리 언어에 대해 속속들이 알 필요는 없지만 “빅 픽쳐"는 이해하고 있어야 합니다. 이러한 기본기는 본 튜토리얼에서 다룰것입니다. 만약 더 깊게 알고싶다면 이 챕터 맨 아래의 링크들을 참고하세요.

그렇다면, 어셈블리 언어란 정확하게 무엇일까요? 어셈블리 언어는 머신 코드의 상단에 있는 얇은 문법 레이어(thin syntax layer)로서, 컴퓨터가 알아듣는 바이너리 표현(머신 코드)을 인코딩 하여 보여주는 것입니다. 그러면 우리가 바로 기계 언어를 바로 쓰면 어떨까요? 음, 그렇다면 매우 고통스러울 것입니다. 우리 컴퓨터는 어셈블리 코드를 그대로 실행할 수 없습니다. 왜냐하면 기계어만 실행 가능하기 때문입니다. 우리는 어셈블리 코드를 기계어로 바꾸는 GNU 어셈블러인 GNU Binutils 프로젝트를 통해 *.s 확장자를 가진 소스코드를 처리할 것입니다.

어셈블리를 *.s 확장자를 가진 파일로 작성 하였다면, 그것을 아래와 같이 as 명령어를 통해 변환 후 ld 명령을 통해 바이너리로 링크 시킬 수 있습니다.

$ as program.s -o program.o  # 어셈블리를 기계어로
$ ld program.o -o program    # 기계어를 바이너리로

어셈블리의 내부에서

어셈블리 언어의 매우 아래에서부터 위까지 공부해봅시다. 매우 아래 레벨에는 우리는 기계 회로의 전기 신호가 있죠. 전류는 신호로 변환되어 두 개의 레벨로 나뉩니다 - 0 볼트를 “꺼짐”, 5 볼트를 “켜짐” 이라고 합시다. 왜냐하면 시각적으로는 회로가 정확하게 몇 볼트인지 알 수 없어서, 우리가 전류가 켜짐/꺼짐 상태임을 숫자 0과 1을 통해 나타내기 위함입니다. 그 후 0과 1을 컴퓨터 프로세서가 이해하는 가장 작은 단위인 기계어 형태로 표현합니다. 아래에 기계어로 작성한 명령어 예제입니다.

1110 0001 1010 0000 0010 0000 0000 0001

여기까진 좋은데.. 저런 0과 1의 패턴이 어떤 뜻인지 기억하기 쉽지 않습니다. 그래서 우리는 니모닉(mnemonics)이라고 하는 0과 1의 바이너리 패턴을 이해하기 쉽도록 줄임말 형태를 사용하며, 이것이 각 기계어 명령어의 이름입니다. 이 프로그램은 어셈블리 언어 프로그램으로 불리며, 각 컴퓨터의 어셈블리 언어란 컴퓨터의 기계어를 니모닉의 집합으로 표현한 것을 말합니다. 그러므로, 어셈블리 언어는 인간이 컴퓨터를 프로그래밍 할 때 사용되는 가장 낮은 레벨의 언어입니다. 명령의 각 피연산자(operands)는 니모닉 다음에 옵니다. 아래의 예제를 참고해 주세요.

MOV R2, R1

이제 우리는 어셈블리 프로그램이 니모닉 이라고 불리우는 언어적 정보의 집합으로 만들어 졌다는 것을 알았으며, 우리는 그것들을 기계어로 변환해야 한다는 사실을 알았습니다. 위에서 언급한 것 처럼 ARM 어셈블리의 경우에는 GNU Binutils 프로젝트에 포함되어 있는 as 명령어를 통해 기계어로 변환할 수 있으며, 이 과정을 어셈블링(assembling) 이라고 부릅니다.

정리하면, 우리는 컴퓨터가 전기 신호의 존재 여부를 인식하도록 만들어 졌고, 우리는 다양한 신호의 집합을 0과 1(bits)의 집합으로 표현할 수 있다는 사실을 배웠습니다. 우리는 기계어(신호의 집합)를 사용해서 컴퓨터를 미리 설계된 방식으로 작동하도록 제어할 수 있습니다. 우리가 그러한 신호의 집합을 모두 외우고 있기 어려우므로, 줄임말인 니모닉(mnemonics)을 사용하여 각 작동 방식 별로 명령어(이름)를 부여했습니다. 니모닉의 집합을 컴퓨터의 어셈블리 언어라고 부르며 우리는 어셈블러 라고 불리는 프로그램을 사용해서 하이레벨 언어를 위해 컴파일러가 하는 것 처럼 니모닉의 집합을 컴퓨터가 이해할 수 있는 기계어로 변환합니다.

더 자세히 알아보고 싶다면..

  1. ARM 어셈블리의 회오리 투어(Whirlwind Tour)

https://www.coranac.com/tonc/text/asm.htm

  1. 라즈베리파이의 ARM 어셈블러

http://thinkingeek.com/arm-assembler-raspberry-pi/

  1. 리버스 엔지니어링 실무: x86, x64, ARM, 윈도우즈 커널, 리버싱 도구, 난독화 (작성자: Bruce Dang, Alexandre Gazet, Elias Bachaalany and Sebastien Josse)

  2. ARM 레퍼런스 메뉴얼

http://infocenter.arm.com/help/topic/com.arm.doc.dui0068b/index.html

  1. 어셈블러 사용자 가이드

http://www.keil.com/support/man/docs/armasm/default.htm