#include <stdio.h>
int test(int num){
if (num == (100%260)){
return 1;
}
return 0;
}
void win(int a, int b, int c, int d, int e){
printf("good\n");
}
void lose(){
printf("bad\n");
}
int main(){
int num;
int res;
printf("Enter Num : ");
scanf("%d", &num);
res = test(num);
if (res == 1){
win(1,2,3,4,5);
}
else if (res == 0){
lose();
}
return 0;
}
5개의 파일들
우선은 vim을 사용하여 위와 같은 내용의 소스파일을 만들고 ESC -> :wq로 저장을 해줍니다.
그리고 컴파일을 해서 저렇게 5종류의 test 파일을 만들어줍니다.
그리고 file [파일명]으로 파일을 지정해줍니다.
b main으로 Breakpoint도 만들어주면 실행 전까지의 준비 과정은 모두 끝났습니다!
run 명령어 실행 후main 함수의 전체 소스코드
run 명령어를 실행하니 어셈블리 코드가 잘린 것처럼 나와있었습니다.
이상함을 느끼고 명령어를 좀 찾아보니 전체 코드를 볼 수 있게 해주는 명령어가 있었습니다.
u [해당 함수의 주소 값] [명령어 개수]를 입력해주면 전체 코드가 나옵니다.
이제 덩어리별로 분석을 해보겠습니다.
- 첫 번째 부분
push rbp : 어셈블리에서 항상 나오는 문장입니다. 스택에서 rsp 값에 rbp 값을 넣어주고 rsp는 한 칸 올라갑니다. SFP를 스택 프레임에 넣기 위한 과정입니다. (SFP = Stack Frame Pointer)
mov rbp, rsp :
rsp의 값을 rbp에 복사합니다. 그 말인 즉슨, rbp와 rsp의 위치가 동일해진다는 뜻입니다.
sub rsp, 0x10:
rsp의 메모리 주소에 있는 값에서 0x10을 뺍니다. 여기서 0x10은 16과 같으므로 rsp의 위치는 16만큼 올라갑니다. 메모리 공간 하나당 차지하는 bit 수는 8bit이므로 총 2칸을 올라가게 됩니다.
mov rax, qword ptr fs:[0x28] :
qword ptr fs:[0x28]의 값을 rax에 복사합니다. 이 부분은 오버플로우 공격에 대한 보안을 목적으로 하는 부분이라 스택에는 영향을 미치지 않습니다.
mov qword ptr [rbp-8], rax :
rax의 값을 qword ptr [rbp-8]에 복사합니다. 여기서 rbp-8이라는건 rbp보다 8bit 위에 있는 공간을 뜻합니다. 즉, rax의 값을 rbp보다 한 칸 위에 복사한다는 뜻입니다.
xor eax, eax : eax, eax에 xor 연산을 한 뒤에 eax에 저장합니다. (xor 연산 : 두 피연산자의 비트가 다를 경우 1, 같은 경우 0) 그러므로 xor eax, eax는 mov eax, 0과 같습니다.
lea rdi, [rip+0xde1] : [rip+0xde1]의 주소를 rdi에 복사합니다. 여기서 [rip+0xde1]은 rip보다 0xde1 떨어진 곳을 말합니다.
여기서 rdi에 들어가는 주소에는 위 사진에서 보시다시피 "Enter Num : "이라는 문자열이 들어가 있습니다.
mov eax, 0 : eax에 0을 넣습니다. (eax : rax와 같은 역할, 32bit) eax의 역할은 함수의 return 값을 저장해주는 것입니다. eax에 0을 넣는다는 건 return 값을 0으로 초기화한 것입니다.
call printf@plt : printf 함수를 호출합니다.
첫 번째 부분에 있는 코드들은 간략하게 말하자면
printf를 호출하기 위한 준비 과정들을 수행하는 부분이었습니다.
- 두 번째 부분
lea rax, [rbp - 0x10] : [rbp - 0x10]의 주소를 rax에 넣습니다. 여기서 rbp - 0x10을 계산한 주소를 rax에 복사해야 합니다. 이 부분은 주소 값이기도 하고, 위치를 보고 유추를 해보자면 scanf에서 &num을 인자로 주는 부분인 것 같습니다.
mov rsi, rax :
rax의 값을 rsi에 복사합니다. rax와 rsi의 값이 같은 것을 확인할 수 있습니다.
lea rdi, [rip + 0xdd6] :
rip+0xdd6의 주소 값이 rdi에 복사됩니다.
mov eax, 0 : 다음 실행될 함수의 return 값을 0으로 초기화시킵니다.
call __isoc99_scanf@plt : scanf 함수를 호출합니다.
두 번째 부분은 scanf 함수를 호출하기 위한
준비 과정들을 수행하는 부분이었습니다.
- 세 번째 부분
mov eax, dword ptr [rbp - 0x10] : dword ptr [rbp - 0x10]의 값을 eax에 복사합니다. rbp - 0x10은 입력을 받는 변수가 위치한 주소를 뜻합니다. 이 변수의 입력값이 eax에 저장됩니다.
그렇기에 eax 레지스터의 값을 출력해보면 1이라는 값이 들어있는 걸 알 수 있습니다. (값이 1인 이유는 아까 키보드로 아무 숫자나 눌렀는데 그 숫자가 1이었기 때문...!)
mov edi, eax : eax의 값을 edi에 복사합니다. edi는 함수의 매개변수로 사용됩니다.
call test : test 함수를 호출합니다.
세 번째 부분은 num이 100인지 아닌지 판별하는 test 함수를 호출하는 부분이었습니다.
- 네 번째 부분
mov dword ptr [rbp - 0xc], eax : eax값을 dword ptr [rbp - 0xc]에 복사합니다. 여기서 eax는 return 값일 거고, rbp - 0xc는 rbp에서 12bit 떨어진 부분을 뜻합니다.
cmp dword ptr [rbp - 0xc], 1 : rbp에서 12bit만큼 떨어진 부분 (바로 위에서 참조했던 부분)에다가 1을 복사합니다. 직역하자면 위처럼 해석이 되지만, 아까 위에서 eax = 0이었으므로 eax와 1을 비교하여 Flag Register를 설정하는 부분입니다.
jne main+120 : jne라는 명령어는 비교한 값이 A!=B이면 지정된 위치로 점프를 하는 명령어입니다. 이 부분에서는 두 가지 경우로 나뉩니다. A==B, A!=B, 이렇게 두 가지 경우로 말이죠. 전자의 경우에는 별 다른 반응 없이 다음 명령어를 실행하고 후자의 경우에는 main+120의 위치로 이동합니다.
네 번째 부분은 test 함수의 return 값(eax)을 활용하여 비교 연산을 하고, eax가 1이냐 아니냐에 따라 main+120의 위치로 이동할 것인지를 따지는 부분이었습니다.
- 다섯 번째 부분
mov r8d, 5: r8d에 5를 넣는다. r8d는 함수의 매개변수 중 하나로, 다섯 번째 인자의 값을 저장하는 매개변수입니다.
mov ecx, 4: ecx는 네 번째 인자의 값을 저장하는 매개변수이고, ecx에 4를 넣습니다.
mov edx, 3 : edx는 세 번째 인자의 값을 저장하는 매개변수이고, edx에 3을 넣습니다.
mov esi, 2 : esi는 두 번째 인자의 값을 저장하는 매개변수이고, esi에 2를 넣습니다.
mov edi, 1 : edi는 첫 번째 인자의 값을 저장하는 매개변수이고, edi에 1을 넣습니다.
call win : win 함수를 호출합니다.
다섯 번째 부분은 win 함수를 호출하기 위해 5개의 매개변수에 값을 넣어주고, win 함수를 호출합니다.
- 여섯 번째 부분
jmp main+136 : main+136의 위치로 무조건 점프합니다.
cmp dword ptr [rbp - 0xc], 0 : rbp - 0xc의 값이 0인지 1인지 판별합니다. (rbp - 0xc의 안에는 test 함수의 return값이 들어있습니다)
jne main+136 : 위 명령어의 반환 값이 1이면 실행합니다. main+136의 위치로 점프합니다.
mov eax, 0 : eax를 0으로 초기화해줍니다. eax에는 lose의 return값이 들어갈 예정입니다.
call lose : lose 함수를 실행합니다.
여섯 번째 부분은 test의 return값에 따라 main+136으로 점프할지, lose 함수를 호출할지 정하는 부분이었습니다.
- 마지막, 일곱 번째 부분
mov eax, 0 : main 함수의 return값을 0으로 설정합니다.
mov rdx, qword ptr [rbp - 8] : rbp - 8의 위치에 있는 값을 rdx에 복사합니다. 스택 오버플로우가 일어났는지 검증하기 위해 들어간 구문 같습니다.
xor rdx, qword ptr fs:[0x28] : 메모리와 코드를 보호하기 위한 부분입니다.
je main+161 : main + 161로 점프합니다.
call __stack_chk_fail@plt : 스택 오버플로우가 났을 때 실행되는 함수인 것 같습니다.
leave : mov rsp, rbp와 pop rbp를 수행합니다. 즉, 현재까지 썼던 메모리 스택을 깔끔히 비우고, 자신을 호출했던 메모리의 베이스 주소를 ebp에 다시 채우는 과정입니다.
ret : 함수가 끝나면 스택에 저장되어있던 eip 주소를 원래대로 되돌려 놓습니다.
일곱 번째 부분은 코드 실행을 끝마친 후에 스택과 메모리들을 원래대로 돌려놓거나 깔끔하게 마무리를 하며 동시에 보안도 유지하는 부분입니다.