본문 바로가기

System Programming/CSAPP Lab

[Bomb Lab] Phase 1, gdb 명령어, assembly code 변환, gdb 메모리 참조

지난 포스팅에서 Bomb Lab을 시작할 준비를 끝냈으므로, 이제 맨 첫 단계인 phase_1 함수부터 분석해보기로 한다. bomb.c 소스 코드를 보면 알 수 있는데, 이 프로그램은 총 phase 1부터 6까지 6개의 단계로 구성되어 있다. 하나의 단계를 해결하면 그 다음으로 넘어가는 식으로 이루어져 있다.

 

다시 gdb 디버거로 bomb 프로그램을 여는 것으로 시작한다.

 

Assmebly 코드로 변환하기

disas (함수명)

 

을 입력하면 함수에 대한 assembly 코드 변환 결과를 보여준다. 먼저, main 함수에 대해 assmebly 코드를 보면,

PC 값과 그에 해당하는 assembly 명령어가 출력된다. procedure call (함수 호출) 명령어의 경우, 호출하는 함수 이름과 함께 출력된다. c 소스 코드에서 본 것처럼 phase_1, 2, ... , 6까지 있는 것을 볼 수 있다. 먼저 phase_1 함수가 어떻게 돌아가는지에 대한 정보를 얻어야 하므로, 이 함수에 대해 disassembler를 다시 돌려본다.

assembly 코드를 읽어보면, stack pointer 값을 변경시키고 argument 레지스터에 값을 옮겨주고, strings_not_equal() 함수를 실행한다. 이 결과가 0이면 jump를 하여 phase_1 함수를 이어서 실행할 것이고, 그렇지 않으면 explode_bomb() 함수를 호출하며 프로그램이 종료된다.

 

strings_not_equal 함수가 0을 반환하도록 해야 한다. 이 함수를 disassmebler로 열어보면,

레지스터 rdi, rsi가 함수의 argument를 담고 있는 레지스터이므로, 이 값을 확인할 필요가 있다. 그 아래 부분은 레지스터에 포함된 정보를 이용하여 string_length 만큼 두 string의 문자끼리 비교하는 코드일 것이다. cmp 명령어로 두 값을 비교하여 같다면 zero flag가 켜질 것이다. je 명령어는 zero flag가 켜져있을 때 jump하므로, 예를 들어 +41의 명령어의 경우 zero flag가 켜져있으면 함수의 동작을 이어가고 그렇지 않으면 함수 종료 부분으로 바로 jump하도록 되어 있는 것을 알 수 있다.

 

Breakpoint 설정하기

이제 breakpoint를 잡아서 그 시점에 레지스터가 어떤 값을 가지고 있는지를 확인해야 한다.

 

break (함수명 또는 PC)

 

strings_not_equal 함수의 맨 첫 명령어의 주소인 0x401338에 breakpoint가 설정되었다. 이제 프로그램을 실행시킨다.

 

run

 

만약 이어서 실행시키려면,

continue

 

를 입력하면 된다.

 

맨 처음에 실행시켰을 때와 같이 우선 뭔가 문자열이 출력되고 사용자 입력을 기다린다. hi라고 마찬가지로 입력을 했고, 이번에는 breakpoint를 설정해놨으므로 여기까지만 실행되고, bomb explode까지는 가지 않았다. 이제 이 시점에 레지스터 rdi (1번째 argument), rsi (2번째 argument) 값을 살펴보기로 한다.

 

info register

 

첫 번째는 hex로, 두 번째는 같은 값을 decimal로 출력한 것이다. 아무래도 컴퓨터 시스템 레벨에서 볼 때는 hex 표현이 유용한 경우가 많을 것이다.

 

rdi, rsi에 문자열이 저장되어 있을 것이고, c에서 문자열 자료형을 생각해본다면 맨 첫 번째 문자가 저장된 메모리의 주소를 값으로 가지고 있을 것이다. 특정 주소의 메모리 값을 확인하는 명령어는

 

x/(옵션) (주소 값)

 

인데, 옵션에서 몇 개의 정보 단위를 읽어올지, 출력 형식을 어떻게 할지를 지정할 수 있다. 또한 주소 값은 상수로 줄 수도 있고, 레지스터를 통해 참조하는 것도 가능하다. 

 

예를 들어, rdi 레지스터에 저장된 값인 0x603780을 복사하여 이를 주소 값으로 하여 hex 형식으로 출력한 것과 $rdi를 주소 값으로 하여 출력한 것은 같다.

출력된 값은 0x68 = 104인데, 아스키 코드 표를 참조해보면 소문자 h에 해당한다. 처음에 input으로 넣어준 hi가 들어있음을 추측할 수 있는데, 실제로 2 byte를 읽어보면 그 다음 값은 105이고, 이는 소문자 i에 해당한다. 그 다음부터는 NULL을 뜻하는 0이 찍힌다. (명령어: x/3bx $rdi, 3 byte를 읽어와서 이를 hex로 출력)

 

한편 문자열로 출력할 수도 있는데, 출력 옵션을 s로 주는 것이다. 그러면 읽어올 정보 단위가 문자열 끝까지로 자동 설정되는 것 같다. 입력 문자열 전체를 읽어오는 것을 확인할 수 있다.

 

즉, $rdi 레지스터는 사용자가 입력한 입력 문자열이 저장된 메모리의 맨 첫 주소 값을 가지고 있다. 아마도 assembly 코드를 분석한다면, 이를 base address로 하여 offset을 반복적으로 더함으로써 각 문자들을 순차적으로 참조할 것이다.

 

이제 binary bomb 해체에 중요한 $rsi 레지스터도 마찬가지로 뜯어보자.

미리 저장된 문자열을 얻을 수 있다. 이제 이 두 문자열이 같다면 0을 return하면서 다음 phase로 이어갈 것이다. 이를 breakpoint를 통해 확인해보자. 이제, strings_not_equal 함수의 맨 마지막 줄에 breakpoint를 걸어둔다.

사용자 입력에는 hi를 입력하고, continue를 통해 두 번째 breakpoint까지 실행시킨다. 여기서 레지스터 정보를 출력해보면, return 값이 1이다.

이번에는 메모리 참조를 통해 얻은 문자열을 입력하고 다시 위 작업을 반복해본다.

 

kill

 

명령어를 통해 이전 프로그램 실행을 종료시킬 수 있다.

'Border~'로 시작하는 문자열을 입력하였다. 이번에는 rax의 값이 0인 것을 확인할 수 있다. 이제 이후까지 프로그램이 잘 이어서 돌아가는지를 확인해보자.

이제 phase 1을 성공적으로 통과하였다. 'Phase 1 defused. ~ ' 문자열은 c 소스 코드에서도 확인할 수 있다. 이제 새로운 사용자 입력을 받으며 phase 2로 넘어가는 것이다.