Windows内核函数

字符串处理

在驱动中一般使用的是ANSI字符串和宽字节字符串,在驱动中我们仍然可以使用C中提供的字符串操作函数,但是在DDK中不提倡这样做,由于C函数容易导致缓冲区溢出漏洞,针对字符串的操作它提供了一组函数分别用来处理ANSI字符串和UNICODE字符串。
针对两种字符串,首先定义了它们的结构体

typedef struct _STRING {USHORT  Length;//字符串的长度USHORT  MaximumLength;//字符缓冲的长度PCHAR  Buffer;//字符缓冲的地址
} ANSI_STRING, *PANSI_STRING;typedef struct _UNICODE_STRING {USHORT  Length;USHORT  MaximumLength;PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

对于这两个字符串的打印,可以使用%wZ打印UNICODE_STRING用%Z打印ANSI_STRING

字符串的初始化

VOID RtlInitAnsiString(IN OUT PANSI_STRING  DestinationString,IN PCSZ  SourceString);VOID RtlInitUnicodeString(IN OUT PUNICODE_STRING  DestinationString,IN PCWSTR  SourceString);

这两个函数只是简单的将SourceString 的首地址赋值给Buffer成员,并初始化相关的长度,所以在使用时需要考虑缓冲的生命周期,权限,同时如果我们改变SourceString 里面存储的字符串,那么对应的UNICODE_STRING 或者ANSI_STRING中的值也会改变,比如下面的代码

RtlInitUnicodeString(&uTest, L"Hello World");
RtlCopyMemory(uTest.Buffer, L"Test");

由于Buffer指向的是不可修改的常量内存部分,所以后面试图修改它的时候会造成程序崩溃。

void InitString(&pUnicodeString)
{WCHAR szBuf[255] = L"Hello world";RtlInitUnicodeString(pUnicodeString, szBuffer);
} void test()
{UNICODE_STRING uTest;InitString(&uTest);//后面的操作
}

我们在另外一个函数中利用局部变量来初始化这个字符串的时候由于当函数调用完成,函数中局部变量被销毁,这个时候指向的那块内存可能已经被其他函数所占用,而我们后面通过操作UNICODE_STRING,又要操作这段内存,这个时候一定会出现问题,所以一般如果要在多个函数中使用这个UNICODE_STRING时一般申请一段堆内存,但是在使用完成后一定要记得自己回收这段内存,否则会造成内存泄露,对此DDK专门提供了一组函数来销毁字符串中的堆内存

VOID RtlFreeAnsiString(IN PANSI_STRING  AnsiString);VOID RtlFreeUnicodeString(IN PUNICODE_STRING  UnicodeString);

字符串拷贝:

VOID RtlCopyString(IN OUT PSTRING  DestinationString,IN PSTRING  SourceString  OPTIONAL);VOID RtlCopyUnicodeString(IN OUT PUNICODE_STRING  DestinationString,IN PUNICODE_STRING  SourceString);

字符串比较

LONG RtlCompareString(IN PSTRING  String1,IN PSTRING  String2,BOOLEAN  CaseInSensitive//是否忽略大小写);LONG RtlCompareUnicodeString(IN PUNICODE_STRING  String1,IN PUNICODE_STRING  String2,IN BOOLEAN  CaseInSensitive);

字符串转化为大写

VOID RtlUpperString(IN OUT PSTRING  DestinationString,IN PSTRING  SourceString);NTSTATUS RtlUpcaseUnicodeString(IN OUT PUNICODE_STRING  DestinationString,IN PCUNICODE_STRING  SourceString,IN BOOLEAN  AllocateDestinationString//是否要求该函数自行为输出参数分配内存);

这两个函数在调用是目标字符串和源字符串可以是同一个字符串
字符串与整形数字之间的转化可以使用函数

NTSTATUSRtlUnicodeStringToInteger(IN PUNICODE_STRING  String,IN ULONG  Base  OPTIONAL,//需要的数的进制OUT PULONG  Value);NTSTATUS RtlIntegerToUnicodeString(IN ULONG  Value,IN ULONG  Base  OPTIONAL,IN OUT PUNICODE_STRING  String);

ANSI与UNICODE字符串的相互转化可以使用下面的函数

NTSTATUS RtlUnicodeStringToAnsiString(IN OUT PANSI_STRING  DestinationString,IN PUNICODE_STRING  SourceString,IN BOOLEAN  AllocateDestinationString);NTSTATUS RtlAnsiStringToUnicodeString(IN OUT PUNICODE_STRING  DestinationString,IN PANSI_STRING  SourceString,IN BOOLEAN  AllocateDestinationString);

文件操作

创建或者打开一个文件

文件的创建和打开都是使用函数ZwCreateFile

NTSTATUS  ZwCreateFile(OUT PHANDLE  FileHandle,IN ACCESS_MASK  DesiredAccess,IN POBJECT_ATTRIBUTES  ObjectAttributes,OUT PIO_STATUS_BLOCK  IoStatusBlock,IN PLARGE_INTEGER  AllocationSize  OPTIONAL,IN ULONG  FileAttributes,IN ULONG  ShareAccess,IN ULONG  CreateDisposition,IN ULONG  CreateOptions,IN PVOID  EaBuffer  OPTIONAL,IN ULONG  EaLength);
  1. FileHandle:这个函数通过这个参数返回文件句柄
  2. DesiredAccess:以何种权限打开或者创建这个文件,GENERIC_READ可读,GENERIC_WRITE可写,GENERIC_EXECUTE可执行,GENERIC_ALL所有权限
  3. ObjectAttributes:这是一个文件属性的结构体,里面包含有要打开的文件的名称
  4. IoStatusBlock:接受函数操作文件的结果状态
  5. AllocationSize:指定在创建爱女或者写文件时初始大小,如果给0,则文件大小会随着写入数据的增加而动态的增加
  6. FileAttributes:指定新创建文件的属性,一般给0或者FILE_ATTRIBUTE_NORMAL
  7. ShareAccess:文件的共享权限,其他线程或者进程通过这个句柄访问文件的权限,给0表示不允许其他进程通过这个句柄访问,FILE_SHARE_READ读, FILE_SHARE_WRITE写,FILE_SHARE_DELETE删除
  8. CreateDisposition:指定当文件存在或者不存在时这个函数的动作。它的取值可以有下面几个
取值文件存在文件不存在
FILE_SUPERSEDE新建一个文件替代新建文件
FILE_CREATE返回一个错误创建文件
FILE_OPEN打开文件返回一个错误
FILE_OPEN_IF打开文件创建文件
FILE_OVERWRITE打开,并且将之前的内容覆盖返回错误
FILE_OVERWRITE_IF打开,并且将之前的内容覆盖创建文件

9. CreateOptions打开或者创建文件时的附加操作,一般给FILE_SYNCHRONOUS_IO_NONALERT
10. EaBuffer指向扩展空间的指针
11. EaLength扩展空间的大小
这个函数与应用层的CreateFile不同的时,在指定打开或者创建文件名时是使用结构OBJECT_ATTRIBUTES来指定,针对这个结构,有一个函数能够初始化它

VOID InitializeObjectAttributes(OUT POBJECT_ATTRIBUTES  InitializedAttributes,IN PUNICODE_STRING  ObjectName,//文件名IN ULONG  Attributes,IN HANDLE  RootDirectory,IN PSECURITY_DESCRIPTOR  SecurityDescriptor);

Attributes:该对象的描述信息,一般给OBJ_CASE_INSENSITIVE 表示对大小写敏感
RootDirectory :该文件的根目录,一般给NULL
SecurityDescriptor :安全描述符,一般也是给NULL
另外这里的名称必须使用符号链接名或者设备名,而不是我们熟悉的“C:\”这种形式对于C盘可以使用名称“\??\C”或者“\Device\HarddiskVolum1”这种形式
当程序结束时需要调用ZwClose来清理文件句柄这个函数的参数比较简单,只是简单的传入文件句柄即可

获取和设置文件的相关信息

可以下面两个函数分别获取和设置文件的相关信息

NTSTATUS ZwQueryInformationFile(IN HANDLE  FileHandle,OUT PIO_STATUS_BLOCK  IoStatusBlock,OUT PVOID  FileInformation,IN ULONG  Length,IN FILE_INFORMATION_CLASS  FileInformationClass);NTSTATUS ZwSetInformationFile(IN HANDLE  FileHandle,OUT PIO_STATUS_BLOCK  IoStatusBlock,IN PVOID  FileInformation,IN ULONG  Length,IN FILE_INFORMATION_CLASS  FileInformationClass);

其中FileInformationClass是一个枚举值,根据这个值得不同FileInformation可以被解析成不同的内容。
1. 当这个参数为FileStandardInformation时,使用结构体FILE_STANDARD_INFORMATION

typedef struct FILE_STANDARD_INFORMATION {LARGE_INTEGER  AllocationSize; //为文件分配簇所占空间的大小LARGE_INTEGER  EndOfFile;//距离文件结尾还有多少字节,当文件指针位于文件头时,这个值就是文件本身大小ULONG  NumberOfLinks;//有多少个链接文件BOOLEAN  DeletePending;//是否准备删除BOOLEAN  Directory;//是否为目录
} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;
  1. 当这个参数为FileBasicInformation使用结构体FILE_BASIC_INFORMATION
typedef struct FILE_BASIC_INFORMATION {LARGE_INTEGER  CreationTime; //创建时间LARGE_INTEGER  LastAccessTime;//上次访问时间LARGE_INTEGER  LastWriteTime;//上次写文件时间LARGE_INTEGER  ChangeTime;//上次修改时间ULONG  FileAttributes;//文件属性
} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;

其中时间参数是一个LARGE_INTEGER类型的整数,代表从1601年到现在经过多少个100ns。文件属性参数如果为FILE_ATTRIBUTE_DIRECTORY表示这是一个目录文件,FILE_ATTRIBUTE_NORMAL表示是一个普通文件,FILE_ATTRIBUTE_HIDDEN表示这是一个隐藏文件,FILE_ATTRIBUTE_SYSTEM表示这是一个系统文件,FILE_ATTRIBUTE_READONLY表示这是一个只读文件
3. 当这个参数为FileNameInformation时,使用结构体FILE_NAME_INFORMATION

typedef struct _FILE_NAME_INFORMATION {ULONG  FileNameLength;//文件名长度WCHAR  FileName[1];//文件名
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;
  1. 当这个参数是FilePositionInformation时,使用结构体FILE_POSITION_INFORMATION
typedef struct FILE_POSITION_INFORMATION {LARGE_INTEGER  CurrentByteOffset;//当前文件指针的位置
} FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;

读写文件

写文件调用函数ZwCreateFile

NTSTATUS ZwWriteFile(IN HANDLE  FileHandle,//文件句柄IN HANDLE  Event  OPTIONAL,//时间对象一般给NULLIN PIO_APC_ROUTINE  ApcRoutine  OPTIONAL,//一般给NULLIN PVOID  ApcContext  OPTIONAL,//一般给NULLOUT PIO_STATUS_BLOCK  IoStatusBlock,//记录写操作的状态用里面的Information成员记录实际写了多少字节IN PVOID  Buffer,//写入文件中缓冲区的指针IN ULONG  Length,//缓冲区中数据的长度IN PLARGE_INTEGER  ByteOffset  OPTIONAL,//从文件的多少地址开始写IN PULONG  Key  OPTIONAL//一般给NULL);

读文件使用函数ZwReadFile

NTSTATUS ZwReadFile(IN HANDLE  FileHandle,//文件句柄IN HANDLE  Event  OPTIONAL,//一般给NULLIN PIO_APC_ROUTINE  ApcRoutine  OPTIONAL,//一般给NULLIN PVOID  ApcContext  OPTIONAL,//一般给NULLOUT PIO_STATUS_BLOCK  IoStatusBlock, //读取的字节数保存在结构的成员Information中OUT PVOID  Buffer,//缓冲区的指针IN ULONG  Length,//缓冲区的长度IN PLARGE_INTEGER  ByteOffset  OPTIONAL,//从文件的多少位置开始读IN PULONG  Key  OPTIONAL//一般给NULL);

注册表操作

注册表中有下面几个概念:
1. 注册表项:注册表项类似于目录的概念,下面可以有子项或者注册表的键-值对
2. 注册表子项:类似于子目录的概念
3. 键名:通过键名可以寻找到相应的键值
4. 键值类别:每个键值在存储的时候有不同的类型,相当于变量的类型,主要有字符串和整型
5. 键值:键名下对应存储的数据
这里写图片描述

创建和关闭注册表

创建注册表使用函数ZwCreateKey

NTSTATUS ZwCreateKey(OUT PHANDLE  KeyHandle,IN ACCESS_MASK  DesiredAccess,IN POBJECT_ATTRIBUTES  ObjectAttributes,IN ULONG  TitleIndex,IN PUNICODE_STRING  Class  OPTIONAL,IN ULONG  CreateOptions,OUT PULONG  Disposition  OPTIONAL);
  1. KeyHandle:输出一个注册表对应项的句柄,以后针对这个项操作都是以这个句柄作为标示
  2. DesiredAccess:访问权限,一般都设置为KEY_ALL_ACCESS
  3. ObjectAttributes:用法与文件操作中的用法相同
    其中应用层中注册表项与内核中注册表项的对应关系如下:
应用层中的子健内核中的路径
HKEY_CLASSES_ROOT没有对应的路径
HKEY_CURRENT_USER没有简单的对应路径,但是可以求得
HKEY_USERS\Registry\User
HKEY_LOCAL_MACHINE\Registry\Machine

4. TitleIndex:一般设置为0
5. Class 一般给NULL
6. CreateOptions:创建选项,一般给REG_OPTION_NON_VOLATILE
7. Disposition:返回创建的状态,如果是REG_CREATED_NEW_KEY表示创建了一个新的注册表项如果是REG_OPENED_EXISTING_KEY表示打开一个已有的注册表项
8. ### 添加、修改注册表键
注册表中的键是类似与字典中的键值对,通过键名找到对应的值,键值的类型大致可以分为下面几种

分类描述
REG_BINARY键值采用二进制存储
REG_SZ键值用宽字符串,以\0结尾
REG_EXPAND_SZ与上面的REG_SZ相同,它是上面那个字符串的扩展字符
REG_MULTI_SZ能够存储多个字符串,每个都以\0隔开
REG_DWORD键值用4字节整型存储(这个类型的数据在驱动中使用ULONG来替代)
REG_QWORD键值用8字节存储(这个用LONGLONG)

用函数ZwSetValueKey可以添加和修改注册表的一项内容

 NTSTATUS ZwSetValueKey(IN HANDLE  KeyHandle, //注册表句柄IN PUNICODE_STRING  ValueName,//要修改或者新建的键名IN ULONG  TitleIndex  OPTIONAL,//一般设置为0IN ULONG  Type,//在上面的表中选择一个IN PVOID  Data,//键值IN ULONG  DataSize//键值数据的大小);

当传入的键值不存在则创建一个新键值,否则就修改原来的键值

查询注册表

查询注册表使用函数ZwQueryValueKey

NTSTATUS ZwQueryValueKey(IN HANDLE  KeyHandle, //注册表句柄IN PUNICODE_STRING  ValueName,//注册表键名IN KEY_VALUE_INFORMATION_CLASS  KeyValueInformationClass,OUT PVOID  KeyValueInformation,//接收返回信息的缓冲区IN ULONG  Length,//缓冲区的大小OUT PULONG  ResultLength//真实缓冲区的大小);

使用这个函数时利用参数KeyValueInformationClass来指定接收数据的类型,根据这个值的不同,函数会返回不同的结构体放到一个缓冲区中。一般这个值可取:KeyValueBasicInformation 返回注册表项的基础信息
KeyValueFullInformation 返回注册表的全部信息
KeyValuePartialInformation 返回注册表的部分信息
一般情况下使用KeyValuePartialInformation查询键值数据
利用这个函数来查询时一般也是采用两次调用的方式,第一次返回数据所需缓冲,然后分配缓冲并进行第二次调用

枚举子项

DDK提供了两个函数用于这个功能

NTSTATUS ZwQueryKey(IN HANDLE  KeyHandle,//注册表句柄IN KEY_INFORMATION_CLASS  KeyInformationClass,//保存注册表信息的结构体的类型OUT PVOID  KeyInformation,//返回查询到信息的缓冲IN ULONG  Length,//缓冲的大小OUT PULONG  ResultLength//真正信息的大小);NTSTATUS ZwEnumerateKey(IN HANDLE  KeyHandle,//句柄IN ULONG  Index,//这个值是表示第几个子项IN KEY_INFORMATION_CLASS  KeyInformationClass,//查询到的信息的结构体OUT PVOID  KeyInformation,//返回信息的缓冲IN ULONG  Length,//缓冲长度OUT PULONG  ResultLength//返回信息的长度);

其中ZwQueryKey函数用于查询某个注册表项中有多少个子项,在调用这个函数时传入的KeyInformationClass的值一般给KeyFullInformation,在这个结构体中的SubKeys表示有多少个子项,而ZwEnumerateKey则是用于查询各个子项中的具体内容,通过指定Index表示我们要查询该项中的第几个子项,将KeyInformationClass填入KeyBasicInformation,这样在结构体的Name里面可以得到具体的注册表子项的名称

枚举子健

枚举子键的方法于上面的大致相同,首先利用ZwQueryKey查询注册表,然后取结构体KeyFullInformation的成员Values,根据这个值在循环中依次调用函数ZwEnumerateValueKey,结构体类填入 KeyValueBasicInformation查询基本信息即可

删除子项

删除子项使用的内核函数是ZwDeleteKey

NTSTATUS ZwDeleteKey(IN HANDLE  KeyHandle);

这个函数只能删除没有子项的项目,如果有子项,则需要先删除所有子项。

其他注册表函数

为了简化注册表操作,DDK提供了另外一组以Rtl开头的函数,把之前的Zw函数进行了封装,下面是这些函数与它们功能的对应关系

函数名描述
RtlCreateRegistryKey创建注册表项
RtlCheckRegistryKey查看注册表中的某项是否存在
RtlWriteRegistryValue写注册表
RtlDeleteRegistryValue删除注册表的子键

转载于:https://www.cnblogs.com/lanuage/p/7725717.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/354664.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端应该关注的2021年UI设计趋势

UI设计趋势几乎每年都在发生变化,变化的原因是人们的审美在变导致的,还是设计越来越人性化。市场上是谁在主导设计趋势?其中原因不得而知,我们先看看究竟有哪些变化:1. 3D插图(依然流行)3D图像将…

如何让你在开发者工具中查看源代码有语法高亮和暗黑主题的效果

如何让你在Chrome浏览器开发者工具中查看源代码的时候,和在代码编辑器中有同样的代码语法高亮的效果,而且还是深色主题,如果你是深色主题的爱好者就更合你意了。国外的美女开发者为你实现了这样功能的浏览器拓展,她的Github主页&a…

Hitting refresh on my career(译)----重新定义我的事业

我是在PlanetXamarin这个网站上,看到的这篇文章,看完后,觉得挺能引起人的思考的,特别是我们广大程序员. "Hit Refresh"这两个单词,字面上理解:点击刷新按钮. 国内有人翻译为"拥抱变革",我觉得“拥…

冒泡排序 快速排序 插入排序 选择排序

最近面试好多公司考察算法,特意整理了一下: 1. 冒泡排序思路分析:在要排序的一组数中,对当前还未排好的序列,从前往后对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即&…

“太空语言”JavaScript编码标准规范指南

喷气推进实验室是 美国国家航空航天局的科研机构。 该实验室JPL开发大部分的软件是用在无人深度太空和其他行星探测的领域。他们拥有著名的 好奇号火星探测器 和 旅行者号探测器 。已经离开太阳系25年,仍然在飞行并提供科学信息。高水平的自动化和长期的任务导致了对…

如何在JUnit 5中替换规则

最近发布的JUnit 5(又名JUnit Lambda) Alpha版本引起了我的兴趣,在浏览文档时,我注意到规则以及跑步者和阶级规则都消失了。 根据文档,这些部分竞争的概念已被单个一致的扩展模型取代。 多年来, Frank和我…

sikuli 搜索例子

#coding:utf-8kw input(u"请输入您要搜索的关键字:")#openAPP(‪C:\Users\ceshi\AppData\Local\Google\Chrome\Application\chrome.exe)doubleClick("asm.png")wait("fEDi.png")type(Key.CTRL)type(www.baidu.com)type(Key.TAB)sleep(1)type(kw…

微页面设计开发指南

一、目标实现左侧:为可用的组件列表,可拖动任一组件到中间的预览区域中间:为页面预览效果页面,选中任一组件,可在右侧进行参数配置右侧:为组件的参数配置(选中中间的组件时出现)&…

商城商品购买数量增减的完美JS效果

商城商品购买数量增减的完美JS效果 近期在开发一个地方O2O租书项目,使用ASP.NET MVC技术,其中在图书详情页,用户可以输入借阅的数量,这里使用了js来控制数量的增减和校验。 数量一定是数字 点击增减按钮的时候要能自动加1或减1 …

这款插件让你在VSCode上也能答题背单词

在VSCode上也可以在线答题了,插件市场上线了一款答题的插件,免去了去其它网站或者软件的烦恼,代码写累了,随手打开答题功能,换换脑子,或者熟悉两个单词,程序员的别样休闲时光,哈哈&a…

java jaas_基于Java JAAS表单的身份验证

java jaas使用JAAS实现登录模块是一个高级主题,而且大多数开发人员也很少有机会参与这种开发。 但是JAAS登录模块的基本实现不是那么难实现,这是因为我打算将其发布。 在这里,我正在解释如何实现tomcat管理的身份验证模块。 此实现与容器无关…

在ubuntu上如何将多张图片或PDF合到一个PDF上

在日常工作中我们可能需要将不同的PDF或图像合并为一个PDF上,这种情况并不少见。 这时候我们需要学习使用一个非常强大的命令行工具ImageMagick。通过apt命令下载安装ImageMagick在Ubuntu 20.04版本中已经预装了ImageMagick,因为有许多软件包使用此工具作…

使用Java 8在地图上流式传输

在本文中,我将向您展示如何在标准Java映射上有效地实现Speedment Open Source流,并将Stream接口扩展为MapStream! 即使在复杂的情况下,此添加将使保持流的具体性和可读性变得更加容易。 希望这将允许您继续流式传输而不会过早收集…

日期差值 1096

问题描述 链接:https://www.nowcoder.com/questionTerminal/ccb7383c76fc48d2bbc27a2a6319631c 有两个日期,求两个日期之间的天数,如果两个日期是连续的我们规定他们之间的天数为两天 输入描述: 有多组数据,每组数据有两行&#x…

如何使用python给PDF文件加水印

Python作为编程界最火的语言,能做的事几乎你能想到的它都能干,就连抢茅台都可以,还有什么不行?!Python作为脚本编程语言,可以做很多事情。使用Python,你可以轻松地给pdf加上水印。 你可以使用名…

搭建一个redis高可用系统

一、单个实例 当系统中只有一台redis运行时,一旦该redis挂了,会导致整个系统无法运行。 单个实例二、备份 由于单台redis出现单点故障,就会导致整个系统不可用,所以想到的办法自然就是备份(一般工业界认为比较安全的备…

建设者还是二传手?

不用说,每个对象都需要先创建才能使用。 无论我们是在谈论域,框架,库还是任何其他类型的类,都没有关系。 当您的代码是面向对象的时,这些类仅是对象的定义。 在创建对象之前,不能使用它们。 在谈论对象的初…

SSH连接远程服务器,本地known_hosts文件记录了什么

今天工作时,使用ssh命令远程连接公司的本地服务器时,突然出现以下错误bash-3.2$ ssh argus192.168.200.8 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdroppin…

“全人类的知识宝藏”维基百科迎来了20岁的生日!

维基百科从一个伟大的想法开始,与无数的像你像我一样的阅读者,创作者,捐赠者和粉丝经历了互联网的20年,今天让我们一起为这个属于所有互联网人的成果庆祝一次生日。值此20周年特地为它做了一个主页:https://wikimediaf…

转:智能音箱市场深度报告:怎么大家都在抢这个两亿小蛋糕?

原文链接:http://www.sohu.com/a/199335366_115978 智能音箱是今年最热的智能硬件项目之一。目前,智能音箱已经有了比较成熟的技术方案和模式思路,但消费市场似乎依然秉持着比较谨慎的态度。智能音箱市场上的主流产品都有什么思路&#xff1f…