본문 바로가기

System Programming/CSAPP Lab

[Bomb Lab] Phase 5, 문자열 조작 연산, gdb 메모리 값 참조

Assembly code of phase_5

assembly code 1/2
assembly code 2/2

첫 번째 분기문: string_length 함수의 return 값 확인

29, 32줄에서 첫 번째 분기문이 있는데, 폭탄이 터지지 않고 실행을 이어가려면 string_length 함수의 return 값이 6이어야 한다. 함수 이름으로부터 유추해보면, 입력한 문자열의 길이가 6인 것이라고 추측해볼 수 있다. 먼저, breakpoint를 29줄에 걸어둔다.

 

예를 들어, hello world를 입력해보면,

eax 레지스터 값이 11이다. hello world 문자열의 길이가 11이므로, string_length 함수는 사용자 입력 문자열의 길이를 return한다고 추측할 수 있다. 이제 다시 길이가 6인 임의의 문자열 helloo를 입력하고 계속 분석하기로 한다.

 

112줄로 jump했다가 eax 레지스터를 0으로 만들어준 뒤 다시 41줄로 돌아오므로, 분석을 41줄부터 이어나가면 된다.

 

반복문 구조 (41줄~74줄)

41줄부터 74줄은 반복문임을 알 수 있다. rax 레지스터에 1씩 더하고 6과 같은지를 확인해서 아니면 계속 41줄로 돌아가기 때문이다. 이 부분의 분기는 사용자 입력에 의존하지 않고, 폭탄이 터지는 곳으로 가지도 않으므로, 실행을 이어갈 것이다. breakpoint를 strings_not_equal 함수에 두고 계속 실행을 시켜본다.

 

strings_not_equal 함수

함수 내부를 분석하기 전에 먼저 96, 98줄을 보면, 함수의 return 값이 0이어야 119줄로 jump하여 실행을 무사히 이어갈 수 있다. strings_not_equal 함수가 0을 return해야 한다는 사실을 확인한 뒤, 함수 내부를 들여다보기로 한다.

4, 7줄에서 함수의 1, 2번째 argument에 해당하는 rdi, rsi 레지스터의 값을 옮겨담는 부분이 있는데, 이를 통해 이 함수가 2개의 argument를 갖는다는 것을 추측할 수 있다. 이 값들이 어떤 것을 가리키는지를 확인해보기로 한다.

rdi 레지스터가 스택 영역의 주소 값을 가지므로 사용자 입력을 가리키고 있을 것인데, 메모리 값을 출력해보니 사용자 입력이 뭔가 변해있는 것을 알 수 있다. 한편, rsi 레지스터는 미리 정의되어 있는 문자열을 출력하고, 마찬가지로 길이가 6이다. 이제 이 두 문자열이 같아야 하는데, 지금은 다르기 때문에 함수가 종료되는 곳에 breakpoint를 찍어보고 rax 레지스터 값을 확인해보면 1이다.

 

이를 풀어내려면, 다시 41줄~74줄의 반복문에서 어떠한 조작을 하는지를 구체적으로 알아야 한다. 다시 반복문으로 돌아가기로 한다.

 

그리고 조작의 결과는 flyers 라는 문자열이 되어야 한다.

 

41~74줄의 반복문은 사용자 입력 문자열을 바꾸는 연산을 한다

다시 연산의 핵심이 되는 부분의 assembly code만 가져오기로 한다.

여기서 66줄부터는 반복문의 반복 여부를 확인하는 부분이므로, 연산 부분은 41줄부터 62줄까지의 6개의 명령어이다.

42줄에서 rax, rbx 레지스터를 사용하는데, 일단 rax 레지스터는 0이고, rbx 레지스터를 주소로 하여 메모리 값을 찍어보면 사용자 입력 문자열이 들어있다. 저장 공간을 확인해보면, 하나의 char 당 1-byte를 차지하고 있다.

 

참고로, char가 1-byte 자료형이라서 1-byte만 접근하는 명령어들이 있는데, 예를 들어 레지스터 cl, dl은 각각 rcx, rdx 레지스터의 하위 1-byte이다.

 

41줄에서는 rbx 레지스터 값을 base address로 하여 1-byte를 읽어온다. movzbl은 zero-extended byte를 move한다는 것이므로 큰 의미는 없다. 41줄이 실행된 뒤, ecx 레지스터에는 1번째 문자가 들어있다. 지금은 h의 아스키 값인 104가 들어있다.

 

45줄에서는 이 값을 스택 포인터가 가리키는 주소의 메모리에 저장한다.

 

48, 52줄에서는 이 값을 rdx 레지스터에 옮기고 0xf와 AND 연산을 하는데, 0xf가 하위 4-bit만 1111임을 생각한다면, 하위 4-bit만 선택하는 연산이라고 이해할 수 있다. 이 연산을 수행한 후에, rdx 레지스터에는 8이 담겨있다. 0x68 중 하위 4-bit만 취했기 때문이다.

 

55줄에서 미리 정의되어 있는 상수와 rdx 레지스터 값을 더한 값을 주소로 하여 메모리에 접근해 이 값을 rdx 레지스터에 저장한다. 이를 찍어보면,

 

62줄의 명령어는 조작한 rdx 레지스터 값을 스택 포인터 + 16에 순서대로 저장한다.

 

이제 여기서 flyers에 해당하는 문자를 찾아야 한다. 각각의 offset을 구하면

 

f: 9

l: 15

y: 14

e: 5

r: 6

s: 7

 

0xf와의 AND 연산 결과 값의 범위가 0~15임을 생각한다면, 문자열의 길이가 딱 들어맞는다. 이제 하위 4-bit을 위 숫자로 하고, 상위 4-bit을 적절히 아무 값이나 주면 될 것이다.

 

그래도 영어 소문자 중에 고른다면,

 

ionefg

 

가 되겠다.

 

 

물론, 하위 4-bit만 상관있으므로, 하위 4-bit이 같은 한 상위 bit을 어떻게 잡든 상관이 없다. 예를 들어, 첫 번째 정답 문자열은 16진수 기준 _9 인 모든 문자가 가능하므로 소문자 i (아스키 코드 0x69) 외에 대문자 I (아스키 코드 0x49), 숫자 '9' (아스키 코드 0x39) 등도 모두 가능하다.

 

1-byte 자료형인 char은 컴퓨터 메모리에는 결국 ascii code 규칙에 의해 변환된 숫자로 저장되어 있고, 이 값은 숫자로서 사용될 수 있다는 것을 보여주는 실습이었다. AND 연산이나 메모리 주소 값 연산에 char 값을 그대로 사용했던 것에서 알 수 있었다.