说明

Secure Boot,顾名思义就是用来保证启动安全的一套措施。

Secure Boot是一个比较普通的说法,使用的场景也很多,所以这里要特别说明一下,这里指的是UEFI BIOS下的,用来启动诸如Windows、Mac OS之类系统的“Secure Boot”。

Secure Boot最早在《UEFI Spec》2.3.1版本中提出,关于它的具体说明,可以参考下面的网站:

Secure boot | Microsoft Docs

Frequently Asked Questions about Secure Boot

为什么要使用Secure Boot?

首先BIOS执行的过程中可能需要运行一些第三方的程序。比如有一张显卡,其上包含OPROM(就是BIOS下可运行的初始化程序),BIOS为了初始化这张显卡,就会从该显卡处获取到OPROM然后执行,这是一个例子,但是对于这种情况还好,即使不执行显卡的初始化,影响也不大。但是还存在另外的一个东西,是大部分系统启动所必须的,那就是GRUB。通常支持UEFI的系统在安装过程中都会将一个GRUB文件到磁盘的FAT32分区中,然后UEFI BIOS启动时会执行这个文件,并通过这个文件找到系统内核并启动操作系统。

这个GRUB也是一个第三方的程序,关于它可以参考下面的文章:

【GRUB】GRUB2基本操作

【GRUB】GRUB2编译与使用

【GRUB】增加自定义命令

【GRUB】GRUB2代码初步解析

另外,在UEFI Shell下也可以执行第三方的程序,具体可以参考【UEFI基础】UEFI Shell 。

当我们要执行这些第三方程序时,如何保证它们的安全呢?以及当GRUB加载内核时,如何保证内核是安全的呢?

正是因为有了这些顾虑,才引入了Secure Boot。

当开启了Secure Boot功能之后,在执行第三方程序时,都会对它们进行验签,只有保证其数字签名正常,才会将控制权限交给它们。

关于验签、数字签名等概念,稍后会说明。

之后需要说明的是Secure Boot存在的问题。在搜索网站上实际上会看到很多对于“Disable Secure Boot”的咨询,这是因为Secure Boot在保证安全的同时也带来了很多麻烦。比如买了一张新的显卡,但是因为其OPROM没有有效签名,结果卡死在BIOS下面了;又比如想换个系统,结果因为该系统的GRUB没有签名,导致系统起不来了,等等。

关于如何打开和关闭Secure Boot,实际上在BIOS的配置菜单中都提供相应的开关,下面是惠普笔记本的一个例子:

从上图我们也可以看到之前遗留的一个问题,即签名的问题。

除了打开和关闭Secure Boot(Legacy不在我们的讨论中),这里还涉及到key的管理,包括使用微软的key,以及导入自定义的key,实际上在保护Windows启动过程 - Microsoft 365 Security | Microsoft Docs中也有说明:

  • 使用具有认证的启动加载程序的操作系统。 由于所有已认证的 Windows10 电脑必须信任 Microsoft 的证书,Microsoft 提供了一种服务来分析和签署任何非 Microsoft 的引导程序,以便所有认证的 Windows10 电脑能够获得信任。 事实上,已经有了能够加载 Linux 的开源启动加载程序。 若要开始获取证书的过程,请转到Sign in to your account。
  • 将 UEFI 配置为信任自定义的启动加载程序。 所有经认证的 Windows10 电脑都允许你通过将签名添加到 UEFI 数据库来信任未验证的加载程序,从而允许你运行任何操作系统,包括 homemade 操作系统。
  • 关闭安全启动。 所有经认证的 Windows10 Pc 允许您关闭安全启动,以便您可以运行任何软件。 但是,这不会帮助保护你免受 bootkit 的攻击。

默认UEFI支持微软的key,即通过微软签名的第三方程序UEFI会认为是安全的。同时我们也可以自己给第三方程序签名,然后通过使用自己的key来对第三方程序进行验签。

最后的最后,再说明下对于启动Linux的GRUB,事实上现在已经有可以支持安全启动和Linux版本的GRUB,称为shim,可以在mjg59 | Secure Boot bootloader for distributions available now中看到具体的说明。

实现

对于第三方的应用如何进行验签,可以从UEFI Shell下执行UEFI应用开始。

对应的代码如下(位于Shell.c中,参考代码vUDK2017: https://github.com/tianocore/edk2.git Tag vUDK2017.):

        case   Efi_Application://// Get the device path of the application image//DevPath = ShellInfoObject.NewEfiShellProtocol->GetDevicePathFromFilePath(CommandWithPath);if (DevPath == NULL){Status = EFI_OUT_OF_RESOURCES;break;}//// Execute the device path//Status = InternalShellExecuteDevicePath(&gImageHandle,DevPath,CmdLine,NULL,&StartStatus);

该函数内部最重要的两步如下:

  //// Load the image with:// FALSE - not from boot manager and NULL, 0 being not already in memory//Status = gBS->LoadImage(FALSE,*ParentImageHandle,(EFI_DEVICE_PATH_PROTOCOL*)DevicePath,NULL,0,&NewHandle);//// now start the image and if the caller wanted the return code pass it to them...//if (!EFI_ERROR(Status)) {StartStatus      = gBS->StartImage(NewHandle,0,NULL);

但其实这里并没有出现验签相关的代码。还需要追踪到LoadImage函数中,对应的实现在MdeModulePkg\Core\Dxe\Image\Image.c:

EFI_STATUS
EFIAPI
CoreLoadImage (IN BOOLEAN                    BootPolicy,IN EFI_HANDLE                 ParentImageHandle,IN EFI_DEVICE_PATH_PROTOCOL   *FilePath,IN VOID                       *SourceBuffer   OPTIONAL,IN UINTN                      SourceSize,OUT EFI_HANDLE                *ImageHandle)

它的实现中有如下的代码:

  if (gSecurity2 != NULL) {//// Verify File Authentication through the Security2 Architectural Protocol//SecurityStatus = gSecurity2->FileAuthentication (gSecurity2,OriginalFilePath,FHand.Source,FHand.SourceSize,BootPolicy);if (!EFI_ERROR (SecurityStatus) && ImageIsFromFv) {//// When Security2 is installed, Security Architectural Protocol must be published.//ASSERT (gSecurity != NULL);//// Verify the Authentication Status through the Security Architectural Protocol// Only on images that have been read using Firmware Volume protocol.//SecurityStatus = gSecurity->FileAuthenticationState (gSecurity,AuthenticationStatus,OriginalFilePath);}} else if ((gSecurity != NULL) && (OriginalFilePath != NULL)) {//// Verify the Authentication Status through the Security Architectural Protocol//SecurityStatus = gSecurity->FileAuthenticationState (gSecurity,AuthenticationStatus,OriginalFilePath);}

这里涉及到两个Protocol,其实是一个Protocol的两个版本:

EFI_SECURITY_ARCH_PROTOCOL        *gSecurity      = NULL;
EFI_SECURITY2_ARCH_PROTOCOL       *gSecurity2     = NULL;

其中版本1是必须要的,而版本2是可选的:

//
// DXE Core Global Variables for all of the Architectural Protocols.
// If a protocol is installed mArchProtocols[].Present will be TRUE.
//
// CoreNotifyOnArchProtocolInstallation () fills in mArchProtocols[].Event
// and mArchProtocols[].Registration as it creates events for every array
// entry.
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY  mArchProtocols[] = {{ &gEfiSecurityArchProtocolGuid,         (VOID **)&gSecurity,      NULL, NULL, FALSE },//
// Optional protocols that the DXE Core will use if they are present
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY  mOptionalProtocols[] = {{ &gEfiSecurity2ArchProtocolGuid,        (VOID **)&gSecurity2,     NULL, NULL, FALSE },

上述的Protocol通过若干个库、PCD和驱动等组件来实现,如下所示(以BeniPkg为例):

PlatformSecureLib|OvmfPkg/Library/PlatformSecureLib/PlatformSecureLib.inf
TpmMeasurementLib|SecurityPkg/Library/DxeTpmMeasurementLib/DxeTpmMeasurementLib.inf
AuthVariableLib|SecurityPkg/Library/AuthVariableLib/AuthVariableLib.infgUefiOvmfPkgTokenSpaceGuid.PcdSecureBootEnable|TRUE
gEfiSecurityPkgTokenSpaceGuid.PcdOptionRomImageVerificationPolicy|0x00MdeModulePkg/Universal/SecurityStubDxe/SecurityStubDxe.inf {<LibraryClasses>NULL|SecurityPkg/Library/DxeImageVerificationLib/DxeImageVerificationLib.inf}
SecurityPkg/VariableAuthenticated/SecureBootConfigDxe/SecureBootConfigDxe.inf

下面一一介绍这些组件。

组件介绍

本节主要介绍上面提到的支持Secure Boot的组件。

PlatformSecureLib

该库非常简单,只提供了一个接口:

/**This function provides a platform-specific method to detect whether the platformis operating by a physically present user. Programmatic changing of platform security policy (such as disable Secure Boot,or switch between Standard/Custom Secure Boot mode) MUST NOT be possible duringBoot Services or after exiting EFI Boot Services. Only a physically present useris allowed to perform these operations.NOTE THAT: This function cannot depend on any EFI Variable Service since they arenot available when this function is called in AuthenticateVariable driver.@retval  TRUE       The platform is operated by a physically present user.@retval  FALSE      The platform is NOT operated by a physically present user.**/
BOOLEAN
EFIAPI
UserPhysicalPresent (VOID);

如注释所说,这个函数保证对Secure Boot的相关修改必须有实际存在的人进行操作,比如说会弹出一个框,需要接键盘来选择确认,当然这个会根据不同的平台来实现,有些可能根本就不需要特别实现。

TpmMeasurementLib

它也提供了一个接口:

/**Tpm measure and log data, and extend the measurement result into a specific PCR.@param[in]  PcrIndex         PCR Index.@param[in]  EventType        Event type.@param[in]  EventLog         Measurement event log.@param[in]  LogLen           Event log length in bytes.@param[in]  HashData         The start of the data buffer to be hashed, extended.@param[in]  HashDataLen      The length, in bytes, of the buffer referenced by HashData@retval EFI_SUCCESS           Operation completed successfully.@retval EFI_UNSUPPORTED       TPM device not available.@retval EFI_OUT_OF_RESOURCES  Out of memory.@retval EFI_DEVICE_ERROR      The operation was unsuccessful.
**/
EFI_STATUS
EFIAPI
TpmMeasureAndLogData (IN UINT32             PcrIndex,IN UINT32             EventType,IN VOID               *EventLog,IN UINT32             LogLen,IN VOID               *HashData,IN UINT64             HashDataLen);

这个是配合TPM使用的,应该并不是必需的。

AuthVariableLib

它是对变量的扩展,在MdeModulePkg/Universal/Variable/RuntimeDxe/VariableRuntimeDxe.inf中使用到这个库。比如:

  if (mVariableModuleGlobal->VariableGlobal.AuthFormat) {//// Authenticated variable initialize.//mAuthContextIn.StructSize = sizeof (AUTH_VAR_LIB_CONTEXT_IN);mAuthContextIn.MaxAuthVariableSize = mVariableModuleGlobal->MaxAuthVariableSize - GetVariableHeaderSize ();Status = AuthVariableLibInitialize (&mAuthContextIn, &mAuthContextOut);if (!EFI_ERROR (Status)) {DEBUG ((EFI_D_INFO, "Variable driver will work with auth variable support!\n"));mVariableModuleGlobal->VariableGlobal.AuthSupport = TRUE;if (mAuthContextOut.AuthVarEntry != NULL) {for (Index = 0; Index < mAuthContextOut.AuthVarEntryCount; Index++) {VariableEntry = &mAuthContextOut.AuthVarEntry[Index];Status = VarCheckLibVariablePropertySet (VariableEntry->Name,VariableEntry->Guid,&VariableEntry->VariableProperty);ASSERT_EFI_ERROR (Status);}}} else if (Status == EFI_UNSUPPORTED) {DEBUG ((EFI_D_INFO, "NOTICE - AuthVariableLibInitialize() returns %r!\n", Status));DEBUG ((EFI_D_INFO, "Variable driver will continue to work without auth variable support!\n"));mVariableModuleGlobal->VariableGlobal.AuthSupport = FALSE;Status = EFI_SUCCESS;}}

AuthVariableLib是用来增加变量的安全性的。

gUefiOvmfPkgTokenSpaceGuid.PcdSecureBootEnable

这个没有特别好介绍的,因为是OVMF独有的,参考意义不大。

gEfiSecurityPkgTokenSpaceGuid.PcdOptionRomImageVerificationPolicy

Policy是用来配置Secure Boot的。实际上这样的Policy有多个:

[PcdsFixedAtBuild, PcdsPatchableInModule]## Image verification policy for OptionRom. Only following values are valid:<BR><BR>#  NOTE: Do NOT use 0x5 and 0x2 since it violates the UEFI specification and has been removed.<BR>#  0x00000000      Always trust the image.<BR>#  0x00000001      Never trust the image.<BR>#  0x00000002      Allow execution when there is security violation.<BR>#  0x00000003      Defer execution when there is security violation.<BR>#  0x00000004      Deny execution when there is security violation.<BR>#  0x00000005      Query user when there is security violation.<BR># @Prompt Set policy for the image from OptionRom.# @ValidRange 0x80000001 | 0x00000000 - 0x00000005gEfiSecurityPkgTokenSpaceGuid.PcdOptionRomImageVerificationPolicy|0x04|UINT32|0x00000001## Image verification policy for removable media which includes CD-ROM, Floppy, USB and network.#  Only following values are valid:<BR><BR>#  NOTE: Do NOT use 0x5 and 0x2 since it violates the UEFI specification and has been removed.<BR>#  0x00000000      Always trust the image.<BR>#  0x00000001      Never trust the image.<BR>#  0x00000002      Allow execution when there is security violation.<BR>#  0x00000003      Defer execution when there is security violation.<BR>#  0x00000004      Deny execution when there is security violation.<BR>#  0x00000005      Query user when there is security violation.<BR># @Prompt Set policy for the image from removable media.# @ValidRange 0x80000001 | 0x00000000 - 0x00000005gEfiSecurityPkgTokenSpaceGuid.PcdRemovableMediaImageVerificationPolicy|0x04|UINT32|0x00000002## Image verification policy for fixed media which includes hard disk.#  Only following values are valid:<BR><BR>#  NOTE: Do NOT use 0x5 and 0x2 since it violates the UEFI specification and has been removed.<BR>#  0x00000000      Always trust the image.<BR>#  0x00000001      Never trust the image.<BR>#  0x00000002      Allow execution when there is security violation.<BR>#  0x00000003      Defer execution when there is security violation.<BR>#  0x00000004      Deny execution when there is security violation.<BR>#  0x00000005      Query user when there is security violation.<BR># @Prompt Set policy for the image from fixed media.# @ValidRange 0x80000001 | 0x00000000 - 0x00000005 gEfiSecurityPkgTokenSpaceGuid.PcdFixedMediaImageVerificationPolicy|0x04|UINT32|0x00000003

它会在后续模块的具体实现中使用的,用来定义如何处理各类不同的二进制文件。可以从下面的代码中看出来:

  //// Check the image type and get policy setting.//switch (GetImageType (File)) {case IMAGE_FROM_FV:Policy = ALWAYS_EXECUTE;break;case IMAGE_FROM_OPTION_ROM:Policy = PcdGet32 (PcdOptionRomImageVerificationPolicy);break;case IMAGE_FROM_REMOVABLE_MEDIA:Policy = PcdGet32 (PcdRemovableMediaImageVerificationPolicy);break;case IMAGE_FROM_FIXED_MEDIA:Policy = PcdGet32 (PcdFixedMediaImageVerificationPolicy);break;default:Policy = DENY_EXECUTE_ON_SECURITY_VIOLATION;break;}

可以看到对于BIOS本身的二进制FV都是默认执行的;而对于其他的比如OPROM,USB中的Grub等,都需要根据不同的配置来确定如何处理。

SecurityStubDxe.inf

这个是实现Secure Boot的主体驱动,但是其实它也是依赖的一个库的:

!if $(SECURE_BOOT_ENABLE) == TRUEMdeModulePkg/Universal/SecurityStubDxe/SecurityStubDxe.inf {<LibraryClasses>NULL|SecurityPkg/Library/DxeImageVerificationLib/DxeImageVerificationLib.inf}
!elseMdeModulePkg/Universal/SecurityStubDxe/SecurityStubDxe.inf
!endif

所以这里的DxeImageVerificationLib.inf才是重点。后面还会进一步介绍。

SecureBootConfigDxe.inf

这个驱动主要用来提供Setup界面,可以对Secure Boot进行一些必要的配置,正如我们在前面的惠普笔记本的Setup下看到的那样。

下面是vUDK2017这份代码下的Setup界面:

可以看到这里Secure Boot是关闭的。

主体实现

讲到主体实现,就需要回到前面介绍的EFI_SECURITY_ARCH_PROTOCOL和EFI_SECURITY2_ARCH_PROTOCOL的实现。

这里以EFI_SECURITY2_ARCH_PROTOCOL为例,下面是其成员的实现:

EFI_STATUS
EFIAPI
Security2StubAuthenticate (IN CONST EFI_SECURITY2_ARCH_PROTOCOL *This,IN CONST EFI_DEVICE_PATH_PROTOCOL    *File,IN VOID                              *FileBuffer,IN UINTN                             FileSize,IN BOOLEAN                           BootPolicy)
{EFI_STATUS                           Status;if (FileBuffer != NULL) {Status = Defer3rdPartyImageLoad (File, BootPolicy);if (EFI_ERROR (Status)) {return Status;}}return ExecuteSecurity2Handlers (EFI_AUTH_OPERATION_VERIFY_IMAGE | EFI_AUTH_OPERATION_DEFER_IMAGE_LOAD | EFI_AUTH_OPERATION_MEASURE_IMAGE |EFI_AUTH_OPERATION_CONNECT_POLICY, 0, File,FileBuffer, FileSize, BootPolicy);
}

这个实现就是在前面提到的SecurityStubDxe.inf。

ExecuteSecurity2Handlers()的实现中,最重要的是如下的部分:

        Status = mSecurity2Table[Index].Security2Handler (AuthenticationStatus,File,FileBuffer,FileSize,BootPolicy);

这里的mSecurity2Table是一个数组,对应的处理函数就来自DxeImageVerificationLib的注册:

  return RegisterSecurity2Handler (DxeImageVerificationHandler,EFI_AUTH_OPERATION_VERIFY_IMAGE | EFI_AUTH_OPERATION_IMAGE_REQUIRED);

到这里我看可以看到真正做验签的函数就是:

/**Provide verification service for signed images, which include both signature validationand platform policy control. For signature types, both UEFI WIN_CERTIFICATE_UEFI_GUID andMSFT Authenticode type signatures are supported.In this implementation, only verify external executables when in USER MODE.Executables from FV is bypass, so pass in AuthenticationStatus is ignored.The image verification policy is:If the image is signed,At least one valid signature or at least one hash value of the image must match a recordin the security database "db", and no valid signature nor any hash value of the image maybe reflected in the security database "dbx".Otherwise, the image is not signed,The SHA256 hash value of the image must match a record in the security database "db", andnot be reflected in the security data base "dbx".Caution: This function may receive untrusted input.PE/COFF image is external input, so this function will validate its data structurewithin this image buffer before use.@param[in]    AuthenticationStatusThis is the authentication status returned from the securitymeasurement services for the input file.@param[in]    File       This is a pointer to the device path of the file that isbeing dispatched. This will optionally be used for logging.@param[in]    FileBuffer File buffer matches the input file device path.@param[in]    FileSize   Size of File buffer matches the input file device path.@param[in]    BootPolicy A boot policy that was used to call LoadImage() UEFI service.@retval EFI_SUCCESS            The file specified by DevicePath and non-NULLFileBuffer did authenticate, and the platform policy dictatesthat the DXE Foundation may use the file.@retval EFI_SUCCESS            The device path specified by NULL device path DevicePathand non-NULL FileBuffer did authenticate, and the platformpolicy dictates that the DXE Foundation may execute the image inFileBuffer.@retval EFI_OUT_RESOURCE       Fail to allocate memory.@retval EFI_SECURITY_VIOLATION The file specified by File did not authenticate, andthe platform policy dictates that File should be placedin the untrusted state. The image has been added to the fileexecution table.@retval EFI_ACCESS_DENIED      The file specified by File and FileBuffer did notauthenticate, and the platform policy dictates that the DXEFoundation many not use File.**/
EFI_STATUS
EFIAPI
DxeImageVerificationHandler (IN  UINT32                           AuthenticationStatus,IN  CONST EFI_DEVICE_PATH_PROTOCOL   *File,IN  VOID                             *FileBuffer,IN  UINTN                            FileSize,IN  BOOLEAN                          BootPolicy)

至于该函数的实现,就可以直接查看代码来了解,这里面涉及一些HASH算法,所以对于这方面也需要有一定的基础,否则可能代码看上去比较困难。

以上,就是对Secure Boot的介绍。

【UEFI实战】Secure Boot相关推荐

  1. 如何在 Linux 系统启用 UEFI 的 Secure Boot

    如何在 Linux 系统启用 UEFI 的 Secure Boot 概述 Secure Boot 作为 UEFI 的一个选项,它可以被设置为开启或关闭 ( 有少数恶心的计算机里面, Secure Bo ...

  2. 华硕主板禁用UEFI安全启动(Disable Secure Boot for ASUS Motherboard)

    新人第一次写博客,算是搬运google的答案吧,因为百度了两三个小时一直没解决问题,google了一次出来一个youtube视频便解决了,有种说不出来的郁闷( ̄﹏ ̄:). 先介绍下背景,最近配了台新电 ...

  3. linux secure boot(安全启动)下为内核模块签名

    文章目录 linux secure boot(安全启动)下为内核模块签名 背景 Secure Boot安全启动开启关闭方法 内核驱动签名 生成签名证书和私钥 导入签名证书 BIOS(UEFI)导入证书 ...

  4. UEFI、BIOS、Secure Boot的关系和知识介绍

    从Windows 8操作系统时代开始,安装操作系统的方法也有了很大的改变,Windows 8采用了Secure Boot引导启动的方式,而不是过去Win XP和Win 7的Legacy启动方式,从而导 ...

  5. UEFI Secure Boot学习草稿(quqi99)

    作者:张华 发表于:2020-09-29 版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明 什么是secure boot secureboot is design ...

  6. UEFI secure boot(3)- 安全引导的实现

    1.先决条件 1)固件必须支持UEFI和SecureBoot. 2)固件必须支持Setup Mode,或至少清除密钥. 3)一个PKI,以及一种管理(生成和签名)您的X.509证书的方法. 2.生成及 ...

  7. 电脑无法从USB启动可能是 UEFI Secure Boot 在捣鬼!

    现在的主板很多都带有 UEFI Secure Boot 功能,可能导致使用USB设备启动不了系统. 需修改BIOS设置: (1)Security 中的 Secure Boot 设置为"Dis ...

  8. [Windows_UEFI BIOS]详解 Secure Boot 和 Winows 8 及 UEFI启动 的关系

    一.自由软件基金会的呼吁 上周,2012年将近结束的时候,自由软件基金会(FSF)发出呼吁,要求人们继续支持反Secure Boot垄断,希望签名者能达到5万人(目前是4万). 我觉得,这个呼吁很重要 ...

  9. (转)详解 Secure Boot 和 Winows 8 及 UEFI启动的关系

    很多同学都发现了,在安装Windows 8或是带UEFI启动的电脑时要更改BIOS里的Secure Boot值!比如戴尔的INS14R-5420,INS15R-5520,INS14R-5421,INS ...

最新文章

  1. python使用matplotlib可视化、使用annotate函数以及arrowprops参数在可视化图像中添加箭头和文本注释(arrow and text annotation)
  2. APACHE利用Limit模块限制IP连接数
  3. 用英语说中国 IOS APP 上线
  4. 网络推广公司浅析网站栏目页该如何优化?
  5. what can be learned from a friend?
  6. 1号店Interview小结
  7. android下关闭软键盘
  8. 给SAP云平台的global账号添加Leonardo机器学习服务
  9. linux概述、基本命令
  10. read函数头文件 window_of_property_read_string 剖析
  11. java json 工具类_Java中JSON处理工具类使用详解
  12. java中ajax的用途_java Ajax的应用
  13. alwayson高可用组_AlwaysOn可用性组–好奇心使您的工作更轻松–第2部分
  14. Redis中SDS简单动态字符串
  15. 华为云SSL证书申请流程
  16. Akka Actor模型的简介与Actor的创建方式
  17. 深富策略核心资产崩了
  18. 9.1.4 用 send2trash 模块安全地删除
  19. 租用游戏服务器的优势
  20. 高分难题,绝对有难度

热门文章

  1. 爱奇艺奇秀直播人气挂核心原理(写爬虫的都来看看)
  2. 什么是第三代搜索引擎
  3. 决策树①——信息熵信息增益基尼系数
  4. Android手机获取相机权限终极大招(兼容国产手机小米华为魅族以及6.0以下系统)
  5. 通过Redis限制API调用次数
  6. C. Ivan the Fool and the Probability Theory------思维+dp
  7. HFSS仿真平行线定向耦合器学习笔记
  8. Struts2 入门教程 HelloWorld示例
  9. 数据接口异常中的错误
  10. python使用nltk库中的download()下载无法使用