BombLab - Phase 1, 2

beans_I·2024년 2월 23일
0

Main

main 함수 분석

폭탄을 해체하기에 앞서, main 함수를 디스어셈블하여 프로그램의 전체적인 구조를 파악하자.

(현재 BOMB과는 다른 BOMB의 사진이지만, 설명에는 문제 없기에 해당 이미지를 사용하였습니다.)

main 함수의 흐름은 각 페이즈마다 read_line 함수로 입력을 받은 뒤, 해당 단계의 함수(phase_1, phase_2, ...)를 호출하는 구조로 이루어져 있다. read_line 함수의 반환 값, 즉 사용자 입력 문자열의 주소는 첫 번째 인자를 전달하는 레지스터인 %rdi에 저장되어 각 phase 함수로 전달된다. 그리고 phase_defused함수를 실행하는 구조로 이루어져있다.

우선은 이정도까지만 알고, phase1을 보도록 하자.


Phase 1

phase_1 함수를 디스어셈블하면 다음과 같다.

<+11>에서 strings_not_equal 함수를 호출하는 것이 핵심이다. 함수명으로 보아 두 문자열이 다른지 판별하는 역할을 추측할 수 있다. 함수의 인자는 리눅스 x86-64 호출 규약에 따라 %rdi, %rsi, %rdx, %rcx 순서로 전달된다.

  • %rdi: main 함수에서 전달된 사용자 입력의 주소이다.
  • %rsi: <+4>에서 0x18a1(%rip)에 위치한 값을 로드한다. 이는 정답 문자열의 주소일 확률이 높다.

GDB를 통해 %rsi에 로드될 메모리 주소에 저장된 문자열을 확인해보자.

정답으로 추정되는 문자열을 찾았다. strings_not_equal 함수는 두 문자열이 다르면 0이 아닌 값을 반환하고, 같으면 0을 반환할 것이다. <+16>, <+18>jne (Jump if Not Equal) 명령어는 함수의 반환 값(%eax)이 0일 때 분기하여 폭탄을 맞게 되므로, 두 문자열이 같아야 함을 알 수 있다.

따라서 Phase 1의 정답은 GDB로 확인한 문자열이다.

답: When I get angry, Mr. Bigglesworth gets upset.


Phase 2

phase_2phase_1보다 길지만, 순차적으로 분석하면 복잡하지 않다.

함수의 초반부(_start ~ <+22>)는 스택 공간을 설정하는 전처리 과정이다. fs:0x28의 주소는 스택 가드 검사를 위한 것이라고 한다. - https://stackoverflow.com/questions/10325713/why-does-this-memory-address-fs0x28-fs0x28-have-a-random-value)

1. 입력 형식 확인

<+25>에서 read_six_numbers 함수를 호출하는 것으로 보아, 6개의 정수를 입력받는 것을 추측할 수 있다. GDB를 사용하여 이 추측을 확인한다.

  1. *phase_2 + 25에 breakpoint를 설정한다.
  2. 임의의 6개 정수(3 2 1 0 1 2)를 입력하고 프로그램을 실행한다.
  3. ni 명령어로 read_six_numbers 함수를 실행시킨 후, 스택 포인터($rsp)가 가리키는 메모리 영역을 16진수, Word단위로 확인한다.

스택을 확인하면 입력한 3 2 1 0 1 2가 순서대로 저장된 것을 볼 수 있으며, 이를 통해 6개의 정수를 입력받는다는 추측이 옳았음을 알 수 있다.

2. 첫 번째 숫자의 조건

다음 명령어는 스택의 가장 위에 있는 값, 즉 첫 번째 입력값과 0x1을 비교한다.

*(%rsp)는 스택의 최상단 값을 의미하며, 이는 첫 번째로 입력한 정수이다. jne(Jump if Not Equal) 명령어를 통해 이 값이 1이 아니면 폭탄이 터지는 것을 알 수 있다. 따라서 첫 번째 숫자는 반드시 1이어야 한다.

현재까지의 답: 1 ? ? ? ? ?

3. 나머지 숫자의 규칙

첫 번째 숫자가 1이라는 사실을 바탕으로, 나머지 다섯 개 숫자의 규칙을 파악하기 위해 프로그램의 핵심 로직인 반복문을 분석해보자. 이를 위해 첫 번째 폭탄을 피할 수 있는 1로 시작하는 임의의 입력값인 1 2 1 0 1 2을 사용한다.

반복문 진입 전 레지스터 및 메모리 설정

반복문에 진입하기 전, 프로그램은 다음과 같이 레지스터와 메모리를 설정한다.

  • <+36> mov %rsp, %rbx: %rbx 레지스터가 %rsp가 가리키는 스택에 저장된 6개의 숫자 배열의 시작 주소를 가리키도록 한다.
  • <+39> lea 0x14(%rbx), %rbp: %rbp 레지스터에 *(%rbx+20)의 값을 저장한다. (0x14 = 20) 앞서 봤듯, 입력된 6개의 정수는 워드(4바이트) 단위로 *(%rbx),*(%rsp)가 가리키는 주소에 연속적으로 저장된다. %rbx를 기준으로 각 숫자의 위치는 다음과 같다.
    • (%rbx): 첫 번째 숫자

    • (%rbx + 4): 두 번째 숫자

    • (%rbx + 8): 세 번째 숫자

    • ...

    • (%rbx + 20): 여섯 번째 숫자

      따라서 마지막에 입력한 값인 2가 %rbp에 저장된다.

반복문 로직 분석

  1. 값 로드

    <+61> mov (%rbx),%eax 명령어를 통해 포인터 %rbx가 가리키는 현재 값을 %eax 레지스터에 로드한다. 예를 들어, 루프의 첫 진입 시 %rbx는 첫 번째 숫자인 1을 가리키므로 %eax에는 1이 저장된다.

  2. 2배 연산

    <+63> add %eax,%eax를 실행하여 %eax의 값을 2배로 만든다. (%eax1 * 2 = 2가 된다).

  3. 값 비교

    <+65> cmp %eax,0x4(%rbx)는 2배가 된 %eax의 값과 다음 숫자를 비교한다. 0x4(%rbx)는 현재 위치에서 4바이트 떨어진 주소, 즉 배열의 다음 원소를 의미한다.

  4. 조건부 분기

    <+68>je 명령어는 비교 결과 두 값이 같을 경우에만 분기하여 폭탄을 피하게 한다.

  5. 포인터 값 이동
    <+52> add $0X4, %rbx
    를 취해 %rbx의 값을 4씩 증가시키며(다음 숫자로 이동) 입력된 6개의 숫자를 모두 순회할 때까지 계속된다. (종료기준: <+56>, <+59>)

이 로직을 통해 N번째 숫자는 N-1번째 숫자의 2배여야 한다는 핵심 규칙(Ni=2×Ni1N_i =2 \times N_{i-1})을 도출할 수 있다.

앞서 첫 번째 숫자는 1이어야 함을 확인했고, 나머지 숫자들은 이전 숫자의 2배라는 규칙을 따르므로 전체 수열을 완성할 수 있다.

: 1 2 4 8 16 32

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

0개의 댓글