-
[시스템해킹 #2] 어셈블리어, 함수 프롤로그/에필로그, gdb, setUIDEVI$ION/SYSTEM HACKING 2019. 5. 5. 21:51
저번 시간에 했던 어셈블리어를 복습 겸 다시 되짚어보았다.
[어셈블리어]
CALL [접근할 함수 주소]
PUSH EIP // 현재 EIP가 가리키는 값을 스택에 저장한 뒤
JMP [접근할 함수 주소] // [접근할 함수 주소]로 점프한다.
PUSH [인자]
스택 영역에 4바이트 공간을 할당하고, [인자] 값을 스택에 저장한다.
CALL 전에 PUSH가 나오는 경우, PUSH는 인자를 전달하는 역할을 한다.
POP [인자]
ESP가 가리키는 곳의 값을 [인자]에 저장한 후, ESP가 가리키는 곳이 4바이트 올라간다.
RET
POP EIP // ESP가 가리키는 값을 EIP에 저장하고 ESP가 4바이트 올라간다.
JMP EIP // 방금 저장한 EIP 값으로 점프한다.
SUB [인자1], [인자2]
인자1 = 인자1 - 인자2
ESP가 인자1로 올 경우, 스택 공간을 확보한다는 뜻이다.
ADD [인자1], [인자2]
인자1 = 인자1 + 인자2
ESP가 인자1로 올 경우, 사용했던 스택 공간을 정리한다는 뜻이다.
LEAVE
MOV ESP, EBP // ESP가 EBP 값으로 이동하고
POP EBP // 현재 ESP가 가리키는 값을 EBP에 저장한다.
→ 최종적으로 ESP는 EBP보다 4바이트 위로 올라가게 된다.
[함수 프롤로그 & 에필로그]
함수 프롤로그
메인 함수의 첫 부분은 아래 두 문장으로 시작된다.
PUSH EBP
MOV EBP, ESP
먼저 PUSH EBP로 이전 EBP 레지스터의 값을 SFP 형태로 저장하고, ESP는 4바이트 아래로 이동한다.
그 다음 MOV EBP, ESP를 통해 방금 움직인 ESP 레지스터 주소를 EBP로 설정함으로써 새로운 EBP가 생성된다.
이 과정을 함수 프롤로그라 한다.
함수 프롤로그 과정 뒤에 나오는
SUB ESP, 0x38
SUB ESP, 0x4
는 스택 공간을 각각 56바이트, 4바이트씩 확보하는 과정이다.
이 다음에는
LEA EAX, [EBP-56]
PUSH EAX
CALL 0x8048360 <fgets>
이라는 명령행이 나온다.
LEA [인자1], [인자2]는 원래 [인자2]의 주소값을 [인자1]에 넣는다는 뜻인데, MOV와 비슷하게 [인자2]의 값을 [인자1]에 넣을 수도 있다. 다만 특정 메모리나 레지스터의 값을 옮기기만 하는 MOV와는 달리, LEA는 옮겨지는 값에 연산을 할 수 있다. 즉, LEA는 실행 중에 계산된 값을 넣을 수 있다.
예를 들어, LEA EAX, [EBP+5]이라 하면 EBP의 값에 5을 더한 결과값을 EAX에 저장한다는 뜻이다. 만약 EBP의 값이 100이었다면, 100에 5를 더한 105를 EAX에 넣는다. 이를 MOV로 나타내려면 ADD EBP, 5, MOV EAX, EBP 이렇게 두 개의 명령행으로 표현해야 한다. 만약 MOV EAX, [EBP+5]로 표현한다면, EBP+5의 위치에 있는 값을 EAX에 넣는다는 뜻이다. 만약 EBP의 값이 100이라면 105번지에 있는 값을 EAX에 넣는다.
다시 돌아와서, <main+17>의 LEA EAX, [EBP-56]이라는 명령행은 EBP에서 56바이트를 뺀 값을 EAX에 넣는다는 것으로 해석할 수 있다. (아마 버퍼의 크기가 56바이트인 것으로 추정된다.) 그 다음 EAX의 값을 스택에 PUSH하고 fgets 함수를 호출한다. 여기서 EAX의 값은 fgets 함수의 인자가 된다.
함수 에필로그
<main+72>의 ADD ESP, 0x10 명령행에서는 사용했던 스택 공간을 정리한다. 그러나 이것만으로는 사용했던 스택 공간이 완전히 정리되지 않으므로 아래와 같은 과정을 거친다.
/* LEAVE */
MOV ESP, EBP
POP EBP
/* RET*/
POP EIP
JMP EIP
먼저 MOV ESP, EBP를 통해 EBP가 가리키는 값을 ESP에 저장함으로써 ESP를 EBP쪽으로 이동시킨다. 그 다음 POP EBP로 현재 ESP가 가리키는 값(SFP, 이전 함수의 EBP)을 EBP로 설정함으로써 현재 EBP가 이전 함수의 EBP로 복구된다. 여기가지가 LEAVE의 내용이다.
LEAVE의 과정이 끝나면 POP EIP를 통해 현재 ESP가 가리키는 값(리턴 어드레스)을 EIP로 설정한다. 그 다음 JMP EIP를 통해 현재 EIP에 들어있는 값으로 EIP를 이동시킨다. 여기까지가 RET의 내용이다.
위의 과정을 통틀어서 함수 에필로그라 한다.
[실습]
문제 1
4바이트 정수형 변수의 값을 변조하여 1234를 5678로 바꾸어라. (파일 이름: ./test)
1234를 5678로 바꾸려면 20바이트 크기의 버퍼를 아무 값으로 채운 다음 바로 5678을 삽입하면 된다.
1234와 5678을 16진수로 바꾸면 각각 0x4D2, 0x162E인데, Intel CPU에서는 Little Endian을 사용하기 때문에 4바이트 정수 값은 반대 방향으로 저장된다. 그러므로 5678의 16진수 값인 0x162E를 그대로 쓰면 안되고 2E 16으로 바꿔줘야 한다.
따라서 총 입력해야 할 값은 "A"*20 + "\x2E\x16"이다. 그러나 shell에서 이를 그대로 입력하면 모두 문자로 처리되기 때문에, perl이나 python의 도움을 받아 명령어로 처리해야 한다.
perl -e 'print "A"*20 + "\x2E\x16"' 또는
python -c 'print "A"*20 + "\x2E\x16"'
하지만 이 두 문장 자체는 명령어이므로, 이 shell 명령어들의 출력 결과를 다시 shell에 되돌려주기 위해서는 `(백쿼터, 백틱)이라는 특수 문자를 사용해야 한다.
따라서 답은
./test `perl -e 'print "A"*20 + "\x2E\x16"'` 또는
./test `python -c 'print "A"*20 + "\x2E\x16"'`
이다.
문제 2
4바이트 리턴 어드레스 값을 변조해 0x12345678로 바꾸어라. (파일 이름: ./test)
문제 1과 같은 방식으로 하면 된다. 20바이트 크기의 버퍼에 아무 값이나 채워준 다음 0x12345678 주소값을 Little Endian 방식으로 바꿔서 입력해주면 된다.
./test `python -c 'print "A"*20 + "\x78\x56\x34\x12"'`
참고)
버퍼 오버플로우 공격을 시도할 때 Segmentation fault 에러 문구가 출력되는 경우가 있다. Segmentation fault는 return address가 뭔가 엉뚱한 주소로 바뀌었거나, 메모리 주소 접근에 실패했을 때, 할당되지 않는 메모리 주소일 때, 접근권한이 충분하지 않을 때에 출력된다. 이 에러 문구가 뜬다면 공격이 가능한 상태라고 생각하면 된다.
[gdb]
C언어로 컴파일된 파일을 디버깅해주는 툴로, 어셈블리어로 바꿔주는 툴이다. 메모리를 볼 수 있고, 코어 파일을 덤프하는 등 많은 기능이 있다.
옵션
-q : gdb 실행 시에 나오는 부연 설명들을 생략해준다.
set disassembly-flavor intel : 어셈블리어를 Intel 환경으로 바꿔준다. Intel 문법이냐 AT&T 문법이냐에 따라 어셈블리어를 해석하는 방법이 완전히 달라지기 때문에 꼭 바꿔주어야 한다.
b : 브레이크 포인트
r : 실행. 브레이크 포인트가 걸려있을 경우 그 부분까지 실행된다. r 뒤에 인자가 올 수도 있다. (인자 구분은 공백으로 한다.)
n : 브레이크 포인트 이후 실행.
i : info 정보를 보여준다. 레지스터의 경우 프로그램이 실행되어야만 보인다.
x/[보고싶은 바이트 수][옵션] [주소] : 레지스터의 값 확인
(옵션) s : 문자열, o : 8진법, u : 부호 없는 10진법, t : 2진법, x : 16진법
ex) x/4x $ebp-16 : $ebp-16부터 4바이트의 주소값을 보여준다.
x/3x $ebp-16 : $ebp-16부터 3바이트의 주소값을 보여준다.
(※ gdb에서 디버깅 시 주소값은 Big Endian 방식으로 출력된다.)
gdb -c [코어 파일] : 떨궈진 코어 파일 덤프
(코어 파일: Segmentation fault와 같이 프로그램의 비정상적인 종료가 발생한 경우, 커널에서 해당 프로세스와 관련된 메모리를 덤프시킨 파일이다. 보통 core라는 이름으로 생성된다.)
[SetUID]
사용자 A의 권한으로 지정된 명령어는 원칙적으로 다른 사용자가 사용할 수 없지만, 가끔 필요할 때가 있다. 그래서 A는 하나의 프로세스를 생성하여 SetUID를 설정함으로써, 다른 사용자도 이 프로세스를 통해 A의 권한으로 된 명령어의 일부를 실행할 수 있게 해준다.
그러나 SetUID에는 취약점이 존재한다. 악의적인 사용자가 해당 프로세스를 통해 A가 원하지 않았던 A 권한의 다른 명령어를 실행할 수 있다.
/usr/bin/passwd 파일은 root 권한의 SetUID 비트가 설정된 대표적인 파일이다. passwd 파일은 계정의 비밀번호를 변경할 수 있는 실행 파일로, 소유자는 root이지만 SetUID가 설정되어 있어 일반 사용자들도 비밀번호를 변경할 수 있다.
SetUID를 적용하려면 기존의 허가권 앞에 4를 붙이면 된다.
ex) #chmod 4644 파일이름
SetUID가 걸린 파일은 ls -l 명령어를 통해 확인할 수 있다.
x자리에 있는 s가 SetUID를 의미한다. (s가 소문자이면 실행 권한이 있다는 뜻이고, 대문자(S)이면 실행 권한이 없다는 뜻이다.) 즉, gremlin 파일이 실행되는 동안에는 gremlin 권한의 명령어를 모두 사용할 수 있다는 뜻이다.
위 소스코드에서 setreuid(3093, 3093)의 의미는, 해당 파일이 실행되는 동안에는 UID와 GID가 3093인 명령어를 모두 사용할 수 있다는 뜻이다.
[과제]
strcpy가 끝난 후 4바이트 리턴 어드레스 값을 변조해서 0x12345678로 바꾸어라. (파일 이름: ./test)
<main+47>의 lea %eax, [%ebp-256]에서 버퍼의 크기가 256 바이트임을 알 수 있고, eax에는 ebp에서 256을 뺀 값이 저장된다. 그 다음 eax값을 스택에 push하므로 esp는 ebp로부터 (256+4) 바이트 떨어진 곳에 위치해있다. 그 다음에는 printf 함수가 호출되므로, strcpy가 끝난 뒤의 리턴 어드레스를 변조하려면 총 260바이트의 아무 값을 넣은 뒤에 원하는 주소값을 삽입해야 한다.
답: ./test `python -c 'print "A"*260 + "\x78\x56\x34\x12"'`
[참고 문헌 및 출처]
https://vencedor.tistory.com/27
'EVI$ION > SYSTEM HACKING' 카테고리의 다른 글
[pwnable] PLT와 GOT (0) 2019.07.19 [시스템해킹 #5] RET SLED, FEBP (0) 2019.06.24 [시스템해킹 #4] NOP Sledding, strcpy와 gets, 링크, RTL (0) 2019.05.13 [시스템해킹 #3] 메모리 보호 기법, 쉘코드, 환경변수 (0) 2019.05.06 [시스템해킹 #1] 함수의 호출/복귀 원리, 메모리 구조, 스택 프레임, 함수 프롤로그/에필로그, 지역변수/전역변수 할당, 어셈블리어 기 (0) 2019.05.05