Step to UEFI (195)谁动了我的 RET

前面的文章“EFI 文件研究(1)”提到了入口地方有一个奇怪的现象,直接从ProcessLibraryConstructorList 函数跳到了ProcessModuleEntryPointList。百思不得其解之后咨询天杀,他提到有编译器有一种优化方式,针对“连续两个函数的调用 ,在优化后可能会将第一个函数的尾部返回优化成对第二个函数的跳转,然后由第二个函数来进行返回”。经过这样的提醒后,我在代码中查找,找到了非常类似的代码。

在\MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c 定义了Application 的入口:

/**
  Entry point to UEFI Application.

  This function is the entry point for a UEFI Application. This function must call
  ProcessLibraryConstructorList(), ProcessModuleEntryPointList(), and ProcessLibraryDestructorList().
  The return value from ProcessModuleEntryPointList() is returned.
  If _gUefiDriverRevision is not zero and SystemTable->Hdr.Revision is less than _gUefiDriverRevison,
  then return EFI_INCOMPATIBLE_VERSION.

  @param  ImageHandle                The image handle of the UEFI Application.
  @param  SystemTable                A pointer to the EFI System Table.

  @retval  EFI_SUCCESS               The UEFI Application exited normally.
  @retval  EFI_INCOMPATIBLE_VERSION  _gUefiDriverRevision is greater than SystemTable->Hdr.Revision.
  @retval  Other                     Return value from ProcessModuleEntryPointList().

**/
EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS                 Status;

  if (_gUefiDriverRevision != 0) {
    //
    // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
    //
    if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
      return EFI_INCOMPATIBLE_VERSION;
    }
  }
  //
  // Call constructor for all libraries.
  //
  ProcessLibraryConstructorList (ImageHandle, SystemTable);
  //
  // Call the module's entry point
  //
  Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);

  //
  // Process destructor for all libraries.
  //
  ProcessLibraryDestructorList (ImageHandle, SystemTable);

  //
  // Return the return status code from the driver entry point
  //
  return Status;
}

因此,前面_gUefiDriverRevision == 0 在编译期内部代码直接会被优化掉,剩下的就是连续两次调用ProcessLibraryConstructorList 和ProcessModuleEntryPointList 函数。

为了证明这一点,我在 Inf 文件中加入关闭优化的指令 /Od:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /FAsc /Od

加入之后再次编译,

在 \Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\ApplicationEntryPoint.cod 中可以看到完整的图景:

_ModuleEntryPoint PROC					; COMDAT

; 45   : {

$LN25:
  00000	48 89 5c 24 08	 mov	 QWORD PTR [rsp+8], rbx
  00005	48 89 74 24 10	 mov	 QWORD PTR [rsp+16], rsi
  0000a	57		 push	 rdi
  0000b	48 83 ec 20	 sub	 rsp, 32			; 00000020H
  0000f	48 8b fa	 mov	 rdi, rdx
  00012	48 8b f1	 mov	 rsi, rcx

; 46   :   EFI_STATUS                 Status;
; 47   : 
; 48   :   if (_gUefiDriverRevision != 0) {
; 49   :     //
; 50   :     // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
; 51   :     //
; 52   :     if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
; 53   :       return EFI_INCOMPATIBLE_VERSION;
; 54   :     }
; 55   :   }
; 56   : 
; 57   :   //
; 58   :   // Call constructor for all libraries.
; 59   :   //
; 60   :   ProcessLibraryConstructorList (ImageHandle, SystemTable);

  00015	e8 00 00 00 00	 call	 ProcessLibraryConstructorList

; 61   : 
; 62   :   //
; 63   :   // Call the module's entry point
; 64   :   //
; 65   :   Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);

  0001a	48 8b d7	 mov	 rdx, rdi
  0001d	48 8b ce	 mov	 rcx, rsi
  00020	e8 00 00 00 00	 call	 ProcessModuleEntryPointList

; 66   : 
; 67   :   //
; 68   :   // Process destructor for all libraries.
; 69   :   //
; 70   :   ProcessLibraryDestructorList (ImageHandle, SystemTable);

  00025	48 8b d7	 mov	 rdx, rdi
  00028	48 8b ce	 mov	 rcx, rsi
  0002b	48 8b d8	 mov	 rbx, rax
  0002e	e8 00 00 00 00	 call	 ProcessLibraryDestructorList

; 71   : 
; 72   :   //
; 73   :   // Return the return status code from the driver entry point
; 74   :   //
; 75   :   return Status;
; 76   : }

  00033	48 8b 74 24 38	 mov	 rsi, QWORD PTR [rsp+56]
  00038	48 8b c3	 mov	 rax, rbx
  0003b	48 8b 5c 24 30	 mov	 rbx, QWORD PTR [rsp+48]
  00040	48 83 c4 20	 add	 rsp, 32			; 00000020H
  00044	5f		 pop	 rdi
  00045	c3		 ret	 0
_ModuleEntryPoint ENDP

这里可以清楚的看到分别调用了2个函数,并且和我们上面找到的位置代码是一致的。

从上面的试验可以得知:

  1. _ModuleEntryPoint ()是每个 Application 的起点,如果有需要可以在其中添加代码;
  2. 编译器有时候会将连续的2个函数调用优化。

Step to UEFI (194)EFI 文件研究(1)

EFI 文件使用的是 PE 格式(PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件【参考1】),所以很多关于PE文件的知识在 EFI 文件上仍然是通用的。

我们编写一个代码来进行研究。功能非常简单,如果运行时加入 a 参数,那么打印一串字符,否则无任何动作。具体代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        if ((Argc>1)&&(Argv[1][0]=='a')) {
                Print(L"Hello there fellow Programmer.\n");
        }
  
  return(0);
}

此外在对应的 inf 文件加入下面一段:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /FAsc

编译之后,可以在\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\DEBUG目录下看到 es.efi 和 es.map 文件。打开 es.map 文件,有如下  Section:

Start         Length     Name                   Class
 0001:00000000 000011aeH .text$mn                CODE
 0002:00000000 00000654H .rdata                  DATA
 0002:00000654 00000114H .rdata$zzzdbg           DATA
 0003:00000000 00000020H .data                   DATA
 0003:00000020 00000020H .bss                    DATA
 0004:00000000 00000084H .pdata                  DATA
 0005:00000000 0000007cH .xdata                  DATA

这些 Section 的作用是:

.rdata – 保存常量数据的节。这个可以对应C语言中的常数和常量字符串,同上面一样的原因,他们的初值被保存到了PE文件的次Section中,而不是在运行时被赋值。

.data – 保存数据的节,这个对应C语言中以初始化的全局变量数据。想想为什么你在源码里初始化一个全局变量后运行时这个变量的值正是你想要的那个?int a = 12;并不意味着CRT为你执行了一个赋值语句,而是a在PE文件中保存的位置已经被硬编码了一个12的值。这样loader加载程序时,你给的初值被从PE文件读取到了内存中变量a的位置,这样才使你的变量a有了初值。

.bss – (Block Start with Symbol) 这个section对应C程序中的全局未初始化变量。啥?你说C中未初始化的全局变量实际上全被初始化成了0?这是因为实际上操作系统是这样干的——你的全局未初始化变量由于没有初值,所以不需要将值像上面两个一样保存到PE文件中(所以.bss节除了描述信息之外不占据磁盘空间),但是.bss会描述一段内存区域,loader在加载.bss section时直接开辟这么一块包括所有未初始化数据的内存区域,然后直接将这区域清零。这就是C中全局未初始化数据之所以为零的原因了。

.pdata和 .xdata都存放的是异常处理相关的内容。

.text 是最重要的执行代码段 。如果我们想直接修改 EFI 文件中的代码,需要直接在 .text section 中查找修改。【参考2】【参考3】

使用 NikPEViwer(这是一个 Windows PE 文件查看工具) 打开 es.efi。在左侧的 .text 双击可以直接定位到 TEXT Section。

同样的,我们可以使用这个工具找到这个 EFI文件的EntryPoint为 0x2C0,具体是在  NT Headers-> Optional header –> AddressOfEntryPoint【参考5】

在编译之后的结果中查找,第一个条语句在

\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\AutoGen.cod 文件中(从实习结果来看,这样生成的COD文件中有很多注释是不对的,所以具体要根据C语句和汇编语言对照来进行分析)

ProcessLibraryConstructorList PROC			; COMDAT
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c
; 62   :   gBS = SystemTable->BootServices;
  00000	48 8b 42 60	 mov	 rax, QWORD PTR [rdx+96]
  00004	48 89 05 00 00
	00 00		 mov	 QWORD PTR gBS, rax

其中COD 文件列出的 0004 开始的 48 89 05 00 00 00 00 末尾的4字节00 是Link时才会确定的取值,因此和实际 48 89 05 19 00 00 00 是有差别的。

此外这个函数的末尾有一个 ret,但是最后生成的代码中是 E9 03 00 00 00 这个字样。

  0001d	48 89 15 00 00
	00 00		 mov	 QWORD PTR gST, rdx

; 208  : }

  00024	c3		 ret	 0
ProcessLibraryConstructorList ENDP
_TEXT	ENDS

反编译结果如下【参考4】:

就是说在编译过程中 ret 被替换成跳转指令,跳转的位置是下一个函数。

ProcessModuleEntryPointList PROC			; COMDAT
; 232  : {
$LN27:
  00000	4c 8b dc	 mov	 r11, rsp
  00003	49 89 5b 08	 mov	 QWORD PTR [r11+8], rbx
  00007	49 89 73 20	 mov	 QWORD PTR [r11+32], rsi
  0000b	57		 push	 rdi

我们 C 代码中的判断条件if ((Argc>1)&&(Argv[1][0]==’a’)) { 也在这个 COD文件中,可以看到 000a2 处有一个 cmp ,之后000a6 会根据结果进行跳转:

; 33   :         if ((Argc>1)&amp;&amp;(Argv[1][0]=='a')) {

  0008e	48 8b 44 24 50	 mov	 rax, QWORD PTR EfiShellInterface$2[rsp]
  00093	48 83 78 18 01	 cmp	 QWORD PTR [rax+24], 1
  00098	76 1a		 jbe	 SHORT $LN18@ProcessMod
  0009a	48 8b 40 10	 mov	 rax, QWORD PTR [rax+16]
$LN25@ProcessMod:
  0009e	48 8b 48 08	 mov	 rcx, QWORD PTR [rax+8]
  000a2	66 83 39 61	 cmp	 WORD PTR [rcx], 97	; 00000061H
  000a6	75 0c		 jne	 SHORT $LN18@ProcessMod

对于我们来说,只要修改  jne 为 je (机器码74)即可完成目标。前面图片展示过,ProcessModuleEntryPointList  函数从0x2ec开始,因此,0006a 在文件中的偏移是 0x2ec+0xa6=0x392.直接查看这里是 75 修改为 74 (JE),保存为 esM.efi

修改之后可以直接在 Nt32 环境中测试:

可以看到,修改之后输入 a 参数不会输出,反而输入其他参数会输出字符。

本文提到的源代码和 EFI 文件下载(注意:EFI文件并非试验中使用的,具体偏移可能有差别,只供参考)

通过上述试验我们可以了解下面的知识:

  1. 一个 EFI Application 从\MdePkg\Library\UefiBootServicesTableLib 中的UefiBootServicesTableLibConstructor 函数开始,到\ShellPkg\Library\UefiShellCEntryLib\UefiShellCEntryLib.c 中的ShellCEntryLib 函数;
  2. 如何通过 COD 文件计算一个代码在 EFI 文件中的偏移。

参考:

  1. https://baike.baidu.com/item/pe%E6%96%87%E4%BB%B6/6488140?fr=aladdin
  2. https://docs.microsoft.com/en-us/windows/win32/debug/pe-format  权威,建议以此为准
  3. https://blog.csdn.net/lj94093/article/details/50503964 windows PE结构解析
  4. https://onlinedisassembler.com/odaweb/ 一个在线反编译工具很好用
  5. The PE format is documented (in the loosest sense of the word) in the WINNT.H header file. About midway through WINNT.H is a section titled “Image Format.” This section starts out with small tidbits from the old familiar MS-DOS MZ format and NE format headers before moving into the newer PE information. WINNT.H provides definitions of the raw data structures used by PE files, but contains only a few useful comments to make sense of what the structures and flags mean. Whoever wrote the header file for the PE format (the name Michael J. O’Leary keeps popping up) is certainly a believer in long, descriptive names, along with deeply nested structures and macros. When coding with WINNT.H, it’s not uncommon to have expressions like this:

pNTHeader->

OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;

上述资料来自 https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN

Teensy 听音辨数

有一千个读者便有一千个哈姆雷特。又好比鲁迅先生评价《红楼梦》 “经学家看见《易》,道学家看见淫,才子看见缠绵,革命家看见排满,流言家看见宫闱秘事。” 再比如经常被人批判的《古惑仔》系列电影,觉得它教坏小朋友,我觉得并不是。在我看起来应该属于青春励志题材。展现了“以陈浩南为首的香港下层青年人不甘平庸,通过打拼努力向上”的故事。当然,还可以说他是批判现实主义题材的作品,也包含了做人的道理。比如,影片中靓坤转眼间就被几个小时前羞辱的小警察射杀。

再比如3Q大战之时,记者采访了红衣教主,没成想有牛人直接从采访的视频声音中分析出来了他的电话【参考1】。谁能想到流氓软件之父竟然栽倒这样的事情中。

这次我们使用 Teensy 和数字麦克风来实现这个功能。

先介绍一下原理。电话实现拨号有两种方式:脉冲拨号和音频拨号。

脉冲拨号是一种时域处理方法,它用脉冲的个数来表示号码数字。脉冲拨号方式对脉冲的宽度、大小、间距、形状都有着严格的要求,如果由于线路的干扰或其他原因而使得这些参数发生了变化,则可能引起号码接收的错误。另一方面,由于每个脉冲都占有一定的时间(一般每个脉冲占用的时间为100ms),而使得这种拨号方式比较慢。当拨号时,用户通常会听到一串拨号音,老式的转盘电话就使用脉冲拨号。比如,拨号“0”时,电路“断”、“续”10次,代表数字“0”。可以看到,如果号码较长拨号耗时也会很长。因此,这种拨号方式逐渐为音频拨号所取代。

我们常用的音频拨号是双音多频 DTMF(Dual Tone Multi Frequency),双音多频,由高频群和低频群组成,低频群包含3个频率,高频群包含4个频率。一个高频信号和一个低频信号叠加组成一个组合信号,代表一个数字。

 120913361447
697123
770456
852789
941*0#

比如,用频率为770Hz 的正弦波加到1366Hz 的正弦波合成一个声音表示数字“5”

这个合成过程用Matlab 模拟如下:

首先是 770Hz 的正弦波:

这是 1366Hz 的正弦波

二者相加的结果是

对我们来说,目标是将上面这个合成后的结果分解得到具体是哪两个信号合成的。自然而然想到使用傅立叶变换来处理:

放大,可以看到是 770 和 1366 合成的。

这次选择 Teensy+SPH0645LM4H实现声音的采集和分析,最终结果显示在一个 1602 LCD上。

接线方式:

Teensy 3.2     SPH0645LM4H

        GND             SEL

        D23              LRCL

         D13             DOUT

         D9               BCLK

         GND           GND

         3.3V            3V

Teensy 3.2     LCD1602

         GND           GND

         Vin              VCC     

         Pin19          SCL

         Pin18          SDA

代码上分析获得频率并没有通过傅立叶变换,而是直接使用 Teensy 的音频库中的AudioAnalyzeToneDetect 函数。这个函数使用的是Goertzel算法,该算法的主要思想是检查音频数据中是否包含某一个给定的频率【参考2】。因此,对于我们来说就是不断测试当前的信号中是否有697到1477这些频率,如果存在的话就转化为对应的数字。

完整代码如下:

// Dial Tone (DTMF) decoding

 

#include &lt;Audio.h>

#include &lt;Wire.h>

#include &lt;SPI.h>

#include &lt;LiquidCrystal_I2C.h>

 

LiquidCrystal_I2C lcd(0x3f,20,4);

 

// Create the Audio components.  These should be created in the

// order data flows, inputs/sources -> processing -> outputs

//

AudioInputI2S            audioIn;

AudioAnalyzeToneDetect   row1;     // 7 tone detectors are needed

AudioAnalyzeToneDetect   row2;     // to receive DTMF dial tones

AudioAnalyzeToneDetect   row3;

AudioAnalyzeToneDetect   row4;

AudioAnalyzeToneDetect   column1;

AudioAnalyzeToneDetect   column2;

AudioAnalyzeToneDetect   column3;

 

// Create Audio connections between the components

//

AudioConnection patchCord01(audioIn, 0, row1, 0);

AudioConnection patchCord02(audioIn, 0, row2, 0);

AudioConnection patchCord03(audioIn, 0, row3, 0);

AudioConnection patchCord04(audioIn, 0, row4, 0);

AudioConnection patchCord05(audioIn, 0, column1, 0);

AudioConnection patchCord06(audioIn, 0, column2, 0);

AudioConnection patchCord07(audioIn, 0, column3, 0);

 

int charpos=0;

 

void setup() {

  // Audio connections require memory to work.  For more

  // detailed information, see the MemoryAndCpuUsage example

  AudioMemory(12);

 

  lcd.init();

  lcd.backlight();

 

 

  while (!Serial);

  delay(100);

  Serial.println("Start decoding");

 

  // Configure the tone detectors with the frequency and number

  // of cycles to match.  These numbers were picked for match

  // times of approx 30 ms.  Longer times are more precise.

  row1.frequency(697, 21);

  row2.frequency(770, 23);

  row3.frequency(852, 25);

  row4.frequency(941, 28);

  column1.frequency(1209, 36);

  column2.frequency(1336, 40);

  column3.frequency(1477, 44);

}

 

const float row_threshold = 0.009;

const float column_threshold = 0.009;

char lastdigit=0;

 

void loop() {

  float r1, r2, r3, r4, c1, c2, c3;

  char digit=0;

 

  // read all seven tone detectors

  r1 = row1.read();

  r2 = row2.read();

  r3 = row3.read();

  r4 = row4.read();

  c1 = column1.read();

  c2 = column2.read();

  c3 = column3.read();

 

/*

  // print the raw data, for troubleshooting

  Serial.print("tones: ");

  Serial.print(r1);

  Serial.print(", ");

  Serial.print(r2);

  Serial.print(", ");

  Serial.print(r3);

  Serial.print(", ");

  Serial.print(r4);

  Serial.print(",   ");

  Serial.print(c1);

  Serial.print(", ");

  Serial.print(c2);

  Serial.print(", ");

  Serial.println(c3);

*/

  // check all 12 combinations for key press

  if (r1 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '1';

    } else if (c2 > column_threshold) {

      digit = '2';

    } else if (c3 > column_threshold) {

      digit = '3';

    }

  } else if (r2 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '4';

    } else if (c2 > column_threshold) {

      digit = '5';

    } else if (c3 > column_threshold) {

      digit = '6';

    }

  } else if (r3 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '7';

    } else if (c2 > column_threshold) {

      digit = '8';

    } else if (c3 > column_threshold) {

      digit = '9';

    }

  } else if (r4 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '*';

    } else if (c2 > column_threshold) {

      digit = '0';

    } else if (c3 > column_threshold) {

      digit = '#';

    }

  }

 

  // print the key, if any found

  if ((digit > 0)&amp;&amp;(lastdigit!=digit)) {

  //if (digit > 0) {

    Serial.print("  --> Key: ");

    Serial.println(digit);

   

    lcd.setCursor(charpos % 16, (charpos / 16)%2);

    lcd.print(digit);

    charpos++;  

  }

  lastdigit=digit;

 

 

 

  // uncomment these lines to see how much CPU time

  // the tone detectors and audio library are using

  //Serial.print("CPU=");

  //Serial.print(AudioProcessorUsage());

  //Serial.print("%, max=");

  //Serial.print(AudioProcessorUsageMax());

  //Serial.print("%   ");

 

}

工作的照片:

工作的视频:

https://zhuanlan.zhihu.com/p/70966370

很明显,你的按键信息包含在了发出的声音中,这是一个安全隐患。现实生活中,自动柜员机(ATM)也有一个键盘。仔细观察能发现每个按键会发出相同的声音。但是在十几年前,那个键盘的数字对应着的是不同的声音………..

参考:

1. https://www.cnblogs.com/emouse/archive/2012/09/01/2666308.html?utm_source=debugrun&utm_medium=referral 转:技术宅逆天了!如何从按键音中听出周鸿祎的手机号码

2. https://blog.csdn.net/silent123go/article/details/54022037

Step to UEFI (193)IDT 研究

前面做过了 GDT 的解析,这次研究一下 IDT。

X86 上有2种中断模式:中断(interrupts)和异常(exceptions )

Interrupt 是异步,通常由 I/O 设备来生成,比如:设置一个定时器在某个时间之后发生;

Exception 是同步的,当处理器执行某个指令之后产生的。对于 Exception 还分为 faults, traps 和 abort. X86处理器对于上面两种处理方式相同,当某一个中断发生之后,CPU会通过 IDT来找到对应的处理函数。处理完成之后将控制权返回产生的位置。

X86处理器有32个预定义中断异常,余下224个可以由用户自己定义。每个IDT有一个定义的数值: vector。当然,我们更常见的是IRQXX 的说法,在X86中还有一套让 IRQ 和 Vector对应起来的机制,未来会继续研究。

和 GDT 非常类似,可以通过 IDTR寄存器获得IDT 长度和它在内存中的位置。

其中的描述符定义在\MdePkg\Include\Library\BaseLib.h 文件中如下:

///
/// Byte packed structure for an x64 Interrupt Gate Descriptor.
///
typedef union {
  struct {
    UINT32  OffsetLow:16;   ///< Offset bits 15..0.
    UINT32  Selector:16;    ///< Selector.
    UINT32  Reserved_0:8;   ///< Reserved.
    UINT32  GateType:8;     ///< Gate Type.  See #defines above.
    UINT32  OffsetHigh:16;  ///< Offset bits 31..16.
    UINT32  OffsetUpper:32; ///< Offset bits 63..32.
    UINT32  Reserved_1:32;  ///< Reserved.
  } Bits;
  struct {
    UINT64  Uint64;
    UINT64  Uint64_1;
  } Uint128;
} IA32_IDT_GATE_DESCRIPTOR;

GateType可能是下面三种类型之一:

具体类型定义如下:

#define IA32_IDT_GATE_TYPE_TASK          0x85
#define IA32_IDT_GATE_TYPE_INTERRUPT_16  0x86
#define IA32_IDT_GATE_TYPE_TRAP_16       0x87
#define IA32_IDT_GATE_TYPE_INTERRUPT_32  0x8E
#define IA32_IDT_GATE_TYPE_TRAP_32       0x8F

接下来编程解析 Shell 下 IDT完整代码如下:

/** @file
  Application for Cryptographic Primitives Validation.

Copyright (c) 2009 - 2016, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/DebugLib.h>
#include <Protocol/PciIo.h>
#include <Library/ShellLib.h>

EFI_STATUS
EFIAPI
GDTMain (
  IN     EFI_HANDLE                 ImageHandle,
  IN     EFI_SYSTEM_TABLE           *SystemTable
  )
{
        IA32_DESCRIPTOR   Idtr;
        IA32_IDT_GATE_DESCRIPTOR         *IdtTable;
        UINT16            Index;
        UINT16            IdtEntryCount;

        ShellSetPageBreakMode(TRUE);
        
        AsmReadIdtr (&Idtr);
        IdtEntryCount = (UINT16) ((Idtr.Limit + 1) / sizeof (IA32_IDT_GATE_DESCRIPTOR));
       
        IdtTable = (IA32_IDT_GATE_DESCRIPTOR *) Idtr.Base;
        Print(L"IDTR=0x%lX\n",IdtTable);
        for (Index = 0; Index < IdtEntryCount; Index++) {
            Print(L"No.[%d] ",Index);
            Print(L"Selector[%d] ",IdtTable->Bits.Selector);
            Print(L"Type [0x%X] ",IdtTable->Bits.GateType);
            Print(L"Offset [0x%lX]\n",
                        (IdtTable->Bits.OffsetUpper<<32)|
                        (IdtTable->Bits.OffsetHigh <<16)|
                        (IdtTable->Bits.OffsetLow)
                        );
            IdtTable++;
        }
  return EFI_SUCCESS;
}

在实体机上运行结果如下:

感觉最早实模式下的中断向量表非常类似,只是中断向量表一定是在内存从0开始的位置,而这个可以放置在内存的任何位置。

前面提到处理器有18个预定义的异常:

可以看到上面预定义了21个异常,此外还有11个被 Reserved 起来。

接下来我们用DCI来trace #DE(Divide Error)的处理。

首先编写一个产生 #DE 的代码。这个是除数为0错误。主要代码如下:

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        volatile x=0;
        
        //CpuDeadLoop();
        for (x = 0; x == 0;);
        
        x=x/x;
        
        return(0);
}

对应的汇编语言如下:

ShellAppMain PROC					; COMDAT

; 21   : {

$LN6:
  00000	48 89 54 24 10	 mov	 QWORD PTR [rsp+16], rdx
  00005	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  0000a	48 83 ec 18	 sub	 rsp, 24

; 22   :         volatile x=0;

  0000e	c7 04 24 00 00
	00 00		 mov	 DWORD PTR x$[rsp], 0

; 23   :         
; 24   :         //CpuDeadLoop();
; 25   :         for (x = 0; x == 0;);

  00015	c7 04 24 00 00
	00 00		 mov	 DWORD PTR x$[rsp], 0
$LN2@ShellAppMa:
  0001c	8b 04 24	 mov	 eax, DWORD PTR x$[rsp]
  0001f	85 c0		 test	 eax, eax
  00021	75 02		 jne	 SHORT $LN3@ShellAppMa
  00023	eb f7		 jmp	 SHORT $LN2@ShellAppMa
$LN3@ShellAppMa:

; 26   :         
; 27   :         x=x/x;

  00025	8b 04 24	 mov	 eax, DWORD PTR x$[rsp]
  00028	8b 0c 24	 mov	 ecx, DWORD PTR x$[rsp]
  0002b	99		 cdq
  0002c	f7 f9		 idiv	 ecx
  0002e	89 04 24	 mov	 DWORD PTR x$[rsp], eax

; 28   :         
; 29   :         return(0);

  00031	33 c0		 xor	 eax, eax

; 30   : }

  00033	48 83 c4 18	 add	 rsp, 24
  00037	c3		 ret	 0
ShellAppMain ENDP

为了便于观察这里面我们使用        for (x = 0; x == 0;); 等效于 CpuDeadLoop(); 可以让我们在运行的时候停下来。具体的调试操作如下:

1.先用 IDT 确定 Vector 0 的入口是 0x42254018 (这里和上一次查看的位置不同,这是一个安全特性,同样的BIOS每次运行各种 Table 的位置有所不同);

2.运行DETest.efi。运行之后会 Hang 在 Shell下。在调试机上运行 itp.halt() 停止;

3.使用 itp.threads[0].asm(“$”,20)查看停的位置

可以看到这里就是前面的 “eb f7 jmp   SHORT $LN2@ShellAppMa”,是我们让CPU Dead loop 的代码。

4.使用 itp.threads[0].mem 修改内存代码跳过去。再次查看,可以看到 jmp 已经被修改为 nop

5.使用 itp.threads[0].brnew(“0x42254018”,”exe global”) 加入一个断点,当运行到 vector 0 给出的内存位置时自动停止。这时,使用 itp.go() 即触发了了停在了入口。

6.使用命令查看当前位置的汇编代码

具体代码在 \UefiCpuPkg\Library\CpuExceptionHandlerLib\X64\ExceptionHandlerAsm.nasm 中:

AsmIdtVectorBegin:
%rep  32
    db      0x6a        ; push  #VectorNum
    db      ($ - AsmIdtVectorBegin) / ((AsmIdtVectorEnd - AsmIdtVectorBegin) / 32) ; VectorNum
    push    rax
    mov     rax, strict qword 0 ;    mov     rax, CommonInterruptEntry 【参考2】
    jmp     rax
%endrep
AsmIdtVectorEnd:

7.下面可以使用 itp.trheads[0].step 单步执行,最终代码跳入下面的代码段中

对应的代码还是上面还在提到的  ExceptionHandlerAsm.nasm 文件中:

global ASM_PFX(CommonInterruptEntry)
ASM_PFX(CommonInterruptEntry):
    cli
    pop     rax
    ;
    ; All interrupt handlers are invoked through interrupt gates, so
    ; IF flag automatically cleared at the entry point
    ;
    xchg    rcx, [rsp]      ; Save rcx into stack and save vector number into rcx
    and     rcx, 0xFF
    cmp     ecx, 32         ; Intel reserved vector for exceptions?
    jae     NoErrorCode
    bt      [ASM_PFX(mErrorCodeFlag)], ecx
    jc      HasErrorCode

这里使用的是在vector 0 入口地址下断点拦截的方式,除此之外,我还尝试了使用 step指令一直跟踪 到 idiv 指令的方法,会导致死机,具体原因不清楚。我记得有看过介绍性的文章,说 step 是通过不断在下一条指令之后插入 int 1来实现的,而这里我们同时在调试int0,有可能是因为这样的原因会导致冲突所以无法用 step 跟踪查看到。

参考:

  1. Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4  P2903
  2. STRICT约束优化 :当汇编时将优化器打开到2或更高级别时,nasm会使用尺寸约束,会给(byte word dword qword tword)尽可能小的尺寸,可以使用关键字STRICT来制约这种优化。
    强制一个特定的操作数为原来尺寸:
    push strict dword 33
    但当优化器关闭时,无论是否有strict,都会产生相同代码。 来自 https://yq.aliyun.com/articles/24926

Step to UEFI (192)UEFI Shell 下实现 INI 文件的解析

INI 文件是一种有简单格式的文本文件(ASCII),可以用来提供一些配置信息。比如下面这个文件:

; exp ini file
[port]
Portname=COM4
Port=4
[settings]
Baud=115200 ;Speed

第一行是注释。然后  port 和 settings 都被称作 section。其中的 Portname Port 和 Baud 都是 Key,相应的,每一个 Key 都有 value 。

虽然这样的结构并不复杂,但是如果完全自己来写还是很麻烦的。经过搜索,找到了 iniparser 【参考1】这个开源项目。对应的库需要有一点修改才能编译成功, iniparser.c 中需要如下改动:  

return last - s;    ---->    return (unsigned int) (last - s);

根据例子编写的测试代码:

#include  <Uefi.h>
#include  <Library/BaseLib.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

#include "iniparser.h"

extern  EFI_SYSTEM_TABLE    *gST;
extern  EFI_BOOT_SERVICES   *gBS;


void create_example_ini_file(void)
{
    FILE    *   ini ;
    if ((ini=fopen("example.ini", "w"))==NULL) {
        fprintf(stderr, "iniparser: cannot create example.ini\n");
        return ;
    }

    fprintf(ini,
    "#\n"
    "# This is an example of ini file\n"
    "#\n"
    "\n"
    "[Pizza]\n"
    "\n"
    "Ham       = yes ;\n"
    "Mushrooms = TRUE ;\n"
    "Capres    = 0 ;\n"
    "Cheese    = Non ;\n"
    "\n"
    "\n"
    "[Wine]\n"
    "\n"
    "Grape     = Cabernet Sauvignon ;\n"
    "Year      = 1989 ;\n"
    "Country   = Spain ;\n"
    "Alcohol   = 12.5  ;\n"
    "\n");
    fclose(ini);
}


int parse_ini_file(char * ini_name)
{
    dictionary  *   ini ;

    /* Some temporary variables to hold query results */
    int             b ;
    int             i ;
    double          d ;
    const char  *   s ;

    ini = iniparser_load(ini_name);
    if (ini==NULL) {
        fprintf(stderr, "cannot parse file: %s\n", ini_name);
        return -1 ;
    }
    iniparser_dump(ini, stderr);

    /* Get pizza attributes */
    printf("Pizza:\n");

    b = iniparser_getboolean(ini, "pizza:ham", -1);
    printf("Ham:       [%d]\n", b);
    b = iniparser_getboolean(ini, "pizza:mushrooms", -1);
    printf("Mushrooms: [%d]\n", b);
    b = iniparser_getboolean(ini, "pizza:capres", -1);
    printf("Capres:    [%d]\n", b);
    b = iniparser_getboolean(ini, "pizza:cheese", -1);
    printf("Cheese:    [%d]\n", b);

    /* Get wine attributes */
    printf("Wine:\n");
    s = iniparser_getstring(ini, "wine:grape", NULL);
    printf("Grape:     [%s]\n", s ? s : "UNDEF");

    i = iniparser_getint(ini, "wine:year", -1);
    printf("Year:      [%d]\n", i);

    s = iniparser_getstring(ini, "wine:country", NULL);
    printf("Country:   [%s]\n", s ? s : "UNDEF");

    d = iniparser_getdouble(ini, "wine:alcohol", -1.0);
    printf("Alcohol:   [%g]\n", d);

    iniparser_freedict(ini);
    return 0 ;
}

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
main (
  IN UINTN Argc,
  IN CHAR8 **Argv
  )
{
    int     status ;

    if (Argc<2) {
        create_example_ini_file();
        status = parse_ini_file("example.ini");
    } else {
        status = parse_ini_file(Argv[1]);
    }
    return status ;
}

因为用到的fopen ,所以需要特别加入  DevShell 在LibraryClasses 中

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = init
  FILE_GUID                      = a912f198-7f0e-4805-b90A-b757b806ec85
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = ShellCEntryLib

#
#  VALID_ARCHITECTURES           = IA32 X64
#

[Sources]
  INITest.c
  iniparser.c
  iniparser.h
  dictionary.h
  dictionary.c

[Packages]
  MdePkg/MdePkg.dec
  ShellPkg/ShellPkg.dec
  StdLib/StdLib.dec
  
[LibraryClasses]
  UefiLib
  ShellCEntryLib
  IoLib
  LibC
  LibStdio
  DevShell

运行结果:

不加参数会读取example.ini

加入参数可以读取指定文件:

完整的代码下载:

参考:

1.https://github.com/ndevilla/iniparser

Step to UEFI (190)Segment Registers in 64 Bits mode

最近在 “Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4”看到下面这一段话:

以是说,在 64Bits模式下 CS DS ES SS 的 Base 无论设置为什么,都会被认为是0.

于是,做实验进行验证:

1.我尝试直接在BIOS中修改 GDT 发现会导致 Post 死机。

2.尝试编写一个Driver来完成加载新的 GDT 的事情。最简单的方法是修改前面的 CPUDxe。直接拿来主义将 CpuDxe Copy 一份修改为 MyCpuDxe。使用下面的语句进行编译:

build -a X64 -p ueficpupkg\UefiCpuPkg.dsc -m UefiCpuPkg\MyCpuDxe\CpuDxe.inf

3.根据前面的研究, DS 段选择子设置成 Base=0x4000 (CpuGdt.c)

  //
  // LINEAR_DATA64_SEL
  //
  {
    0x0FFFF,        // limit 15:0
//LABZDebug 0x0,            // base 15:0
   0x4000,            // base 15:0
    0x0,            // base 23:16
    0x092,          // present, ring 0, data, read/write
    0x0CF,          // page-granular, 32-bit
    0x0,
  },

4.上面的代码后,使用 Load MyCpuDxe.efi 加载运行。再使用之前的gdt.efi 查看当前的GDT。

5.随机选择一个内存位置,比如:0x3000.使用 Mem指令查看, 结果如下

Memory Address 0000000000003000 200 Bytes
  00003000: 36 EF 55 63 64 A5 7C 87-34 FF 57 73 66 B5 7E D6  *6.Ucd.|.4.Wsf.~.*
  00003010: 24 FF 47 73 76 B5 6E D6-A4 FF C7 73 F6 B5 EE D6  *$.Gsv.n....s....*
  00003020: A4 FD C7 71 F6 B7 CE D4-A4 DD C7 51 F6 97 EE F4  *...q.......Q....*
  00003030: 84 DD E7 51 D6 97 DE F4-C4 DD A7 51 96 97 8E F4  *...Q.......Q....*
  00003040: 11 3E 9D 0F 5A 17 38 9B-10 3E 9C 0F 5A 17 39 9B  *.>..Z.8..>..Z.9.*
  00003050: 30 BE BC 8F 7A 97 19 1B-30 BC BC 8F 72 97 19 1B  *0...z...0...r...*
  00003060: 30 BC BD 8D 7A 95 19 19-34 BC BA 9D 7E 85 1D 09  *0...z...4...~...*
  00003070: 3C EC B0 DD 76 C5 15 49-7C EC F0 DD 36 C5 55 49  *&lt;...v..I|...6.UI*

使用 DCI 查看

使用 DCI 修改此处值

使用 DCI 修改此处值

Memory Address 0000000000003000 200 Bytes
  00003000: EF CD AB 90 78 56 34 12-34 FF 57 73 66 B5 7E D6  *....xV4.4.Wsf.~.*
  00003010: 24 FF 47 73 76 B5 6E D6-A4 FF C7 73 F6 B5 EE D6  *$.Gsv.n....s....*
  00003020: A4 FD C7 71 F6 B7 CE D4-A4 DD C7 51 F6 97 EE F4  *...q.......Q....*
  00003030: 84 DD E7 51 D6 97 DE F4-C4 DD A7 51 96 97 8E F4  *...Q.......Q....*
  00003040: 11 3E 9D 0F 5A 17 38 9B-10 3E 9C 0F 5A 17 39 9B  *.>..Z.8..>..Z.9.*
  00003050: 30 BE BC 8F 7A 97 19 1B-30 BC BC 8F 72 97 19 1B  9*0...z...0...r...*

这就说明无论DS 的 Base 是多少,都是按照默认为0 来处理的(当然基本的检查机制还是存在的,需要保证加载正确的描述符).

完整的实验代码和EFI下载:

Step to UEFI (191)GDT 中1Bit的差别

细心的读者会发现前面的GDT 试验中,设置的选择子和实际查看到的选择子有着1bit的差别。比如:

  //
  // LINEAR_CODE64_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x09A,          // present, ring 0, code, execute/read
    0x0AF,          // page-granular, 64-bit code
    0x0,            // base (high)
  },

实际读取到的是:

No.[7] Seg. Desc 0xAF9B000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
64-bit code segment

设置的是 9A 读取到的是 9B,二者相差1Bit。

对于这个问题开始研究。

首先确定差别的位置:

typedef struct _GDT_ENTRY {
  UINT16 Limit15_0;
  UINT16 Base15_0;
  UINT8  Base23_16;
  UINT8  Type;
  UINT8  Limit19_16_and_flags;
  UINT8  Base31_24;
} GDT_ENTRY;

是 Type 中有差别。结合【参考1】资料:

【参考1】

差别就是上图中 Type A Bit. 表示当前的是否被访问过。没有访问过就是 0,访问过就是1.

接下来通过一个实验来验证上面的知识。首先,选择一个 selector,我这里用的是下面这个

  //
  // LINEAR_SEL
  //
  {
    0x0FFFF,        // limit 15:0
    0x0,            // base 15:0
    0x0,            // base 23:16
    0x092,          // present, ring 0, data, read/write
    0x0CF,          // page-granular, 32-bit
    0x0,
  },

最主要是直接使用这个我们就不用重新编译BIOS或者再写一个加载描述符的Application了。

其次,加载 fs 的代码如下:

[BITS 64]
mov ecx,28h
mov fs,cx

用Nasm 转化为机器码对应如下:

     1                                  [BITS 64]
     2 00000000 B928000000              mov ecx,8h
     3 00000005 8EE1                    mov fs,cx

使用之前的代码,我们写一个 LoadFS.efi代码如下:

#include  &lt;Uefi.h>
#include  &lt;Library/BaseLib.h>
#include  &lt;Library/UefiLib.h>
#include  &lt;Library/ShellCEntryLib.h>
#include  &lt;Library/IoLib.h>

extern  EFI_SYSTEM_TABLE    *gST;
extern  EFI_BOOT_SERVICES   *gBS;

void
SetFS()
{
        __nop();
        __nop();
        __nop();
        __nop();
        __nop();
        __nop();
        __nop();
        __nop();        
}

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        UINT8 *f=(UINT8 *)&SetFS;
        *(f+0)=0xB9;
        *(f+1)=0x08;
        *(f+2)=0x00;
        *(f+3)=0x00;
        *(f+4)=0x00;
        *(f+5)=0x8E;
        *(f+6)=0xE1;

        SetFS();
        return(0);
}

编译后就可以进行实验了,实验必须在实体机上进行。

1.启动到Shell 后先用 gdt.efi 查看一下

GDTR=0x8C634718
No.[0] Seg. Desc 0x0
Base=0x0
Limit=0x0
Descriptor Type: system
Not 64-bit code segment
No.[1] Seg. Desc 0xCF92000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment 
(后面省略)

注意 92 那个位置

2.运行 loadfs.efi

3.再次运行 gdt.efi 进行查看

GDTR=0x8C634718
No.[0] Seg. Desc 0x0
Base=0x0
Limit=0x0
Descriptor Type: system
Not 64-bit code segment
No.[1] Seg. Desc 0xCF93000000FFFF
Base=0x0
Limit=0xFFFFF
Descriptor Type: Code or Data
Not 64-bit code segment
(后面省略)

可以看到已经变成了93h,就是说如果 selector被加载到某个段寄存器,对应的 selector上的 type accessed 位将会做出标记。

完整代码下载:

参考:

1.” Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4”  Figure 5-1. Descriptor Fields Used for Protection P2855

开机的窗口哪里来的?

最近发现开机之后桌面有一个弹出窗口,郁闷的是上面没有关闭按钮,也无法通过 ALT+F4关闭。

从上面也可以看出来制作广告的人非常不用心

为了确认这个窗口的归属,使用 SPY++

再进一步查看属性就得知他是多玩坦克世界盒子的广告窗口。关掉坦克世界盒子这个窗口也会随之消失。

我挺喜欢这个法系小炮的。射速快8s,一局时间越久,战绩通常越好。

Step to UEFI (189)Read&WriteMMX0

最近偶然看到BaseLib 提供了AsmReadMm0() 和AsmWriteMm0()函数,于是进行了下面的实验。

首先,用AsmReadMm0 读取当前 MM0 寄存器的值,然后随机生成一个再写入 MM0 中。

#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>

EFI_STATUS
EFIAPI
MMXTestMain (
  IN     EFI_HANDLE                 ImageHandle,
  IN     EFI_SYSTEM_TABLE           *SystemTable
  )
{
        UINT64  x=AsmReadMm0();
        Print(L"Current MM0 =0x%lX\n",x);
        AsmRdRand64(&x);
        Print(L"Random Value=0x%lX\n",x);
        AsmWriteMm0(x);

        return EFI_SUCCESS;
}

之后在 NT32Pkg 的模拟器中运行结果如下:

可以看到,前一次运行之后随机生成一个数值,写入MM0之后再次运行还可以读出。

=====================================================================================

R0-R7 是独立于 RXX 的寄存器,之前文章引用过一幅示意图,上面写的 RAX or R0 这句话会误导读者以为他们是同一个寄存器。

实际上,R0-R7 是用来进行浮点运算的 80Bits的寄存器。MMX0-7是R0-R7 0-64Bits的别名:

完整代码: