정리 내용은 리버스 엔지니어링 바이블 책의 내용 입니다.
문자열 컨트롤은 프로그래밍의 기본 중의 기본이라고 한다. 별도의 문자열 처리 라이브러리를 이용해 문자열을 컨트롤하는 경우도 많겠지만 기본적인 strcpy나 strcat 등의 구조를 알아두면 문자열 뿐 아니라 보안 프로그램이나 악성코드 등에서 많이 사용하는 바이너리 컨트롤에 대해서도 많은 영감을 얻을 수 있다고 한다.
BOF를 예방하기 위해 strcpy보다는 안전한 함수를 써야하는거 아닌가요? 라는 식의 질문은 태클이라나다. 악성코드가 안전을 신경쓸 필요가 없다는게 저자의 말이다. 나도 동감한다.
xor eax, eax
lea edx, dword ptr ss:[esp]
push esi
push edi
- xor eax, eax는 eax를 0으로 만들어버리는 것으로 알고 있다. xor은 비교되는 2개의 bit가 서로 달라야만 결과 값이 1이
되고, 같을 경우 0이 되기 때문에 xor eax, eax를 하게 되면, eax는 0으로 초기화 된다.
- 그리고 esp의 번지를 lea 명령어를 이용해 edx에 집어 넣는다.
- push esi와 push edi를 살펴보자. push 명령어로 이렇게 레지스터 (esi, edi)를 보존한다는 것은 이 레지스터를 이제부터 변수처럼 사용한다는 것과 관계가 깊다. 아울러 esi와 edi를 보존하는 것으로 봐서 지금부터 가동되는 코드는 출발지와 목적지와 관계가 있다고 생각할 수 있다.
mov edi, example.00407034 "Hello World !" ; ASCII
repne scas byte ptr es:[edi]
not ecx
- 출발지로 "Hello World !" 라는 문자열이 00407034 번지에 들어가 있는데 이 문자열을 edi에 넣는 코드가 보인다.
- repne 나왔다. rep 계열의 명령어는 문자열 컨트롤에 자주 쓰이는 어셈블리 명령어 중 하나라고 한다.
rep는 repeat의 약자라고 생각하면 쉽다. rep 계열 명령어는 주로 ecx 레지스터와 함께 자주 사용되는 편이며,
ecx > 0인 동안 어떠한 행위를 처리한다.
- repne를 사용한 것으로 보아 Zero Flag가 0이고, ecx가 0보다 클 경우 scas 명령어로 eax와 edi 값을 반복해서 비교한다. 현재 edi에는 "Hello World !" 가 들어 있는 상태인데, 이런 행위를 하는 이유가 무엇일까?
- 위 연산에 대한 ecx 결과 값은 FFFFFFF1 이라고 한다. FFFFFFFF - FFFFFFF1 을 하게 되면 0xE (10진수 14)가 되는데 바로 이 값은 "Hello World !"의 글자 수다.
- not ecx로, FFFFFFF1을 0xE로 변환하는 역할을 한다.
- 결국 위와 같은 형태의 코드 패턴이 등장한다면,
특정 번지수에 있는 문자열의 길이를 구하는 것이라고 생각해도 크게 틀리지 않을 것이다.
mov edi, X
repne scas byte ptr es:[edi]
not ecx
------------------------------------------------------------------------------------------------------------
rep : ecx > 0인 동안 명령어 반복
repz, repe : Zero Flag가 1이고, ecx > 0인 동안 명령어 반복
repnz, repne : Zero Flag가 0이고, ecx > 0인 동안 명령어 반복
- z는 zero, e는 equal이라고 생각하면 되고, z나 e나 똑같은 것이라고 생각하면 된다. n은 not이라고 생각하자.
- repxx 계열 명령어는 그냥 ecx가 양수인 동안 반복하는 행위라고 생각하면 될 것이라고 책에서 설명하고 있다.
scas 명령어도 한번 확인해보면, scas는 al / ax / eax에 저장돼 있는 값과 오른쪽 오프랜드로 지정된 (위 코드에서는 edi) 곳과 값을 비교하는 명령어다.
scasb : BYTE 단위로 비교한다. (1크기)
scasb : WORD 단위로 비교한다. (2크기)
scasb : DWORD 단위로 비교한다. (4크기)
------------------------------------------------------------------------------------------------------------
지금까지의 패턴으로 ecx에는 "Hello World !"의 글자 수가 들어 왔다. 그 사실을 알고, 아래의 코드도 같이 살펴보자.
sub edi, ecx
mov eax, ecx
mov esi, edi
mov edi, edx
- edi는 repne를 계산한 이후로 ecx에 들어간 만큼의 포인터가 더해졌다. 원래는 "Hello World !"라는 문자열의 맨 앞을 가리키고 있었는데, 이제는 0xE만큼 길이가 더해졌으므로, "Hello World !"의 맨 끝으로 포인터가 옮겨져 왔다.
- edi에 계속 "Hello World !"를 사용하고 싶으면 포인터 보정이 필요하다. 이 때, sub edi, ecx를 통해 edi가 다시 "Hello World !"를 가리킬 수 있도록 한다.
- ecx는 eax에 넣고, edi는 esi에 넣는다. 길이가 들어있는 ecx를 다시 eax에 넣는 이유는 뭘까?
ecx는 여러 어셈블리 명령어에서 이용되며, 숫자가 늘어났다 줄어들었다 한다.
따라서 원래의 카운터 값을 보존시켜둬야할 필요가 있으므로 eax에 원래의 값을 보존시켜 두는 것이다.
리버싱 할 때, 이런 식의 코드를 자주 볼 수 있을 것이라고 책은 설명하고 있다.
- edx를 edi에 다시 넣고 있는데, edx는 맨처음에 있던 코드에서 esp의 번지를 값으로 가지고 있었다.
edi에 빈 스택을 가리키는 번지를 넣은 것이라고 생각할 수 있다.
- 마지막 두 줄은 출발지인 "Hello World !"는 esi에 넣고, edi에는 '문자열을 복사할 빈 스택'을 넣는 것이라고 생각할 수 있다.