1. 메모리 구조
1.1 메모리 구조
시스템이 초기화 되기 시작하면 시스템은 커널을 메모리에 적재시키고 가용 메모리 영역을 확인한다. 운영에 필요한 기본 명령어 집합을 커널에서 찾기 때문에 반드시 위치해야하며 64Byte나 그 이상의 영역을 사용한다. 64 비트씩 처리하여 0~2^64-1의 범위의 메모리를 가진다.
멀티태스킹으로 여러 개의 프로세스가 병렬적으로 작업을 수행하고, 각 Segment에서 Code, Data, Stack 구조를 가지고 있다. 시스템에 최대 16,383개의 segment가 존재하고 긱 최대 2^32byte의 크기를 가진다.
1.2 Segment 구조
(1) Code segment
-instruction 집합. 명령을 수행하며 많은 분기 과정과 점프, 시스템 호출 등을 수행하는데 메모리 상의 특정 위치에 있는 명령을 지정해 주어야 한다. segment는 정확한 위치를 컴파일 과정에서 알 수 없기 때문에 logic address를 사용한다.
-logic address → physical address 매핑. segment selector에 의해서 시작 위치(offset)를 찾을 수 있고 자신의 시작 위치에(logic address) 있는 명령을 수행할 지를 결정하게 되는 것이다. 따라서 메모리 주소 physical address는 offset + logic address이다.
⇒
ex) segment가 실제로 위치하면 0x80010000 일시 code segment내에 들어있는 하나의 Instruction IS 1을 가르키는 주소는 0x00000100이다. 이것은 logical address이고 이 instruction의 실제 메모리 상의 주소는 둘을 더한 0x80010100이 된다.
(2) Data segment
프로그램이 실행 시에 사용되는 데이터가 들어간다. 각각 현재 모듈의 data structure, 데이터 모듈, 동적 생성 데이터, 다른 프로그램과 공유 데이터 부분이다.
(3) Stack segment
-현재 수행되고 있는 handler, task, program이 저장하는 데이터 영역으로, 버퍼가 이에 자리 잡게 된다. 또한 다중 스텍을 생성할 수 있고 각 스텍들 간의 swith가 가능하다. 지역 변수들이 자리 잡는다.
-스텍은 프로세스의 명령에 의해 데이터를 저장해나가는 과정을 거치는데 stack pointer(SP)라고 하는 레지스터가 스택의 맨 앞에 존재한다. 저장하고 읽는 과정은 PUSH와 POP instruction에 의해서 수행된다.
2. CPU 레지스터 구조
2.1 개념
CPU 내부에 존재하는 메모리 저장 공간을 레지스터(register)라고 한다.
- 범용 레지스터: 논리 연산, 수리 연산에 피연산자, 주소 계산 피연산자, 메모리 포인터 저장
- 세그먼트 레지스터: 세그먼트들의 주소가 들어있는 레지스터
- 플래그 레지스터: 프로그램의 현재 상태나 조건 등을 검사하는 플래그
- 인스트럭션 포인터: 다음 명령 메모리 주소
(1) 범용 레지스터 :
4개의 32bit로서 프로그래머가 임의로 조작할 수 있게 허용되어 있는 레지스터이다.
- EAX(Extended Accumulator Register): 피연산자와 연산 결과의 저장소
- EBX: DS segment안의 데이터를 가리키는 포인터
- ECX(Extended Counter Register): 문자열 처리나 루프를 위한 카운터
- EDX: I/O 포인터
- ESI: DS 레지스터가 가리키는 data Segment 내의 어느 데이터를 가리키고 있는 포인터. 문자열 처리에서 source를 가리킴.
- EDI: ES 레지스터가 가리키고 있는 data segment내의 어느 데이터를 가리키고 있는 포인터. 문자열 처리에서 destunation을 가리킴.
- ESP(stack pointer): SS 레지스터가 가리키는 stack segment의 맨 꼭대기를 가리키는 포인터
- EBP(base pointer): SS 레지스터가 가리키는 스택 상의 한 데이터를 가리키는 포인터→ 이전에 수행하던 함수의 데이터를 기억함
⇒ ESP + EBP 함수 시작 시 새로 지정 = 함수 프롤로그 과정
(2) 세그먼트 레지스터 :
특정 세그먼트를 가리키는 포인터 역할을 한다. CS 레지스터는 code segment를, DS, ES, FS, GS 레지스터는 data segment를 SS 레지스터는 stack segment를 가리킨다.
(3) 플래그 레지스터 :
상태 플러그, 컨트롤 플래그, 시스템 플러그의 집합이다. 초기화 시 이 제지스터는 0x00000002의 값을 가진다. 1,3,5,15,22~31 비트는 예약되어 있어 소프트웨어에 조작할 수 없게 되어있다.
(4) 인스트럭션 포인터:
다음 실행할 명령어가 있는 현재 code segment의 offset 값을 가진다. 이것은 하나의 명령어 범위에서 선형 명령 집합의 다음 위치를 가질 수 있다. 또한 JMP, Jcc, CALL, RET와 IRET insteuction이 있는 주소값을 가진다.
2.2 구조 이해 실습
<mang.c>
void function(int a, int b, int c){
char buffer1[15];
char buffer2[10];
}
void main(){
function(1,2,3);
}
(1) 어셈블리 코드로 변환
💡 gcc -S -o mang.asm mang.c
(2) 변환된 파일 조회
💡 cat mang.asm
.file "mang.c"
.text
.globl function
.type function, @function
function:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movl %esi, -40(%rbp)
movl %edx, -44(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
nop
movq -8(%rbp), %rax
subq %fs:40, %rax
je .L2
call __stack_chk_fail@PLT
.L2:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size function, .-function
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $3, %edx
movl $2, %esi
movl $1, %edi
call function
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 11.2.0-19ubuntu1) 11.2.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
(3) gcc와 gdb
gcc는 GNU 프로젝트에서 개발한 C 및 C++ 컴파일러이다.
gdb는 GNU 프로젝트의 디버거이다. gdb에서 디버깅할 모든 프로그램은 “-g” 옵션을 켜고 gcc에서 컴파일해야한다.
- 프로그램 생성
💡 sudo gcc -o mang mang.c
- 디버거
💡 gdb mang
(gdb) disas main
💡 (gdb) disas funtion
(4) 분석
- 앞에 붙어있는 주소는 logical address이다.
- ⇒ function 함수는 0x0000000000001149, main 함수는 0x0000000000001184로 main 함수가 위에 자리 잡고 있다.
- 프로그램이 시작되면 EIP 레지스터 즉, CPU가 수행할 명령이 있는 레지스터는 main() 함수가 시작되는 코드를 가리키고 있을 것이다.
- ⇒ 특정 부분에서 프로그램을 종료하려면 *포인터를 사용하거나 파일 이름:함수를 사용한다.
(5) 과정 이해
-초기 상태
EIP : main 함수 시작점
ESP: 스택의 맨 꼭대기
EBP: 이전 데이터 보존
어느 지점에 데이터를 넣을 것인지 system architecture에 따라 다름.
-실행 시
<+0>: endbr64
<+4>: push %rbp //이전 함수 bp 저장, sp 4바이트 아래로 내려감
<+5>: mov %rsp,%rbp //mov로 sp를 bp에 복사
<+8>: mov $0x3,%edx //I/O 포인터
<+13>: mov $0x2,%esi //출발 주소 저장
<+18>: mov $0x1,%edi //목적지 주소 저장
<+23>: call 0x1149 <function>
<+28>: nop //EIP에 function의 시작주소
<+29>: pop %rbp
<+30>: ret
<+0>: endbr64
<+4>: push %rbp //함수 프롤로그 실행
<+5>: mov %rsp,%rbp //mov로 sp를 bp에 복사
<+8>: sub $0x30,%rsp //스택을 48 바이트 확장
//스텍은 4바이트씩 확장되어 15>!6, 10>12로 = 24
<+12>: mov %edi,-0x24(%rbp)
<+15>: mov %esi,-0x28(%rbp)
<+18>: mov %edx,-0x2c(%rbp)
<+21>: mov %fs:0x28,%rax //스텍 카나리 프롤로그
<+30>: mov %rax,-0x8(%rbp) //%fs:0x28와 rbp
<+34>: xor %eax,%eax //eax를 0으로 만든다.
<+36>: nop
<+37>: mov -0x8(%rbp),%rax //스텍 카나리 에필로그
<+41>: sub %fs:0x28,%rax
<+50>: je 0x1182 <function+57>
<+52>: call 0x1050 <__stack_chk_fail@plt>
//fail 호출 시 프로그램 강제 종료
<+57>: leave
<+58>: ret //이전 함수로 리턴할 것
**스택 카나리(stack canary)
-스택 버퍼 오버 플로우로부터 반환 주소를 보호하는 보호 기법
-스택 카나리는 함수의 프롤로그에서 스택의 버퍼와 반환 주소 사이에 임의의 값을 삽입하고 에필로그에서 값의 변조를 확인하는 보호 기법이다.
-stack canary가 적용된 바이너리에 BOF 공격을 그대로 수행하면 stack smashing detected라는 오류가 발생
== prologue ==
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
== epilogue ==
0x00000000000006dc <+50>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000006e0 <+54>: xor rcx,QWORD PTR fs:0x28
0x00000000000006e9 <+63>: je 0x6f0 <main+70>
0x00000000000006eb <+65>: call 0x570 <__stack_chk_fail@plt>
**TLS(Thread Local Storage)
스레드 별로 고유한 저장공간을 가질 수 있는 방법
프로세스 실행에 필요한 여러 데이터 저장
**fs
세그먼트 레지스터의 일종으로 시작 시 랜덤 값 지정. 목적이 지정되지 않아 임의로 사용할 수 있는 세그먼트. fs==TLS
**카나리 우회 방법
(1) 무차별 대입(브르투포스)
(2) TLS 접근
TLS에 전역변수로 저장되어 매 함수마다 이를 참조하기 때문에 실행 중 카나리 값을 알아내거나 값을 조작한다.
(3) 스택 카나리 릭
함수의 프롤로그에서 스택에 카나리 값 저장하므로 읽어서 우회.
https://jiravvit.tistory.com/entry/pwnablexyz-message-풀이-OOB-pie-leak-canary-leak
3. Buffer overflow의 이해
3.1 개념
버퍼(buffer): 연산 작업에 필요한 데이터를 일시적으로 메모리 상에 저장. malloc()과는 다르게 지역 변수가 생성되고 사라진다. buffer overflow란 준비 버퍼 이상의 큰 데이터를 작성 시 다른 영역을 침범하게 되는 현상이다.
3.2 원리
준비 버퍼 이상으로 작성을 할 시 return address에 코드가 덮어쓰여지게 된다.
ex. 스택 버퍼 오버 플로우
strcpy(buffer2, receive_from_client);
strcpy 함수는 길이 체크를 해 주지 않기 때문에 receive_from_client 안에 들어있는 데이터에서 NULL를 만날 때까지 복사를 한다.
**Byte order
데이터가 저장되는 순서가 바뀌는 이유는 바이트 정렬 방식이다. Byte order는 big endian과 Iittle endian 방식이 있다.
big endian 방식은 바이트 순서가 낮>높 little endian 방식은 높>낮으로 메모리 주소가 되어있다.
EIP에 return address 위에 있는 쉘 코드의 시작 주소를 넣으려고 시도한다.
exxcve(”/bin/sh”, - ) 수행
4byte 글자 수 맞추기 위한 push 0인 것으로 예상
3.3 만날 수 있는 문제점 한 가지
공격 코드가 return address인 24byte 공간 안에 들어갈 수 없을 경우 빈 스텍 공간을 활용할 수 있다. return address에 정확한 명령어의 위치를 알아내는 것은 어렵기때문에 간접적으로 변경하는 방법을 수행한다.
4. 쉘 코드 제작
sh.c
#include <unistd.h>
void main(){
char *shell[2];
shell[0] = "/bin/sh";
shell[1] = NULL;
execve(shell[0],shell,NULL);
}
**execve
int execve(const char *filename, char *const argv[], char *const envp[]);
4.1 Dynamic Link Library와 Static Link Library
(1) sh.c 프로그램에서 호출하는 execve()함수 보기
💡 gcc -v
-v: 버전 확인
(2) 64비트 컴파일
💡 gcc -static -g -o sh sh.c
-static: 정적 라이브러리에 링크
-o: 파일 이름 지정
-g: 확장 기호 테이블, 문장 번호, 지역과 외부 변수에 대한 디버깅 정보를 삽입한다.
-d: 오브젝트 파일을 기계어로 역어셈블
-A 32: 32라인만 출력함
0000000000446730 <__execve>:
446730: f3 0f 1e fa endbr64
**446734: b8 3b 00 00 00 mov $0x3b,%eax**
446739: 0f 05 syscall
44673b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax
446741: 73 01 jae 446744 <__execve+0x14>
446743: c3 ret
**446744: 48 c7 c1 b8 ff ff ff mov $0xffffffffffffffb8,%rcx**
44674b: f7 d8 neg %eax
**44674d: 64 89 01 mov %eax,%fs:(%rcx)**
446750: 48 83 c8 ff or $0xffffffffffffffff,%rax
446754: c3 ret
44
'tmp' 카테고리의 다른 글
네이버 기사 크롤링 파이썬 코드(네이버 뉴스 특정 카테고리) (0) | 2022.11.23 |
---|---|
네이버 기사 크롤링 파이썬 코드(네이버 전체 창) (0) | 2022.11.18 |
[GIT]깃 사용법 (0) | 2022.05.12 |
[ctf] OSINT 관련 링크 (0) | 2022.05.04 |
[ctf] 참고용 (0) | 2022.05.04 |