나혼자 공부장

달고나 BOF 기초문서 정리 본문

pwnable

달고나 BOF 기초문서 정리

라부송 2019. 11. 19. 20:35

*새로 알게 된 개념 위주

 

1. Memory Architecture

시스템은 운영에 필요한 기본적인 명령어 집합을 커널, 즉 낮은 주소에서 찾는다.

CPU가 한꺼번에 처리할 수 있는 데이터 양

32bit : 2^32-1

64bit : 2^64-1

 

하나의 프로세스를 실행시킬 때, 이를 segment라는 단위로 묶어서 가용 메모리 영역에 저장시킨다.

code segment는 instruction이 포함되어있고, 컴파일러가 만들어낸 코드가 저장되어있다. 명령 수행 과정에서 분기나 점프의 경우 메모리 상의 어떤 주소에 있는 명령을 지정해주어야 한다.

그러나 segment 입장에서는 자신이 메모리의 어느 주소에 저장될 지 컴파일 과정에서는 알 수 없기 때문에 physical address 직접 지정은 불가능하다.

 

그래서 사용하는게 logical address이다.

physical address = offset(segment selector에서 알아낼 수 있음) + logical address

 

segment가 어느 위치에 있더라도 offset만 알아낼 수 있다면 명령어의 정확한 위치를 찾아낼 수 있다.

IP같이 명령어 위치를 지정해둔 포인터도 다 offset을 저장해둘 뿐 physical address를 지정하는 역할이 아니고 그렇게 할 수도 없다.

 

 

2. CPU 레지스터 구조

 

범용, 세그먼트, 플래그, 인스트럭션 포인터 등등으로 나눌 수 있다.

32bit 기준

https://wisepine.tistory.com/9

 

레지스터 요약

1. 레지스터(Register) CPU가 요청을 처리하는데 데이터를 일시적으로 저장하는 기억 장치 CPU 단독만으로는 데이터를 메모리에 저장할 방법이 없기 때문에 연산을 위해서는 반드시 레지스터를 거쳐야한다. Bit -..

wisepine.tistory.com

 

 

Status flags

CF - carry flag. 연산을 수행하면서 carry가 발생하면 1이 됨

PF - parity flag. 연산 결과 최하위 바이트 값이 1이 짝수일 경우에 1이 됨

AF - Adjust flag. 연산 결과 carry가 3bit 이상 발생할 경우 1이 됨

ZF - Zero flag. 결과가 zero임을 나타냄 (if와 같은 조건문이 만족될 경우)

SF - Sign flag. 연산 결과 최상위 비트의 값. 즉 부호를 나타냄 - 양수이면 0, 음수이면 1

OF - Overflow flag. 

DF - Direction flag. 문자열 처리에 있어서 1일 경우 high->low 방향으로 instruction 감소. 0일 경우 증가

 

 

3. Buffer Overflow의 이해

 

Byte order

big endian : low memory -> high memory 순서

little endian : high memory -> low memory 순서

 

왜 직관적이지 못한 little endian 방식이 더 많이 쓰이는가?

- 낮은 수의 변화는 낮은 메모리 영역, 높은 수의 변화는 높은 메모리 영역에 자리를 잡겠다고 하는 것이 little endian 방식의 논리이다. 

 

위 그림은 return 주소에 쉘 코드의 주소를 넣어준 형태는 아니고, 똑같이 main() 함수로 돌아가되 main 으로 돌아가자마자 실행되는 코드가 쉘 코드가 되도록 짠 페이로드이다.

 

쉘 코드의 주소를 return 주소로 전달한 공격 형태이다.

그러나 이렇게 하면 한 가지 문제점이 발생할 수 있다. 만약 RET 위의 버퍼 공간이 쉘 코드를 넣을 만큼 충분하지 않다면 어떻게 할 것인가? 그리고 함수의 총 44byte의 공간을 임의의 문자로 나열하느라고 낭비하고 있다는 점도 문제라고 할 수 있다.

 

같은 오버플로우 공격 코드지만, 배치가 사소하게 다르다.

쉘 코드의 주소를 RET 해주는 형태가 아니라, 함수 안에 쉘 코드를 배치해준다. 그리고 쉘 코드를 return 해주는데, 그림 18과는 다른 쉘 코드다. ESP 레지스터가 가리키는 지점을 공격 쉘 코드가 있는 지점으로 이동시키는 쉘 코드다. 이 과정을 쉘 코드로 변환했을 때는 단 8byte면 충분하기 때문에 버퍼 공간이 부족해서 문제가 생길 확률은 낮다. 그리고 ESP가 쉘 코드를 실행하면 공격 쉘 코드가 있는 곳으로 이동, 공격을 실행하는 형태이다. 

그림 18과 비교해서 훨씬 공간 활용성이 좋다.

 

 

쉘 코드 : 바이너리 형태의 기계어 코드

 

DLL : Dynamic Link Library - 같은 기능을 하는 기계어 코드를 저장해둔 라이브러리

저장 공간의 낭비를 최소화하기 위해 있는게 DLL이다. 하지만 운영체제의 버전과 libc의 버전에 따라 호출 형태나 링크 형태가 달라질 수 있기 때문에, 그 영향을 받지 않도록 실행파일이 직접 가지고 있도록 하는게 Static Link Library이다.

Static Link Libary 방식으로 하면 어떤 운영체제던 간에 상관이 없어지므로 이식성이 좋아지고, 실행파일의 크기가 좀 더 커진다.

 

 

가장 고전적인 방법 

- 시행착오를 거치며 쉘 코드가 있는 곳의 명확한 address를 추측하는 것. 

 

NOP (No Operation) 로 채우는 이유 : intstruction set에 정의된 만큼의 길이를 instruction 이라고 알아듣는데, instruction의 길이가 일정하기 않기 때문에, 서로 섞이는 불상사가 발생한다. 이 instruction을 끊기 위한 목적으로 사용된다.

 

보다 쉬운 방법들

1. 환경변수를 이용하는 방법 - eggshell.c 을 이용할 수 있음

2. Return into libc 기법 (RTL)

- NX bit 기법을 우회하기 위한 것이다.

- EIP 레지스터에 stack segment 영역의 주소가 들어가는걸 방어하는 것이다.

 

 

Comments