지옥방 3주차 과제 달고나 문서 뿌시기!
커널(Kernel) 시스템이 운영에 필요한 기본적인 명령어 집합을 찾는 곳. 기본적으로 커널은 64KByte 영역에 자리잡지만 오늘날에는 더 큰 영역을 사용.
하나의 프로세스(프로그램)을 실행시키면 이 프로세스를 segment라는 단위로 묶어서 가용 메모리 영역에 저장시킨다.(실행 시점에 실제 메모리의 어느 위치에 저장될 지가 결정된다 => 그래서 시작 위치로부터의 위치인 logical address를 사용한당)
오늘날의 시스템은 멀티 테스킹이 가능해서 메모리에 여러 개의 프로세스가 저장되어 병렬적으로 작업 수행.
segment code segment, data segment, stack segment로 이루어져있다.
code segment 시스템이 알아 들을 수 있는 명령어(instruction)들이 들어있다. instruction들은 분기 과정과 점프, 시스템 호출 등을 수행하기 위하여 특정 위치에 있는 명령을 지정해주어야한다. 하지만 위에서 말했다싶이 segment는 자신이 현재 메모리 상에 어디 위치하게 될지 모르기 때문에 정확한 주소를 지정할 수 없다. ->> logical address를 사용함. logical address는 실제 메로리 상의 주소(physical address)와 매핑되어 있다. 컴파일이 다 끝난 후! segment는 segment는 segment selector에 의해서 자신의 시작 위치(offset)을 찾고, 그 오프셋 기준 logical address에 있는 명령을 지정 가능. physical address = offset + logical address
위의 그림에서 code segment 내에 들어있는 instruction IS 1을 가리키는 logical address는 0x00000100이다. segment가 실제로 위치하고 있는 메모리상의 주소를 위의 그림과 같이 0x80010000일때, 이 instruction IS 1의 실제 메모리 상의 주소는 0x80010000 (segment offset)과 0x00000100(logical address)을 더한 0x80010100이 된다.
위의 방법대로 하면 segment가 메모리상의 어느 위치에 있더라도 segment selector가 segment의 offset을 알아내서! 해당 instruction의 정확한 주소값을 찾아낼 수 있게 된다.
data segment 프로그램 실행시에 사용되는 데이터(전역 변수)가 들어가는 곳.
stack segment 현재 수행되고 있는 handler, task, program이 저장하는 데이터 영역. 우리가 사용하는 버퍼, 지역변수들이 저장되는 곳!
stack pointer(SP)라고 하는 레지스터가 스텍의 맨 꼭대기를 가리키고 있다. PUSH와 POP instruction에 의해 데이터를 저장하고 읽어들인다.
PUSH를 하게 되면 그 데이터는 스텍의 가장 맨 위에 위치하게 된다. 그 후로 PUSH 되는 데이터들은 그 위로 차례차례 쌓이게 된다.
POP을 하면 맨 위의 데이터, 즉 가장 최근에 PUSH된 데이터를 가져오게 된다.
레지스터 CPU가 재빨리 읽고 쓰기를 해야 하는 데이터들이 사용하는 CPU 내부에 존재하는 메모리. 레지스터는 다시 목적에 따라서 4개로 나뉜다.
- 범용 레지스터 논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는데 사용되는 피연산자, 그리고 메모리 포인터가 저장되는 레지스터.
- 세그먼트 레지스터 위에서 배웠던 code segment, data segment, stack segment를 가리키는 주소가 들어있는 레지스터
- 플래그 레지스터 프로그램의 현재 상태나 조건 등을 검사하는데 사용되는 플래그들이 있는 레지스터
- 인스트럭션 포인터 다음 수행해야 하는 명령(instruction)이 있는 메모리 상의 주소가 들어있는 레지스터. (EIP였나 !?)
범용 레지스터 프로그래머가 임의로 조작 가능한 레지스터. 전에 사용하던 AX,BX,CX,DX는 16bit의 변수, 현재 사용하는 앞에 E가 붙은 EAX,EBX,ECX,EDX는 32bit의 변수라고 생각하면 된다.(이건 문서 작성시기 기준! 지금은 RAX,RBX,RCX,RDX가 출몰했다..)
필요에 따라 사용 가능. BUT! 자신들의 목적을 가지고 태어났다!
- EAX 피연사자와 연산 결과의 저장소 ( MUL 이나 DIV의 경우 EAX에 연산 결과가 저장된다.밑에서 더 자세하게!)
- EBX DS segment(검색 결과 data segment로 추정)안의 데이터를 가리키는 포인터.
- ECX 문자열 처리나 루프를 위한 카운터. Counter의 C로 추정
- EDX I/O 포인터 ( input/output 포인터)
- ESI DS 레지스터가 가리키는 data segment 내의 어느 데이터를 가리키고 있는 포인터. 문자열 처리에서 source를 가리킴.
- EDI ES 레지스터가 가리키고 있는 data segment 내의 어느 데이터를 가리키고 있는 포인터. 문자열 처리에서 destination을 가리킴
- ESP SS 레지스터가 가리키는 stack segment의 맨 꼭대기를 가리키는 포인터
- EBP SS 레지스터가 가리키는 스텍 상의 한 데이터를 가리키는 포인터
세그먼트 레지스터 프로세스(프로그램)의 특정 세그먼트를 가리키는 포인터 역할
CS Code Segment를 가리키는 포인터 역할을 한다.
DS,ES,FS,GS =>> 싹 다!!! Data Segment (많이 필요한가보다,, 혼자 4개씩이나)
SS Stack Segment를 가리키는 포인터 역할을 한다.
플래그 레지스터 플래그는 정말로 그 깃발을 의미! 깃발을 들거나 내리는거 2가지로 구분 가능하듯이, 플래그 레지스터도 0과 1로 구분 가능
대부분 조건문에 사용( 아직 사용법에 대한 감은 오지 않는다..)
Status flags
- CF carry flag의 약자! 덧셈 연산시 bit bound를 넘어가거나 뺄셈을 할때 빌려오는 경우를 Carry와 Borrow라고 하고 이들이 발생하면 1이 된다.
- PF Parity flag의 약자! 연산 결과 최하위 바이트의 값이 1이 짝수개일 경우에 1이 된다.
- AF Adjust flag의 약자! 연산 결과 위에서 언급되었던 carry와 borrow가 3bit 이상 발생할 경우 1이 된다.
- ZF Zero flag의 약자! 결과가 zero임을 나타냄. If문과 같은 조건문이 만족될 경우 set 된다.
- SF Sign flag의 약자! 연산 결과 최상위 비트의 값과 같다. Signed(양수 음수 존재) 변수의 경우 양수이면 0, 음수이면 1
- OF Overflow flag의 약자! 오버 플로우!! 배운거다. 넘치는거지! 정수형 결과값의 크기 문제로 피연산자의 데이터 타입에 모두 들어가지 않을 경우 1이 된다.
- DF Direction flag의 약자! 문자열 처리(high address에서 low address로)에 있어서 1일 경우 문자열 처리 instruction이 자동으로 감소, 0일 경우 자동으로 증가
System flags
- IF Interrupt enable flag. 프로세서에게 mask한 interrupt에 응답할 수 있게 하려면 1을 준다.
- TF Trap flag, 디버깅을 할때 single-step을 가능하게 하려면 1을 준다.
- IOPL I/O Privilege level field. 현재 수행중인 프로세스 혹은 task의 권한 레벨을 가리킨다.
- NT Nested task flag. Interrupt의 chain을 제어한다. 1이 되면 이전 실행 task와 현재 task가 연결되어 있음을 의미
- RF Resume flag. 예외 디버그를 하기 위해 프로세서의 응답 제어
- VM Virtual-8086 mode flag. Virtual-8086 모드를 사용하려면 1을 준다
- AC Alignment check flag. 이 비트와 CR0 레지스터의 AM 비트가 set되어 있으면 메모리 레퍼런스의 alignment checking이 가능하다
- VIF Virtual interrupting flag. IF flag의 가상 이미지. 밑의 VIP flag와 결합시켜 사용
- VIP Virtual interrupt pending flag. 인터럽트가 pending(경쟁 상태) 되었음을 가리킨다
- ID Identification flag. CPUID instruction을 지원하는 CPU인지를 나타낸다
아쉽게도.. 시스템 플래그 쪽은 이해를 못했다.. 쓰면서 하면 외워질까 했지만, 이해가 안된 상태에서 외워봤자인거 같아서,,
많이 읽어서 이해 시키겠다!!
Instruction Pointer
Instruction Pointer 레지스터는 다음 실행할 명령어가 있는 현재 code segment의 offset값(위의 code segment 부분에서 physical address를 구할때 사용하던 기준 주소값)을 가진당. ex) EIP
다음 실행할 명령을 가리키고 있는 포인터인 instruction pointer(EIP)가 main() 함수의 시작점을 가리키고 있다.
스택의 가장 위를 가리키고 있는 포인터인 stack pointer(ESP)는 프로그램이 수행되면서 PUSH와 POP 명령을 하게 되는데 PUSH와 POP을 할 지점을 가리키는 역할을 한다!
함수의 프롤로그 push ebp는 이전에 수행하던 함수의 데이터를 보존하기 위해 사용, 이를 Base Pointer라고 부른다. 그리고 mov ebp, esp를 수행함으로써 함수의 base pointer와 stack pointer가 같은 지점을 가리키게 한다.
아니아니 근데요 이 해설은 달고나 문서 보면 써있는거고,, 쉽게!? 이해 했으니 넘어가고!
낯선 코드와 제가 이해하는데 엄청난 시간이 걸린 그것을!! 해설하겠습니다
<낯설었던 코드>
and $0xfffffff0, %esp ESP와 11111111 11111111 11111111 11110000과 AND연산을 하여 ESP의 주소값의 맨 뒤 4bit를 0으로 만들기 위함이라고 한당.
<이제 이해한거!!!!>
달고나 문서 p27, step 8에서 function까지 모두 마치고 add $0x10, %esp의 결과 stack pointer가 0X804830, 즉 sub esp, 0x4가 실행되기 전의 위치로 돌아간다는 이걸!!!! 진짜일까?라는 의문 하나때문에!! 꽤 고민을 많이 했다.
그래서 나는 esp의 값이 진짜로 돌아오는지 궁금했고, 이를 확인하기 위해 sub esp,0x4 이전의 esp 주소값을 0이라고 치고 위의 명령어가 실행되면 정말 esp 값이 0으로 돌아오나 확인해봤다.
1. sub esp, 0x4 esp 값에서 4를 뺌으로써 스텍을 4만큼 확장했다.
2. push $0x3, push $0x1, push $0x2 push 한 번당 4 바이트를 먹는다고 한다! 그럼 지금까 지 16만큼 확장되었다.
3.call function 함수를 호출했다. 찾아보니 call 함수는 함수가 끝난 후 원래의 함수로 돌아오기 위해 return address를 push 한다고 한다. 이로써! 지금까지 20만큼 확장되었다.
4.push ebp 함수의 프롤로그 과정이다. base pointer를 push 한다. 24만큼 확장되었다.
5. sub esp, 0x28 0x28은 10진수로 16^1 *2 + 16 ^ 0 * 8 이므로 40이다. 40만큼 확장! 토탈 64다
6.leave leave는 함수 초반에 진행했던 프롤로그 과정을 복구하고 아까 call함수가 push 했던 return address를 pop해서 돌아간다. 아 그리고 leave하는 함수 내에서 확장되었던 스택도 사라진다. 그럼 여기서! 40+4+4=48만큼 축소된다. 토탈 16
7.add esp, 0x10 ret으로 돌아온 메인함수에서 esp 에 10진수로 16을 더한다. 이는 스택 축소를 의미하므로 위의 토탈에서 16만큼 축소되면 0... 크으으으으으으으으으으으으으
잘했져 잘했져 완전 잘했져 이게 별거 아닌걸로 보이시겠지만 저는 이거 1시간 고민했어요오오오오오
Buffer overflow의 이해
위에서 다루었던 simple.c에서는 function에서 확장했던 40만큼의 스텍에 값을 쓰거나 하지 않았다. 하지만 이 버퍼에 데이터를 쓴다고 생각해보자!
strcpy(buffer2, recive_from_client);
위의 코드는 사용자로부터 수신한 데이터를 buffer2와 buffer1에 복사한다. strncpy() 함수는 몇 바이트를 저장할지 지정해주지만 strcpy()는 그와 달리 사용자로부터 수신한 데이터에서 NULL(\0)을 만날 때까지 복사를 진행한다.
데이터를 넣을때 할당된 40바이트 그 이상으로 넣게 되면 위의 그림과 같이 확장된 stack (40 byte)를 넘어 main() 함수의 base pointer는 물론이고 return address, 쉘 코드까지 공격자 마음대로 설정할 수 있게 된다.이를 버퍼 오퍼플로우라고 한당.
Byte order
위에서 스텍을 가로로 봤을때의 데이터를 실제로 버퍼에 복사한 후의 모습이 바로 위 그림이다. 두 그림을 비교해봤을대 데이터와 순서에 있어서 약간의 차이가 있음을 알 수 있다. 그 이유는 바이트 정렬 방식 때문이다
big endian 바이트 순서가 낮은 메모리 주소에서 높은 메모리 주소로 된다.(IBM 370, RISC 기반의 컴퓨터, 모토로라의 마이크로프로세서 해당)
little endian 바이트 순서가 높은 메모리 주소에서 낮은 메모리 주소로 된다.(위에서 언급한 big endian 방식 사용 기기 외의 일반적인 IBM 호환 시스템, 알파 칩의 시스템 해당)
빅 엔디언은 사람이 숫자를 쓰는 방법과 같이 큰 단위의 바이트가 앞에 오는 방식
리틀 엔디언은 그 반대로 작은 단위의 바이트가 앞에 오는 방식
이를 보면 단순히 12345678이 87654321과 같이 거꾸로 저장되는게 아니라, 해당 데이터를 데이터 단위로 나누었을때 그 단위가 거꾸로 배열되는 형태임을 알 수 있다.
공격 코드의 바이트를 정렬할 때에는 byte order를 고려해서 넣어줄 것!
function이 모두 끝나고 ret instruction을 만났을때 return address가 있는 위치의 값을 EIP에 넣고 그 EIP가 가리키는 곳의 명령을 수행할 것이다. 위에서도 봤듯이 공격자는 쉘 코드(셸코드 (shellcode)란 작은 크기의 코드로 소프트웨어 취약점 이용을 위한 내용부에 사용된다, 공격코드를 쉘 코드로 보면 될듯 싶다!)를 return address 위에 있는 스택에 넣어놨다. 이를 실행시키려면 EIP에 쉘 코드의 시작 주소를 넣어야할 것이다.( 정확한 주소를 알아내는 방법은 다음에 알려주신대요,,)
EIP에 쉘 코드의 시작 주소값이 들어가고 ret 함과 동시에!? 쉘 코드가 수행되면서 execve("/bin/sh",...)를 수행하게 된다. 이것이 바로바로 buffer overflow를 이용한 공.격.방.법.
이전에 function 함수가 위치했던 스택에 쉘코드를 넣어서 더 넓은! 공간을 활용하는 방법이다!
위에서 언급했듯이 정확한 주소값을 알아내는 것은 매우 어렵기 때문에 아까와 같이 return address는 바로 위 mov %eax, $0x2C 를 가리키고 return address 위에서 간접적으로 40 바이트의 시작주소를 지정하여 그 곳으로 이동하도록 한다!
달고나 33쪽까지 플래그쪽 살짝 빼고 뿌시기 완료
댓글
이 글 공유하기
다른 글
-
지옥방 3주차 과제 라젠카 뿌시기(NX Bit)
지옥방 3주차 과제 라젠카 뿌시기(NX Bit)
2020.04.24 -
지옥방 3주차 과제 라젠카 뿌시기(Return to Shellcode)
지옥방 3주차 과제 라젠카 뿌시기(Return to Shellcode)
2020.04.24 -
지옥방 3주차 과제 DreamHack-System Exploitation Fundamental
지옥방 3주차 과제 DreamHack-System Exploitation Fundamental
2020.04.21 -
지옥방 스터디 포너블
지옥방 스터디 포너블
2020.04.09