ZINO
HomeResearchBlogTagsPlaygroundStackPortfolio
⌘K

레드팀 · AI 자동화 · 시스템 엔지니어링

직접 만들고, 실험하고, 기록한다

개인정보처리방침·이용약관

© 2026 ZINO LAB

← 태그 목록

Tag Archive

#pwnable

이 태그가 포함된 글을 최신순으로 모아봤어요.

46
posts
BLOG2026-08-01
[🥇 Gold 3] vtable 을 못 옮기면 libc 안의 진짜 vtable 을 빌린다 — DreamHack Bypass IO_validate_vtable 풀이
blog

[🥇 Gold 3] vtable 을 못 옮기면 libc 안의 진짜 vtable 을 빌린다 — DreamHack Bypass IO_validate_vtable 풀이

read 한 번으로 FILE 구조체 300바이트를 통째로 덮을 수 있는데, 대상이 glibc 2.27 이라 vtable 을 엉뚱한 주소로 돌리면 그 자리에서 abort 한다. 2.24 부터 fclose 안에 IO_validate_vtable 범위 검사가 인라인돼 있기 때문이다. 그래서 vtable 을 옮기는 대신 __libc_IO_vtables 섹션 안에 원래부터 있던 _IO_str_jumps 를 그대로 갖다 쓴다. 그 vtable 의 __finish 인 _IO_str_finish 는 FILE+0xe8 을 함수 포인터로 호출하는데, 그 칸도 우리가 덮는 300바이트 안에 있다.
#dreamhack#ctf#pwnable+4
 
BLOG2026-08-01
[🥇 Gold 4] read 한 번을 통째로 훔쳐 오는 법 — DreamHack iofile_aw 풀이
blog

[🥇 Gold 4] read 한 번을 통째로 훔쳐 오는 법 — DreamHack iofile_aw 풀이

memcpy 한 줄이 stdin 이 가리키는 FILE 구조체의 앞 0x40바이트를 통째로 갈아끼운다. 그 0x40 안에 _IO_buf_base 가 들어 있어서, 다음에 fgets 가 부르는 read(2) 의 착지점을 내가 정할 수 있다. Full RELRO 라 GOT 는 못 건드리지만 대신 전역 int 하나 — read 길이를 담고 있는 size — 를 512에서 2560으로 키웠다. 그 순간 소스에 "No overflow here" 라고 주석까지 달려 있던 read_command 가 512바이트 스택 버퍼에 568바이트를 부어 넣는 함수가 된다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-31
[🥈 Silver 2] stderr + 1 이 가리키는 8바이트로 함수 테이블을 갈아끼운다 — DreamHack iofile_vtable 풀이
blog

[🥈 Silver 2] stderr + 1 이 가리키는 8바이트로 함수 테이블을 갈아끼운다 — DreamHack iofile_vtable 풀이

메뉴 4번이 read(0, stderr + 1, 8) 을 한다. stderr 는 FILE 포인터라 +1 은 8바이트가 아니라 구조체 하나만큼, 즉 0xd8 을 건너뛴다. 그 자리가 하필 _IO_FILE_plus 의 vtable 포인터다. 전역 배열 name 에 get_shell 주소를 미리 넣어 두고 vtable 을 name - 0x38 로 돌리면, fwrite 가 부르는 __xsputn 칸이 정확히 name 에 겹친다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-30
[🥇 Gold 4] fread 안에 숨은 read 한 번 — DreamHack _IO_FILE Arbitrary Address Write 풀이
blog

[🥇 Gold 4] fread 안에 숨은 read 한 번 — DreamHack _IO_FILE Arbitrary Address Write 풀이

main이 fopen으로 받은 FILE 포인터를 그대로 read의 버퍼로 넘긴다. _IO_FILE 구조체를 통째로 위조할 수 있다는 뜻이다. fread는 요청량이 버퍼보다 작으면 __underflow로 내려가 read(_fileno, _IO_buf_base, 버퍼크기)를 부르는데, 이 세 인자가 전부 구조체 안에 있다. 버퍼 포인터를 전역 변수로, fd를 0으로 바꿔 stdin에서 0xDEADBEEF를 받아 적었다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-28
[🥈 Silver 3] 카나리 원본이 같은 스레드 스택 안에 있다 — DreamHack Master Canary(mc_thread) 풀이
blog

[🥈 Silver 3] 카나리 원본이 같은 스레드 스택 안에 있다 — DreamHack Master Canary(mc_thread) 풀이

스레드 안에서 스택 버퍼를 위로 무제한 덮어쓸 수 있는 pwnable. 카나리를 유출할 방법이 없어 보이지만, 사실 유출할 필요가 없다. 검사에 쓰이는 원본(TLS의 fs:0x28)이 넘치는 버퍼와 같은 스레드 스택 안에 있어서, 스택 복사본과 원본을 같은 값으로 함께 덮으면 검사를 통과한다. 다만 그 사이에 놓인 TCB 필드를 그냥 뭉개면 다음 read()가 죽는다.
#dreamhack#ctf#pwnable+6
BLOG2026-07-28
[🥉 Bronze 3] 리크도 셸도 없이 strcmp 하나를 오라클로 바꾸기 — DreamHack checkflag 풀이
blog

[🥉 Bronze 3] 리크도 셸도 없이 strcmp 하나를 오라클로 바꾸기 — DreamHack checkflag 풀이

플래그를 맞히면 Correct 를, 틀리면 Failed 를 돌려주는 것이 전부인 바이너리다. 리크할 포인터도 덮을 리턴 주소도 없다. 그런데 main 이 flag 를 담는 버퍼와 우리가 입력하는 버퍼를 같은 스택 영역에 0x40 간격으로 나란히 놓고, read 는 그보다 훨씬 긴 0xc8 바이트를 받는다. 0x40+d 바이트를 보내면 flag 앞 d 글자가 내 바이트로 덮이고, 그 상태의 strcmp 는 "flag 의 남은 뒷부분이 이거 맞아?" 라는 접미사 오라클이 된다. 뒤에서부터 한 글자씩 물어보면 16글자가 16초 만에 나온다.
#dreamhack#ctf#pwnable+7
BLOG2026-07-22
[🥉 Bronze 2] 딱 한 번의 임의 주소 쓰기로 셸 열기 — DreamHack rtld 풀이
blog

[🥉 Bronze 2] 딱 한 번의 임의 주소 쓰기로 셸 열기 — DreamHack rtld 풀이

주어진 건 `*(long*)addr = value` 딱 한 번, 그리고 stdout 리크 하나뿐이다. get_shell() 은 있지만 PIE base 를 몰라 못 쓴다. libc 훅도, FSOP 도, atexit 체인도 전부 막힌 상태에서 문제 이름이 가리키는 곳 — 로더(ld.so) 의 _rtld_global 을 노렸다. 종료 시 _dl_fini 가 호출하는 락 함수 포인터를 one_gadget 으로 덮으면 프로그램이 끝나는 순간 셸이 열린다.
#dreamhack#pwnable#rtld+3
BLOG2026-07-22
[🥉 Bronze 1] 시그널 프레임을 직접 위조해 셸을 딴다 — DreamHack SigReturn-Oriented Programming 풀이
blog

[🥉 Bronze 1] 시그널 프레임을 직접 위조해 셸을 딴다 — DreamHack SigReturn-Oriented Programming 풀이

16바이트 버퍼 오버플로우와 gadget 하나(pop rax; syscall; ret)만 있으면 커널의 rt_sigreturn을 이용해 레지스터 전체를 통째로 위조할 수 있다. 셸코드도 ROP 체인도 없이, No PIE로 고정된 BSS 주소 하나만으로 스택 리크 없이 execve("/bin/sh")까지 도달한다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-21
[🥉 Bronze 1] 가젯이 syscall 하나뿐이어도 셸은 딴다 — DreamHack send_sig SROP 풀이
blog

[🥉 Bronze 1] 가젯이 syscall 하나뿐이어도 셸은 딴다 — DreamHack send_sig SROP 풀이

NX가 켜져 있어 스택에 셸코드를 심을 수 없고, 바이너리 안에 쓸만한 ROP 가젯도 pop rax; ret 와 syscall; ret 딱 두 개뿐이다. 그런데 이 두 개만으로 충분하다 — rax에 15(rt_sigreturn)를 넣고 syscall을 호출하면 커널이 스택에서 "가짜 시그널 컨텍스트"를 읽어 모든 레지스터를 우리가 원하는 값으로 통째로 복원해준다. rax=59(execve), rdi=고정주소의 "/bin/sh"로 채운 위조 컨텍스트 하나면 별도의 리크나 다단계 체인 없이 단번에 셸이 뜬다. GDB로 그 syscall 가젯에 브레이크를 걸어 커널이 실제로 우리가 넣은 값 그대로 레지스터를 복원하는 순간을 직접 확인했다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-20
[🥉 Bronze 4] mmap으로 격리한 flag도 포인터 하나면 새어나온다 — DreamHack mmapped 풀이
blog

[🥉 Bronze 4] mmap으로 격리한 flag도 포인터 하나면 새어나온다 — DreamHack mmapped 풀이

flag 파일을 mmap으로 읽기 전용 매핑해두고, 출력 직전에 mprotect로 아예 접근조차 못 하게 막아버린다 — 언뜻 보면 견고한 보호처럼 보인다. 그런데 정작 화면에 출력되는 건 "진짜 flag" 주소가 아니라 전혀 다른 곳에 있는 "가짜" 문자열의 주소다. 스택 버퍼 오버플로우로 이 두 주소가 담긴 지역 변수 두 개를 동시에 덮어쓸 수 있다면 — 보호 대상 자체를 바꿔치기해서 mprotect를 헛손질로 만들고, 출력 대상은 진짜 flag로 바꿔치는 게 가능해진다.
#dreamhack#ctf#pwnable+3
BLOG2026-07-20
[🥉 Bronze 4] std::string 내부를 파고들다가, 정작 답은 배열 하나 옆에 있었다 — DreamHack cpp_string 풀이
blog

[🥉 Bronze 4] std::string 내부를 파고들다가, 정작 답은 배열 하나 옆에 있었다 — DreamHack cpp_string 풀이

전역 std::string에 짧은 문자열을 넣으면 Small String Optimization이 발동해 문자열 데이터가 객체 자기 자신 안에 저장된다는 사실부터 파고들었다. write_file()이 그 c_str() 포인터에서 항상 64바이트를 읽어 파일에 쓴다는 걸 보고 "짧은 문자열을 넣으면 객체 밖 메모리를 읽어 flag를 흘릴 수 있겠다"고 확신했는데, readelf로 실제 주소를 까보니 flag는 그 문자열 객체보다 앞쪽 주소에 있었다 — 읽는 방향이 반대였다. 로컬 재현도 전부 0바이트로 실패를 확인시켜줬다. 다시 보니 답은 훨씬 단순한 곳에 있었다: 인접한 두 배열과 널 종료 없는 출력.
#dreamhack#ctf#pwnable+4
BLOG2026-07-20
[🥈 Silver 3] Full RELRO·카나리·CET가 다 켜져 있어도 상관없었다 — DreamHack pwn-library 풀이
blog

[🥈 Silver 3] Full RELRO·카나리·CET가 다 켜져 있어도 상관없었다 — DreamHack pwn-library 풀이

checksec을 돌리면 RELRO, 카나리, NX, PIE에 SHSTK·IBT(Intel CET)까지 전부 켜져 있다. ROP도, 카나리 우회도, 컨트롤플로우 하이재킹도 전부 무의미한 방어벽이다. 그런데 취약점은 그 어느 것과도 상관없는 곳에 있었다 — 책을 반납하는 함수가 배열 인덱스를 줄이지 않고 포인터도 지우지 않은 채로 free()만 호출한다. 이 댕글링 포인터를 독립된 malloc 호출 하나가 tcache에서 그대로 주워가면서, "이미 반납한 책"과 "방금 훔친 새 책"이 메모리 상에서 완전히 같은 주소를 가리키게 된다. 리크도, ROP도 없이 순수한 포인터 앨리어싱만으로 로컬 파일을 통째로 읽어냈다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-20
[🥈 Silver 4] 서버가 아니라 내 코드가 죽어있었다 — DreamHack __environ 풀이
blog

[🥈 Silver 4] 서버가 아니라 내 코드가 죽어있었다 — DreamHack __environ 풀이

Full RELRO·PIE·스택 카나리가 전부 켜진 바이너리에 주어진 건 임의 주소를 문자열로 읽어주는 오라클 하나뿐이다. libc 주소를 새고, 그 안의 environ 심볼을 다시 같은 오라클로 읽어 스택 주소까지 얻어내는 원리 자체는 금방 세웠다. 그런데 익스플로잇을 돌리면 두 번째 leak마다 서버가 죽은 것처럼 연결이 끊겼다 — 범인은 서버가 아니라 내가 짠 파이썬 코드의 프로토콜 상태 관리였다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-20
[🥈 Silver 2] 모든 스레드가 같은 카나리를 쓴다 — DreamHack master_canary 풀이
blog

[🥈 Silver 2] 모든 스레드가 같은 카나리를 쓴다 — DreamHack master_canary 풀이

스레드를 만들고 그 스택 버퍼에 무제한으로 읽고 쓸 수 있는 pwnable. 스레드는 함수가 끝나자마자 죽어버리는데, 전역 포인터는 계속 그 죽은 스택을 가리킨다. 죽은 스택을 따라 계속 걸어 올라가면 그 스레드의 TCB, 그리고 그 안의 canary(스택 카나리)에 닿는다 — 이 canary는 프로세스를 만들 때 단 한 번 생성돼 모든 스레드가 그대로 나눠 쓰는 "마스터" 카나리였다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-20
[🥈 Silver 2] 4개만 막고 나머진 다 열어뒀다 — DreamHack Bypass SECCOMP-1 풀이
blog

[🥈 Silver 2] 4개만 막고 나머진 다 열어뒀다 — DreamHack Bypass SECCOMP-1 풀이

임의 셸코드를 그대로 실행시켜주는 pwnable인데, execve로 셸을 따는 흔한 루트가 seccomp로 막혀 있다. 그런데 이 필터는 시스템콜 "번호" 네 개만 콕 집어 차단하는 화이트리스트가 아니라 블랙리스트였다. 목록에 없는 시스템콜은 전부 통과한다는 뜻이라, open 대신 openat으로 파일을 열고 write 대신 sendfile로 그 내용을 그대로 흘려보냈다.
#dreamhack#ctf#pwnable+3
BLOG2026-07-20
[🥉 Bronze 3] 크기 변수 자체가 과일이었다 — DreamHack Cherry 풀이
blog

[🥉 Bronze 3] 크기 변수 자체가 과일이었다 — DreamHack Cherry 풀이

read()를 두 번 부르는 아주 짧은 pwnable. 첫 read는 6바이트짜리 버퍼에 16바이트를 받아 명백히 넘치는데, 그 넘친 자리에 하필 두 번째 read가 쓸 크기(buf_size)를 결정하는 변수 자체가 놓여 있었다. 첫 오버플로우로 크기 제한 자체를 풀어버리면, 두 번째 오버플로우는 원하는 만큼 얼마든지 써도 되는 오버플로우가 된다.
#dreamhack#ctf#pwnable+3
BLOG2026-07-20
[🥉 Bronze 4] 80바이트 버퍼에 128바이트를 채워 tmp_fd를 훔친다 — DreamHack awesome-basics 풀이
blog

[🥉 Bronze 4] 80바이트 버퍼에 128바이트를 채워 tmp_fd를 훔친다 — DreamHack awesome-basics 풀이

chall.c 는 flag 를 읽어 지역변수 tmp_fd 가 가리키는 파일에 쓰기만 할 뿐, 그 결과를 우리에게 보여주는 코드가 없다. 그런데 바로 위에서 read(stdin, buf, 0x80) 이 buf[80] 보다 48바이트 더 받는다. 이 여유분으로 buf 바로 뒤에 붙어 있는 tmp_fd 를 표준출력(1)으로 덮으면, 뒤이은 write(tmp_fd, flag, 0x45) 가 그대로 우리 화면에 flag 를 찍어준다.
#dreamhack#ctf#pwn+4
BLOG2026-07-20
[🥈 Silver 3] BPF 필터가 조용히 실패하면 seccomp는 그냥 없는 셈이다 — DreamHack seccomp 풀이
blog

[🥈 Silver 3] BPF 필터가 조용히 실패하면 seccomp는 그냥 없는 셈이다 — DreamHack seccomp 풀이

scanf("%ld", addr)가 &value 대신 addr 자체를 받는 write-what-where로 전역 seccomp 모드를 STRICT에서 FILTER로 바꿔치기한다. 컴파일된 BPF 필터 배열은 마지막 명령이 BPF_RET이 아니라서 커널 검증에서 -EINVAL로 튕기는데, 그 실패를 코드가 확인하지 않아 seccomp가 통째로 무효화된다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-19
[🥉 Bronze 4] strncmp(18) 부분비교 + 시간기반 토큰 — DreamHack angry_darim 풀이
blog

[🥉 Bronze 4] strncmp(18) 부분비교 + 시간기반 토큰 — DreamHack angry_darim 풀이

time()으로 매초 바뀌는 "동적 토큰"을 strncmp로 딱 18바이트만 비교하는 바이너리. 로컬에서는 실행 순간의 epoch를 그대로 넣어 우회하지만, 정작 DreamHack에 내는 flag는 그 시간 요소를 통째로 들어내야 하는 정적 문자열이었다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-12
[🥈 Silver 4] 인접 힙 청크의 함수 포인터를 넘어 덮다 — DreamHack basic_heap_overflow 풀이
blog

[🥈 Silver 4] 인접 힙 청크의 함수 포인터를 넘어 덮다 — DreamHack basic_heap_overflow 풀이

ptr = malloc(0x20) 과 over = malloc(0x20) 이 힙에서 연속으로 잡히는데 scanf("%s", ptr) 에 길이 제한이 없다. ptr 청크를 지나 over 청크의 첫 멤버인 함수 포인터 over->table 까지 덮어, main 끝의 over->table() 호출을 get_shell 로 돌렸다. 오프셋은 타깃 glibc 2.23 기준 0x28(40) 이었고, 로컬 glibc 와 달라 한 번 헤맸다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🥉 Bronze 3] 리크 없이 스택으로 점프하기 — memset 반환값으로 ASLR을 우회하다 (DreamHack validator)
blog

[🥉 Bronze 3] 리크 없이 스택으로 점프하기 — memset 반환값으로 ASLR을 우회하다 (DreamHack validator)

실행 스택 + 버퍼 오버플로우인데 스택 주소를 흘릴 출력이 하나도 없다. 열쇠는 main 이 ret 하는 순간 rdi 가 buf 를 가리킨다는 것과, memset 이 첫 인자(dest)를 그대로 rax 로 반환한다는 것이다. memset(buf, 0x90, 184) 로 NOP 슬레드를 깔면서 rax=buf 를 얻고, call rax 로 스택을 실행한다. 주소 하드코딩이 하나도 없어 ASLR 이 켜져 있어도 뚫린다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🥇 Gold 4] key 검사를 우회해 같은 청크를 두 번 free — DreamHack tcache_dup2 풀이
blog

[🥇 Gold 4] key 검사를 우회해 같은 청크를 두 번 free — DreamHack tcache_dup2 풀이

전편 tcache_dup 은 libc-2.27 이라 같은 청크를 그냥 두 번 free 하면 됐다. 여기(2.30)는 2.29 부터 들어간 tcache 이중 free 검사(key)가 막는다. free 된 청크를 다시 편집(UAF)해 key 를 0 으로 지워 검사를 우회하고, tcache 의 next 를 puts@got 로 덮어(poisoning) get_shell 을 써넣는다. tcache_get 의 key-clear 때문에 대상은 free@got 가 아니라 puts@got 여야 했던 함정까지.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🥇 Gold 4] printf(buf) 한 줄로 전역 변수를 1337 로 — DreamHack Format String Bug 풀이
blog

[🥇 Gold 4] printf(buf) 한 줄로 전역 변수를 1337 로 — DreamHack Format String Bug 풀이

printf 에 사용자 입력을 형식 문자열로 그대로 넘기는 고전 버그. 64비트 PIE 라 %p 로 코드 주소를 먼저 흘려 base 를 복원하고, %n 으로 전역 changeme 에 1337 을 써넣어 system("/bin/sh") 를 연다. while(1) 루프 덕에 leak 과 write 를 두 번에 나눠 처리했다. 원격 glibc 는 leak 슬롯이 한 칸 앞이라 오프셋을 다시 잡아야 했던 삽질도 함께.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🌱 Sprout] 널 하나 빠졌을 뿐인데 flag 가 새어나온다 — DreamHack memory_leakage 풀이
blog

[🌱 Sprout] 널 하나 빠졌을 뿐인데 flag 가 새어나온다 — DreamHack memory_leakage 풀이

read 는 버퍼 끝에 널을 붙이지 않는다. 16바이트 name 을 널 없이 가득 채우면, printf("%s", name) 이 경계를 넘어 바로 뒤에 붙은 age 와 flag_buf 까지 읽어버린다. flag_buf 에는 case 3 이 /flag 를 미리 담아두니, 출력 한 번으로 flag 가 통째로 새어나온다. 오버플로우 없이 널 하나의 부재만으로 뚫는 메모리 유출.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🥉 Bronze 3] 저장된 EBP 한 바이트를 지워 셸을 연다 — DreamHack off_by_one_000 풀이
blog

[🥉 Bronze 3] 저장된 EBP 한 바이트를 지워 셸을 연다 — DreamHack off_by_one_000 풀이

read 는 널을 붙이지 않고 strcpy 는 널까지 복사한다. 이 어긋남을 이용해 256바이트를 가득 채우면 257번째 널이 저장된 EBP 의 최하위 바이트로 새어 들어간다. 한 바이트가 0이 되면서 프레임 포인터가 0x100 아래로 정렬되고, main 의 leave; ret 이 우리가 뿌려둔 get_shell 로 착지한다. 정렬에 따라 확률적이라 셸이 뜰 때까지 재시도했다.
#dreamhack#ctf#pwnable+4
BLOG2026-07-11
[🥈 Silver 1] 같은 청크를 두 번 free — DreamHack tcache_dup 풀이
blog

[🥈 Silver 1] 같은 청크를 두 번 free — DreamHack tcache_dup 풀이

free() 하고 전역 포인터를 안 지우는 데다 glibc 2.27 이라 double-free 검사도 없다. 같은 청크를 두 번 해제하면 tcache 리스트가 자기 자신을 가리키는 루프가 되고, 이걸 이용해 다음 할당의 fd 를 free@GOT 로 조작한다. no-PIE·Partial RELRO 라 leak 없이 free@GOT 에 get_shell 을 얹고 free 를 부르면 셸이 뜬다.
#dreamhack#ctf#pwnable+5
 
BLOG2026-07-11
[🥇 Gold 4] free 하고 포인터를 안 지웠다 — DreamHack Tcache Poisoning 풀이
blog

[🥇 Gold 4] free 하고 포인터를 안 지웠다 — DreamHack Tcache Poisoning 풀이

메뉴 네 개짜리 힙 문제. free() 뒤에도 전역 포인터를 그대로 두는 바람에 해제된 tcache 청크의 fd 를 마음대로 읽고 쓸 수 있다. no-PIE 라 주소가 고정인 &stdout 를 할당해 libc 를 흘리고, 같은 수법으로 __free_hook 에 system 을 얹어 "/bin/sh" 를 free 하면 셸이 뜬다. glibc 2.27 이라 double-free 검사도 없다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-09
[🌱 Sprout] 한 바이트만 넘쳐도 셸이 열린다 — DreamHack off_by_one_001 풀이
blog

[🌱 Sprout] 한 바이트만 넘쳐도 셸이 열린다 — DreamHack off_by_one_001 풀이

read_str 안의 ptr[len] = 0 이 딱 한 바이트를 버퍼 밖으로 흘린다. 20바이트를 정확히 채워 넣으면 그 NUL 이 바로 뒤에 붙은 int age 의 최하위 바이트를 0으로 덮고, age == 0 분기가 열리며 get_shell 이 실행된다. 오버플로우 한 바이트로 분기 하나를 뒤집는 전형적인 off-by-one.
#dreamhack#ctf#pwnable+4
BLOG2026-07-20
[🥈 Silver 3] age가 fptr로 부활한다 — DreamHack uaf_overwrite 풀이
blog

[🥈 Silver 3] age가 fptr로 부활한다 — DreamHack uaf_overwrite 풀이

Human 구조체와 Robot 구조체가 우연히 크기가 같아 같은 tcache(0x30) bin을 공유한다. glibc 2.27의 tcache는 free() 시 청크 맨 앞(next 포인터)만 덮어쓰고 나머지는 그대로 두기 때문에, 자유로워진 Human의 age 필드가 재할당된 Robot의 함수 포인터(fptr) 자리에 그대로 되살아난다. 일반 one_gadget은 스택 정렬 문제로 전부 실패해서, execve 시스템콜 클러스터의 진입점을 직접 찾아 우회했다. gdb로 age가 fptr로 부활하는 순간을 런타임에서 직접 확인하고, unsorted bin 유출도 /proc의 실제 libc 베이스와 대조해 검증했다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-07
[🥉 Bronze 3] 반환 주소 대신 "열 파일 이름"을 덮는다 — DreamHack bof 풀이
blog

[🥉 Bronze 3] 반환 주소 대신 "열 파일 이름"을 덮는다 — DreamHack bof 풀이

scanf("%144s") 로 스택 버퍼를 넘치는 전형적인 오버플로 문제. 그런데 덮을 것은 반환 주소가 아니다. 버퍼 바로 아래에 read_cat() 이 open() 할 파일명 "./cat" 이 놓여 있어서, 그 자리를 "flag" 로 덮으면 프로그램이 알아서 flag 파일을 열어 출력한다. 버퍼와 파일명 사이 거리 0x80 만 맞추면 끝.
#dreamhack#ctf#pwn+5
 
BLOG2026-07-06
[🥈 Silver 4] 72바이트로 canary 넘어 리크, free_hook 하이재킹으로 쉘 — DreamHack fho 풀이
blog

[🥈 Silver 4] 72바이트로 canary 넘어 리크, free_hook 하이재킹으로 쉘 — DreamHack fho 풀이

read()로 정확히 72바이트만 보내 canary·saved rbp를 non-null로 덮되 return address는 건드리지 않으면, 뒤이은 printf("%s", buf)가 retaddr의 첫 null 바이트에서 멈추며 libc 주소 일부가 그대로 새어나온다. 여기에 문제가 기본 제공하는 임의주소 write/free 프리미티브를 얹어 __free_hook을 system으로 덮고 free("/bin/sh")를 부르면 그대로 쉘이 뜬다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-06
[🥈 Silver 4] free() 이전에 이미 끝난 취약점 — DreamHack hook 풀이
blog

[🥈 Silver 4] free() 이전에 이미 끝난 취약점 — DreamHack hook 풀이

malloc으로 받은 청크의 첫 16바이트를 그대로 주소·값으로 써버리는 한 줄이 문제의 전부다. free()가 두 번 불리지만 실제로는 그 이전 write-what-where 한 방으로 __free_hook을 one_gadget으로 덮고, 첫 free() 호출만으로 셸이 뜬다. double free도, fastbin dup도 필요 없다.
#dreamhack#ctf#pwnable+5
BLOG2026-07-06
[🌱 새싹] strncmp 는 앞 8바이트만 본다 — DreamHack cmd_center 풀이
blog

[🌱 새싹] strncmp 는 앞 8바이트만 본다 — DreamHack cmd_center 풀이

"ifconfig" 로 초기화된 명령 버퍼를 그대로 system() 에 넘기는 바이너리. 바로 옆 24바이트 지역 변수에 read 로 100바이트를 받아주는 탓에, 그 버퍼를 넘쳐서 명령 문자열 뒷부분에 세미콜론으로 원하는 명령을 이어붙일 수 있다. strncmp 가 앞 8글자("ifconfig")만 검사한다는 게 이 문제의 전부다.
#dreamhack#ctf#pwn+5
BLOG2026-07-05
[🥉 Bronze 4] 프로그램이 직접 알려주는 win() 주소 — DreamHack baby-bof 풀이
blog

[🥉 Bronze 4] 프로그램이 직접 알려주는 win() 주소 — DreamHack baby-bof 풀이

printf가 친절하게 win()의 주소를 그대로 출력해주고, 뒤이은 "값을 몇 번 쓸지" 입력은 스택 버퍼 크기와 아무 관계 없이 그 횟수만큼 8바이트씩 이어 쓴다. 리크된 주소를 그 값으로, 쓰기 횟수를 4로 주면 리턴 주소가 win()으로 바뀐다.
#dreamhack#ctf#pwnable+4
 
BLOG2026-07-05
[🥈 Silver 4] 6바이트만 덮어도 충분하다 — DreamHack oneshot 풀이
blog

[🥈 Silver 4] 6바이트만 덮어도 충분하다 — DreamHack oneshot 풀이

16바이트 버퍼에 46바이트를 읽어들이는 전형적인 스택 오버플로우. printf가 무심코 찍어준 stdout 포인터가 libc 주소라는 걸 이용해 베이스를 구하고, 남은 return address 6바이트만 one_gadget으로 덮어 셸을 딴다. rax 제약도 main이 알아서 맞춰준다.
#dreamhack#ctf#pwnable+4
 
BLOG2026-07-04
[🥉 Bronze 4] sprintf가 제가 쓴 글자를 되읽는다 — DreamHack basic_exploitation_003 풀이
blog

[🥉 Bronze 4] sprintf가 제가 쓴 글자를 되읽는다 — DreamHack basic_exploitation_003 풀이

sprintf(stack_buf, heap_buf)에서 heap_buf가 그대로 포맷 문자열이 되는 취약점. stack_buf가 va_arg 스캔 영역과 겹쳐서 %k$ 인자가 "우리가 방금 그 자리에 쓴 값"을 되읽는 자기참조 구조를 실험으로 확인하고, printf@GOT 하위 2바이트만 %hhn으로 덮어 get_shell()로 리다이렉트한다.
#dreamhack#ctf#pwn+5
BLOG2026-07-03
[🥉 Bronze 2] 카나리 leak + GOT leak로 ret2main, 그리고 ret2system — DreamHack rop 풀이
blog

[🥉 Bronze 2] 카나리 leak + GOT leak로 ret2main, 그리고 ret2system — DreamHack rop 풀이

스택 카나리에 NX까지 켜진 클래식 ROP 문제. 입력 기회는 딱 두 번뿐이라 셸코드도, 두 번째 페이로드도 못 넣는다. 카나리를 leak한 뒤 첫 ROP 체인으로 GOT에서 libc 주소를 새고, 반환 주소를 main() 자신으로 되돌려 "세 번째 입력"을 만들어낸 다음 system("/bin/sh")로 마무리한다.
#dreamhack#ctf#pwn+7
BLOG2026-07-02
[🥉 Bronze 2] pop rdi 가젯이 없을 때 — DreamHack basic_rop_x64 ret2csu 풀이
blog

[🥉 Bronze 2] pop rdi 가젯이 없을 때 — DreamHack basic_rop_x64 ret2csu 풀이

canary 없는 스택 BOF에 NX만 켜진 64비트 바이너리. 명시적인 pop rdi;ret 가젯이 없어서 __libc_csu_init 의 레지스터 세팅 루틴(ret2csu)으로 write(1, &GOT[read], 8)을 호출해 libc를 leak하고, "pop r15" 명령 한가운데 숨어 있는 pop rdi;ret로 system("/bin/sh")까지 잇는다.
#dreamhack#ctf#pwn+6
BLOG2026-06-30
[🥉 Bronze 1] 카나리 흘리고 system 으로 되돌아가기 — DreamHack Return to Library 풀이
blog

[🥉 Bronze 1] 카나리 흘리고 system 으로 되돌아가기 — DreamHack Return to Library 풀이

NX 가 켜져 스택 셸코드를 못 쓰는 BOF 문제. 첫 read 뒤 printf("%s") 가 카나리 LSB(\x00) 만 덮으면 상위 7바이트를 흘려준다. 그걸로 카나리를 복원하고, 두 번째 read 오버플로우로 pop rdi → "/bin/sh" → system@plt 로 이어지는 ret2plt 체인을 올려 셸을 띄운다. 바이너리가 처음에 system 을 한 번 호출해 줘서 PLT 가 이미 채워져 있는 게 포인트.
#dreamhack#ctf#pwn+6
BLOG2026-06-27
[🥉 Bronze 2] OOB read 로 카나리 흘려보고 BOF 로 셸 — DreamHack ssp_001 풀이
blog

[🥉 Bronze 2] OOB read 로 카나리 흘려보고 BOF 로 셸 — DreamHack ssp_001 풀이

스택 카나리가 켜진 32비트 BOF 문제. (P)rint 메뉴의 box[idx] 가 인덱스 검사를 안 해 OOB read 가 되고, box 기준 +0x80 위치에 있는 카나리 4바이트를 그대로 읽어올 수 있다. 카나리를 복원한 뒤 (E)xit 메뉴의 read(name, name_len) 길이 무제한 오버플로우로 카나리를 그대로 채우고 반환 주소를 get_shell 로 덮어 셸을 잡는다.
#dreamhack#ctf#pwn+6
BLOG2026-06-26
[🥉 Bronze 3] 카나리 흘려보고 스택에 셸코드 심기 — DreamHack Return to Shellcode 풀이
blog

[🥉 Bronze 3] 카나리 흘려보고 스택에 셸코드 심기 — DreamHack Return to Shellcode 풀이

NX 가 꺼져(스택 RWX) 있지만 스택 카나리가 켜진 BOF 문제. read 뒤의 printf("%s") 가 카나리의 LSB(\x00) 를 덮으면 상위 7바이트를 흘려주고, 그걸로 카나리를 복원한다. 그다음 gets() 무제한 오버플로우로 스택에 셸코드를 올리고 카나리를 그대로 채운 뒤 반환 주소를 buf 로 돌려 ret2shellcode 로 셸을 띄운다.
#dreamhack#ctf#pwn+6
BLOG2026-06-25
[🥉 Bronze 4] 반환 주소를 get_shell 로 덮는다 — DreamHack Return Address Overwrite 풀이
blog

[🥉 Bronze 4] 반환 주소를 get_shell 로 덮는다 — DreamHack Return Address Overwrite 풀이

scanf("%s") 로 스택 버퍼를 넘쳐 반환 주소를 덮는, pwnable 입문의 정석. NX 가 켜져 있어 스택 셸코드는 못 올리지만 바이너리 안에 execve("/bin/sh") 를 부르는 get_shell 함수가 이미 있다. 반환 주소를 그 함수 주소로 덮어 흐름을 가져오는 코드 재사용(ret2win) 기법으로 셸을 띄운다.
#dreamhack#ctf#pwn+5
BLOG2026-06-25
[🥉 Bronze 2] scanf 가 셸코드를 잘라먹는다 — DreamHack basic_exploitation_000 풀이
blog

[🥉 Bronze 2] scanf 가 셸코드를 잘라먹는다 — DreamHack basic_exploitation_000 풀이

buf 주소를 친절히 누출해 주고 scanf 로 스택을 넘치게 하는 전형적인 스택 셸코드 문제. 그런데 교과서 execve 셸코드를 그대로 쓰면 동작하지 않는다. scanf("%s") 가 셸코드 안의 0x0b(SYS_execve) 를 공백으로 보고 거기서 잘라먹기 때문. 0x0b 바이트 없이 11을 만들어 우회하고, 누출된 buf 주소로 점프해 셸을 띄운다.
#dreamhack#ctf#pwn+5
BLOG2026-06-25
[🥉 Bronze 4] gets() 한 줄로 리턴 주소를 read_flag로 — DreamHack basic_exploitation_001 풀이
blog

[🥉 Bronze 4] gets() 한 줄로 리턴 주소를 read_flag로 — DreamHack basic_exploitation_001 풀이

buf[0x80]에 길이 제한 없는 gets()가 입력을 받는다. 128바이트 버퍼에 saved EBP 4바이트를 더한 132바이트 뒤에 리턴 주소가 있고, 그 자리를 system("cat flag")를 부르는 read_flag() 주소로 덮으면 끝이다. 카나리도 PIE도 없어 오프셋과 주소가 그대로 고정인 교과서적 ret2win.
#dreamhack#ctf#pwnable+5
BLOG2026-06-23
[🥉 Bronze 4] execve 만 막힌 seccomp 안에서 ORW 셸코드로 flag 읽기 — DreamHack shell_basic 풀이
blog

[🥉 Bronze 4] execve 만 막힌 seccomp 안에서 ORW 셸코드로 flag 읽기 — DreamHack shell_basic 풀이

RWX 영역에 올린 셸코드를 그대로 실행해 주는데, seccomp 가 execve/execveat 만 KILL 한다. 셸은 못 따니 open-read-write 셸코드로 flag 파일을 직접 읽어 표준출력으로 흘려보낸다. 표준 경로 /home/shell_basic/flag 가 아니라 getdents64 로 디렉토리를 뒤져 진짜 이름을 찾아야 했다.
#dreamhack#ctf#pwnable+5
BLOG2026-06-08
[🥉 Bronze 4] 경계 없는 배열 인덱스로 system 인자 바꿔치기 — DreamHack out_of_bound 풀이
blog

[🥉 Bronze 4] 경계 없는 배열 인덱스로 system 인자 바꿔치기 — DreamHack out_of_bound 풀이

command[idx]에 인덱스 검사가 없어 배열 밖 메모리를 포인터로 읽는다. 마침 command[19]가 우리가 채우는 전역 name 버퍼와 주소가 겹치니, name에 "/bin/sh를 가리키는 포인터"를 심으면 system("/bin/sh")가 그대로 호출된다. NX·카나리·No PIE 환경에서 셸코드도 RIP 덮어쓰기도 없이 셸을 딴다.
#dreamhack#ctf#pwnable+4