2021. 5. 26. 05:18ㆍLayer7/Reverse Engineering
- 사전 준비
#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;
}
우선은 vim을 사용하여 위와 같은 내용의 소스파일을 만들고 ESC -> :wq로 저장을 해줍니다.
그리고 컴파일을 해서 저렇게 5종류의 test 파일을 만들어줍니다.
그리고 file [파일명]으로 파일을 지정해줍니다.
b main으로 Breakpoint도 만들어주면 실행 전까지의 준비 과정은 모두 끝났습니다!
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 주소를 원래대로 되돌려 놓습니다.
일곱 번째 부분은 코드 실행을 끝마친 후에 스택과 메모리들을 원래대로 돌려놓거나
깔끔하게 마무리를 하며 동시에 보안도 유지하는 부분입니다.
'Layer7 > Reverse Engineering' 카테고리의 다른 글
ELF 파일 구조 (0) | 2021.06.02 |
---|---|
리버싱 실전 | 워게임 풀이 - Ubuntu Linux (0) | 2021.05.31 |
X64 레지스터에 대하여 (0) | 2021.05.21 |
pwndbg 명령어 정리 - Ubuntu Linux (0) | 2021.05.17 |
실행파일이 만들어지는 과정 - Ubuntu Linux (0) | 2021.05.16 |