본문 바로가기

System Programming/CSAPP Lab

[Bomb Lab] Phase 2, assembly cmp/jmp, 분기, 반복문

phase_2() 함수의 assembly  확인

disassembler로 phase_2() 함수의 assembly 코드를 출력해보면 다음과 같다.

함수의 2번째 arg에 해당하는 rsi 레지스터에 스택 포인터 rsp 값을 넘겨준 뒤, read_six_numbers() 함수를 호출한다. 이 함수의 disassembler를 돌려보면

이 함수에 breakpoint를 찍고 프로그램을 실행시킨다. 이 때, phase 1은 풀었던 결과대로 정답을 바로 입력해주고, phase 2는 1 2 3 4 5 6이라는 임의의 6개 숫자를 입력한다.

함수의 1번째 arg인 rdi 레지스터가 가리키는 곳의 메모리 값을 찍어보면 입력했던 6개의 숫자가 들어있음을 확인할 수 있다.

 

스택 확인

read_six_numbers() 함수 호출 후에는 스택 포인터 rsp 레지스터의 값이 연산에 사용되는 것을 확인할 수 있다. read_six_numbers()가 종료된 직후의 PC 값에 breakpoint를 찍고 이어서 실행시킨다. 이 때 스택 포인터가 가리키는 주소에서 6개의 word(=4-byte)를 읽어와 decimal로 출력해보면 입력했던 숫자를 얻는다.

 

처음에는 read_six_numbers() 함수 내부를 더 뜯어보려고 했는데, scanf가 나타난 부분에서 막혀서 아예 함수를 빠져나온 후에 레지스터 값을 출력해보았다. 일단 아직 폭탄이 터지지 않았으므로 프로그램이 계속 돌아가고 있다는 것을 알 수 있다.

 

이를 통해 역으로 추적해보자면, read_six_numbers()는 사용자 입력으로 받은 문자열을 정수로 변환하여 스택에 순서대로 저장하는 동작을 수행한다고 이해할 수 있다.

 

cmp & jump 명령어로 분기/반복 흐름 파악하기

read_six_numbers() 함수가 수행된 후에는 스택 포인터가 현재 가리키는 값과 1을 비교하여 같은 경우만 jump하여 함수를 이어서 실행한다. 즉, 첫 번째 숫자는 1이어야 한다는 것을 알 수 있다.

 

우연히 첫 번째 숫자를 맞춘 셈이 되었는데, 만약 그렇지 않았다면 첫 번째 숫자에 1을 넣은 상태로 다시 프로그램을 실행시키면 된다. 그러면 이제 phase_2 함수 내의 +52 줄까지 이어서 실행이 될 것이다.

 

lea 명령어에서 rbx, rbp 레지스터에 값을 할당한다. rbx에는 스택 포인터 + 4의 값을 저장하는데, 이 값을 찍어보면 입력했던 두 번째 숫자이다. 그 이유는 하나의 정수 당 4-byte 크기이고, 스택 포인터가 현재 가리키는 값 기준으로 첫 번째 숫자부터 저장되어 있기 때문이다. rbp에는 스택 포인터 + 24의 값을 저장하는데, 여기에는 의미 없는 값이 들어있다. 입력했던 마지막 숫자는 스택 포인터 + 20에 들어있다. 이 역시 메모리를 주소 값을 넣어 찍어보면 알 수 있다.

다음은 jump 명령어인데, 조건에 상관없이 +27 줄로 이동한다. 27줄, 30줄, 32줄을 실행한 뒤, 34줄에서 폭탄이 터지거나 실행을 이어간다. 즉, 32줄의 cmp 명령어의 해석이 중요하다는 것을 알 수 있다. 41줄로 간 뒤에는 27줄, 64줄 둘 중 하나로 넘어가게 되는데, 27줄은 반복이고, 64줄은 함수의 종료이므로 반복문을 계속 실행할지 여부를 판단하는 코드 정도로 해석할 수 있다.

 

그러면 27, 30, 32줄이 어떤 동작을 하는지를 따져보자.

 

27줄에서는 eax 레지스터에 rbx - 4, 즉 rsp (=스택 포인터)가 가리키고 있는 메모리의 값을 저장하고, 이는 위에서 확인한 것과 같이 사용자 입력 첫 번째 숫자이다. 30줄에서는 add 명령어를 통해 eax 레지스터 값을 2배하여 저장한다. 그리고 32줄에서는 이 값을 rbx가 가리키는 메모리 값과 비교하여 같은지를 확인하는데, 함수의 흐름을 이어가려면 비교 결과가 같아야 한다. rbx는 두 번째 숫자를 가리키므로, 첫 번째 숫자 * 2와 두 번째 숫자가 같아야 한다는 결론이 나온다.

 

41줄에서 rbx에 4를 더하면서 반복이 진행되는데, 위 문단의 내용을 일반화해보면 n번째 숫자는 (n-1)번째 숫자 * 2와 같아야 한다 라는 결론이 된다. 이 때, 첫 번째 숫자는 1이어야 하므로 6개의 숫자는 1 2 4 8 16 32 이다. 이제 이를 입력해보면

'That's number 2 ~' 문자열이 출력되고, c 소스 코드에서 확인해보면 이것은 phase 2를 성공적으로 해결했을 때 출력되는 문자열이다.