포너블 스터디 2주차(달고나 문서 요약)
시스템은 커널을 메모리에 적재시키고 가용 메모리 영역(Available Space)을 확인한다.
기본적으로 커널은 64 KByte 영역에 자리잡지만 32bit 64bit를 이용하는 지금은 더 큰 영역을 사용한다.
하나의 프로세스(프로그램)을 실행시키면 위 그림과 같이 이 프로세스를 segment라는 단위로 묶어서 가용 메모리 영역에 저장시킨다.
하나의 segment는 최대 2^32 byte의 크기를 가질 수 있다.
하나의 segment는 code segment, data segment, stack segment로 이루어져있다.
code segment *시스템이 알아들을 수 있는 명령어, 즉 instruction들이 들어있다. 이 instruction들은 명령을 수행하면서 많은 분기 과정과 점프, 시스템 호출 등을 수행하는데, 분기와 점프의 경우 메모리 상의 특정 위치에 있는 명령을 지정해주어야 한다. 하지만 자신이 어느 위치에 저장될지 컴파일 과정에서는 알 수 없기에 *logical address를 사용한다. 이는 실제 메모리 상의 주소와 매핑되어 있어서 segment는 segment selector에 의해서 자신의 시작 위치를 찾을 수 있고, 이를 기준으로 하는 logical address에 있는 명령을 수행할지를 결정하게 된다. ==> 실제 메모리 주소(physical address) = offset(segment의 시작 위치) + logical address
위의 그림에서 segment가 실제로 위치하고 있는 메모리상의 주소를 0x80010000이라고 가정하자. code segment 내에 들어있는 instruction IS 1을 가리키는 주소는 0x00000100인데 이는 logical address다. 이를 불러올때는 offset+logical address값을 주소값으로 하니 segment가 실제로 위치하고 있는 주소인 0x80010000 + 0x00000100 이므로 0x80010100이 된다.
위의 방법으로 segment가 메모리상의 어디에 위치하게 돼도 필요한 instruction의 정확한 위치를 찾아낼 수 있게 된다.
*data segment * 프로그램이 실행 시에 사용되는 데이터가 들어간다. 이는 전역 변수를 가리킨다. 프로그램 내에서 전역 변수를 선언하면 그 데이터는 data segment에 자리 잡는다.
data segment는 다시 4개의 data segment로 나뉘게 되는데, 각각 현재 모듈의 data structure, 상위 레벨로부터 받아들이는 데이터 모듈, 동적 생성 데이터, 다른 프로그램과 공유하는 공유 데이터 부분이다.
*stack segment * 현재 수행되고 있는 handler, task, program이 저장하는 데이터 영역. 우리가 사용하는 버퍼가 바로 이 stack segment에 자리 잡는다. 또한 프로그램이 사용하는 multiple stack을 생성할 수 있고 각 스텍들간의 switch가 가능하대요,,,뭔 소리인지는 아직 잘 ㅎㅎ. 전역 변수들이 자리잡는 data segment와 달리 stack segment에는 지역 변수들이 자리를 잡게 된다!
스택은 처음 생성될 때 지정한 크기만큼 만들어지고 프로세스의 명령에 의해 데이터를 저장해 나가는 과정을 거친다. stack pointer(SP)라고 하는 레지스터가 스택의 맨 꼭대기를 가리키고 있다.
*PUSH instruction, POP instruction * 스택에 데이터를 저장하고 읽어 들이는 과정을 수행하는 친구들
PUSH는 맨 위에 접시를 올리는 느낌. POP은 맨 위 접시를 빼는 느낌.
*레지스터(register) * 데이터를 적절하게 집어내고 읽고 저장하기 위한 공간, CPU가 재빨리 읽고 쓰기를 해야 하는 데이터들이므로 CPU 내부의 존재하는 메모리를 사용하는데 바로 그것!
레지스터는 목적에 따라서 범용 레지스터(General-Purpose register), 세그먼트 레지스터(segment register), 플래그 레지스터(Program status and control register), 인스트럭션 포인터(instruction pointer)로 구성된다.
범용 레지스터(General-Purpose register)는 논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는 데 사용되는 피연산자, 그리고 메모리 포인터가 저장되는 레지스터
세그먼트 레지스터(segment register)는 code segment, data segment, stack segment를 가리키는 주소가 들어있는 뤠지스퉐.
플래그 레지스터(Program status and control register)는? 프로그램의 현재 상태나 조건 등을 검사하는 데 사용되는 플래그들이 있는 레지스터
인스트럭션 포인터는 다음 수행해야 하는 명령(instruction)이 있는 메모리 상의 주소가 들어가 있는 레지스터
범용 레지스터
프로그래머가 임의로 조작할 수 있게 허용되어 있는 레지스터. 4개의 32bit 변수라고 생각하면 된다.
아무렇게나 사용해도 되지만 주어진 목적대로 사용해 주는 것이 조다.
세그먼트 레지스터
프로세스 내부의 특정 세그먼트를 가리키는 포인터 역할을 한다.
CS | code segment |
DS | data segment |
ES | |
FS | |
GS | |
SS | stack segment |
위의 표처럼 세그먼트 레지스터가 가리키는 위치를 바탕으로 우리가 원하는 segment안의 특정 데이터, 명령어들 정확학 끄집어낼 수 있다.
플래그 레지스터
컨트롤 플래그 레지스터는 상태 플래그, 컨트롤 플래그, 시스템 플래그들의 집합. 초기화되면 이 레지스터는 0x00000002의 값을 가진다.
1,3,5,15,22~31번 비트는 우리가 사용 불가.
과제 자료를 보면 각 플래그들의 역할을 간단히 살펴보자라고 하는데,, 어디가 간단하다는 건지 모르겠다.
이게 뭔소리일까요오~ 모르겠네요오~
Instruction Pointer
Instruction pointer 레지스터는 다음 실행할 명령어가 있는 현재 code segment의 offset 값을 가진다.
{ 이것은 하나의 명령어 범위에서 선형 명령 집합의 다음 위치를 가리킬 수 있다.
뿐만 아니라 JMP, Jcc, CALL, RET와 IRET instruction이 있는 주소값을 가진다. EIP 레지스터
는 소프트웨어에 의해 바로 엑세스 할 수 없고 control-transfer instruction (JMP, Jcc, CALL,
RET)이나 interrupt와 exception에 의해서 제어된다. EIP 레지스터를 읽을 수 있는 방법은
CALL instruction을 수행하고 나서 프로시저 스텍(procedure stack) 으로부터 리턴하는
instruction의 address를 읽는 것이다. 프로시저 스택의 return instruction pointer의 값을 수정
하고 return instruction(RET, IRET)을 수행함으로 해서 EIP 레지스터의 값을 간접적으로 지정
해 줄 수 있다. } 라고 써있긴 한데,, 뭔 소리인지는 모르겠다.
프로그램 구동 시 Segment에서는 무슨 일이!!??
void function(int a, int b, int c){
char buffer1[15];
char buffer2[10];
}
void main(){
function(1, 2, 3);
}
별 동작을 하지 않는 아주 간단한 프로그램이다. 위 프로그램을 어셈블리어로 보면
0x80482fc <main>: push %ebp
0x80482fd <main+1>: mov %esp,%ebp
0x80482ff <main+3>: sub $0x8,%esp
0x8048302 <main+6>: and $0xfffffff0,%esp
0x8048305 <main+9>: mov $0x0,%eax
0x804830a <main+14>: sub %eax,%esp
0x804830c <main+16>: sub $0x4,%esp
0x804830f <main+19>: push $0x3
0x8048311 <main+21>: push $0x2
0x8048313 <main+23>: push $0x1
0x8048315 <main+25>: call 0x80482f4 <function>
0x804831a <main+30>: add $0x10,%esp
0x804831d <main+33>: leave
0x804831e <main+34>: ret
0x804831f <main+35>: nop
End of assembler dump.
(gdb) disas function
Dump of assembler code for function function:
0x80482f4 <function>: push %ebp
0x80482f5 <function+1>: mov %esp,%ebp
0x80482f7 <function+3>: sub $0x28,%esp
0x80482fa <function+6>: leave
0x80482fb <function+7>: ret
End of assembler dump.
(gdb)
앞에 0x
~
는 logical adrress다. 잘 보면 main함수가 위에 있고 function 함수가 밑에 있음을 알 수 있다. 따라서 메모리 주소를 바탕으로 생성될 이 프로그램의 segment는 밑의 그림과 같을 것이다.
이 segment의 logical addresssms 0x08000000 부터 시작하지만 실제 프로그램이 컴파일과 링크되는 과정에서 다른 라이브러리들을 필요로 하게 된다. 따라서 코딩한 코드가 시작되는 지점은 잘 몰라잉~ \
시쟉
EIP는 main()함수의 시작점을 가리키고 있다. ESP는 스텟의 맨 꼭대기에 위치해서 POP PUSH명령어를 수행할 수 있게 해준다.
ebp를 저장하는 이유는 이전에 수행하던 함수의 데이터를 보조하기 위해서이다. =>> base pointer라고 부른다
함수가 시작될 때에는 stack pointer와 base pointer를 새로 지정하는데 이러한 과정을 함수 프롤로그 과정이라고 한다.
*push %ebp * 이전 함수의 base pointer를 저장하면 stack pointer는 4바이트 아래인 0xbffffa78을 가리키게 된대요,,
mov %esp, %ebp ESP 값을 EBP에 복사하였다. 이로써 base pointer와 stack pointer는 같은 지점을 가리키게 된다.
sub $0x8, %esp sub은 빼라는 명령어다. 이 명령어를 읽게 되면 ESP에서 8을 빼게 된다. 따라서 ESP는 8바이트 아래를 가리키게 되고 스택에 8바이트 공간이 생긴다 이를 스택이 8바이트 확장되었다고 말한다. ESP에는 원래 값인 0xbffffa78에서 8 빠진 0xbffffa70이 된다.
and $0xfffffff0, %esp ESP와 11111111 11111111 11111111 11110000과 AND 연산을 수행한다. 그렇게 하면 ESP 주소값의 맨 뒤 4bit를 0으로 만들기 위함이다.
mov $0x0, %eax EAX 레지스터에 0을 넣는다!
sub %eax,%esp ESP에 들어있는 값에서 EAX에 들어있는 값만큼 뺀다. 이는 stack pointer를 EAX만큼 확장시키려 하는 것.
sub $0x4, %esp ESP값에서 4를 빼줌으로써 스택을 4바이트 확장시켰다.
위에서 했었던 명령들을 하고 난 후의 상황이다. ESP는 8바이트 한 번, 4바이트 한 번. 총 12바이트 이동했다. 다음으로는
PUSH $0x03
PUSH $0x02
*PUSH $0x01 *function(1,2,3)을 수행하기 위해 인자값을 차례로 넣어준다.
*call 0x80482f4 *0x80482f4에 있는 명령을 수행하라는 것이다. 0x80482f4에는 function() 함수가 위치해있따!
call 명령은 함수를 호출할 때 사용되는 명령. 함수가 끝난 후 다음 명령이 실행될 수 있도록 다음 명령이 있는 주소를 스택에 넣은 다음에 EIP에 함수의 시작 지점을 넣는다.
*add $x10, %esp *명령이 있는 주소. 함수 수행이 끝나고 POP! 하면 다음 수행해야 할 명령의 주소를 알 수 있게 된다. 이게 바로 Buffer overflow에서 가장 중요하다는 return address당.
이제 EIP에는 function 함수의 주소값이 들어간다.
이제 EIP는 function() 함수 위치를 가리키고 있고 스택에는 아까 전에 push로 넣었던 값이 쌓여있다.
*push %ebp *
mov %esp,%ebp function()함수에서도 마찬가지로 함수 프롤로그가 수행이 된다. main 함수에서 사용하던 base pointer가 저장되고 stack point를 function()함수의 base pointer로 삼는다.
function() 함수를 빠져나오는 leave instruction을 수행했다. leave instruction은 함수 프롤로그 작업을 되돌리는 일을 한다.
위에서 했던 push %ebp mov %esp,%ebp를 되돌리는 작업은 먼저 mov %ebp, %esp 하고 push로 넣었던 push %ebp를 pop %ebp로 되돌린다.
stack pointer를 이전의 base pointer로 잡아서 function() 함수에서 확장했던 스택 공간을 없애버리고 push해서 넣어두었던 main() 함수의 base pointer를 복원!
POP를 했으므로 stack pointe는 하나 올라갈 것이다. 이제 stack pointer는 return address가 있는 지점을 가리킬 것이다.
ret instruction 이전 함수로 return 명령 EIP 레지스터에 return address를 POP 하여 집어넣는 역할 == pop %eip
ret을 수행하고 나면 return address는 POP 되어 EIP에 저장되고 stack pointer는 1 올라간다.
add $0x10, %esp는 스택을 16바이트 줄인다. 따라서 stack pointer는 0x804830c에 있는 명령, sub %eax, %esp를 수행하기 전의 위치로 돌아가게 된다.
leave
ret 을 수행하게 되면 각 레지스터들 값은 main() 함수 프롤로그 작업을 되돌리고 main()함수 인전으로 돌아가게 된다.
큰일 났다. 뭔 소리인지 모르겠다. 공부 빡세게 해야겠다.
댓글
이 글 공유하기
다른 글
-
포너블 스터디 6주차 과젯! [Dreamhack wargame off_by_one 000-001]
포너블 스터디 6주차 과젯! [Dreamhack wargame off_by_one 000-001]
2020.05.29 -
포너블 스터디 2주차 과제 제출
포너블 스터디 2주차 과제 제출
2020.04.10 -
포너블 2주차 과제 드림핵 요약
포너블 2주차 과제 드림핵 요약
2020.04.09 -
Bandit 0~14
Bandit 0~14
2020.04.02