BombLab - Phase 3

beans_I·2024년 2월 23일
0

Phase 3

Phase3는 문제 유형이 2개인데, 이 포스트에서는 입력받는 값이 3개(%d %s %d)인 경우를 다룬다.

그러면 한 번 phase_3의 어셈블리 코드를 해석해보자.

Phase 3는 어셈블리 코드를 분석하여 C언어의 switch 구문과 유사한 구조를 찾아내는 것이 핵심이다.

1. 입력 형식 확인

함수 초반부에서 scanf 함수를 호출하여 사용자 입력을 받는다.

  • <+35>: `mov 0x184f(%rip),%rsi명령어를 통해 포맷 문자열의 주소를%rsi 레지스터에 로드한다. GDB로 해당 주소(0x555555402be6)의 문자열을 확인하면 **%d %c %d`** 임을 알 수 있다. %rsi 는 함수에 들어가는 인자이므로 이 값이 <+42> 의 scanf 함수(<__isoc99_scanf@plt>)로 들어감을 확인할 수 있다. 입력된 세 값(정수, 문자, 정수)은 스택 포인터(%rsp)를 기준으로 한 특정 위치에 저장된다. 이 값들은 <+20>에서 <+30> 사이의 명령어들을 통해 파악할 수 있다.
    • %rdx: 첫 번째 정수(%d)가 저장될 주소 (%rsp + 16)

    • %rcx: 문자(%c)가 저장될 주소 (%rsp + 15)

    • %r8: 두 번째 정수(%d)가 저장될 주소 (%rsp + 20)

      각 데이터 타입의 크기(정수 4바이트, 문자 1바이트)를 고려하면, 이 명령어들은 스택에 세 개의 변수를 위한 공간을 마련하고 각 변수의 시작 주소를 scanf의 인자로 넘겨주는 것과 동일하다.

      예를 들어 사용자가 2 n 4를 입력하면, scanf 함수가 실행된 후 스택에는 다음과 같이 값이 저장된다.

  • <+47>, <+50>: scanf의 반환 값(%rax)이 2보다 큰지 비교한다 (cmp $0x2,%eax, jle ). scanf는 성공적으로 입력받은 인자의 개수를 반환하므로, 입력값은 반드시 3개여야 한다.

2. 첫 번째 정수 조건 확인

코드의 다음 부분은 첫 번째로 입력된 정수에 대한 조건을 검사한다.

  • <+52>, <+57>: cmp $0x7,0x10(%rsp) 명령어는 스택에 저장된 첫 번째 정수가 7보다 큰지 비교하고 크면은 폭탄이 터진다(ja <phase_3+331> ,ja는 unsigned 비교).

따라서 첫 번째 정수는 0에서 7 사이의 값이어야 한다.

현재까지의 답: (0-7) (char) (정수)

3. Jump Table을 이용한 분기 (Switch 구문)

  • <+63>: %eax에 %rsp+10, 첫 번째 정수 x의 주소값을 넘겨준다.
  • <+67>: lea 0x1849(%rip),%rdx 명령어로 특정 주소를 %rdx에 로드한다. 이 주소는 <+74> ~<+81>을 보면은 분기할 주소들의 목록, 즉 점프 테이블의 시작 주소임을 알 수 있다. 해당 주소에 16진수, 워드 단위로의 값출력을 하면은 다음과 같은 값이 나온다.

  • <+74>: movslq (%rdx,%rax,4),%rax 명령어가 실행된다. 여기서 봐야할 점은 다음과 같다.
    1. %rax에는 <+63>을 통해 첫 번째 입력 정수 값이 들어있다.

    2. (%rdx, %rax, 4)rdx + rax * 4 주소를 의미한다. 즉, 시작 주소(<+67>)에서 첫 번째 정수 값만큼의 인덱스를 이용해 이동한다. 이로 $0x555555402c00 은 배열로 유추할 수 있다.

      결과적으로, 이 명령어는 첫 번째 정수 값에 해당하는 분기 주소를 테이블에서 읽어와 %rax에 저장한다.

      만약 첫 번째 정수 x2 이면은 %rax에는 *(0x555555402c00 + 4 x 2)의 값이 sign extension되어 0xffffffffe80b 이 저장된다.

  • <+78>: add %rdx, %rax 명령어는 이 오프셋 값에 점프 테이블의 시작 주소(%rdx)를 더하여 실제 실행할 코드의 주소를 계산한다.
    첫 번째 정수 x2일 경우에는 0x555555402c00 + 0xffffffffe80b = 0x55555540140b 이 된다. 이 값이 %rax에 저장된다.
  • <+81>: jmpq *%rax 명령어는 %rax에 저장된 주소로 프로그램 카운터를 이동시킨다.

이러한 구조는 C언어의 switch문이 컴파일된 전형적인 형태이다. 첫 번째 정수 x의 값(0~7)에 따라 8개의 다른 코드 블록으로 분기하게 된다.

4. 두 번째 정수 조건, Char조건 확인

첫 번째 정수 x2일 경우일 때의 분기를 보도록 하자. 0x55555540140b<Phase_3 +158>에 해당한다.

  • <+158>:%eax에 0x69의 값을 넣는다.
  • <+163>:  *(rsp+20)(두 번째 정수 y)과 0x2f2가 동일한지 판단한다. 동일하면은 <+341>로 분기하고, 그렇지 않으면은 폭탄이 터진다.

따라서 x2 일 때, 두 번째로 입력하는 정수 y는 0x2f2(754)이다.

이 명령어에선 %al(rax 레지스터에서 하위 8비트 부분)과 스택에 저장된 문자(*(%rsp+15))를 비교한다. 이전 단계에서 switch문의 각 case 블록은 %eax에 고유한 값을 저장했다 (<+158>).

따라서 입력된 문자는 해당 case에서 지정한 값의 ASCII 코드와 일치해야 한다. 예를 들어, x=2의 경우 문자는 반드시 ASCII 코드 0x69, 즉 'i'여야 한다.

5. 최종 조건

앞선 분석 과정을 종합하면, Phase 3의 핵심 로직은 C언어의 switch문과 동일한 구조임을 알 수 있다.

  1. 첫 번째 정수 입력은 switch(x)문의 x처럼 분기할 case를 선택하는 역할을 한다. 이 값은 0에서 7 사이로 제한된다.
  2. 0x555555402c00 주소는 점프 테이블, 즉 각 case로 점프하기 위한 주소 오프셋을 담은 배열을 가리킨다.
  3. 프로그램은 첫 번째 정수 입력을 인덱스로 사용하여 이 테이블에서 해당 오프셋 값을 읽어온다.
  4. 읽어온 오프셋을 기반으로 실제 점프할 코드의 주소를 계산하여 해당 case의 로직으로 분기한다.

이러한 메커니즘을 통해, 첫 번째 정수 x의 값에 따라 분기되는 8개의 주소는 다음과 같이 정리할 수 있다.

  • x = 0 : <+90>
  • x = 1 : <+124>
  • x = 2 : <+158>
  • x = 3 : <+192>
  • x = 4 : <+223>
  • x = 5 : <+250>
  • x = 6 : <+277>
  • x = 7 : <+304>

그리고 이 분기에는 두 번째 정수 입력과 Char 입력 조건이 작성되어 있다. 정리해보면 답은 다음과 같다.

답:
0 t 135

1 j 923

2 i 754

3 d 178

4 n 307

5 k 432

6 f 794

7 b 343

profile
노션으로 옮겼습니다. https://beans-i.notion.site/main?pvs=74

0개의 댓글