X64Dbg 介绍->表达式


未经博主允许禁止转载盗取以及爬虫,出处 博客园:ibinary
请远离垃圾网站: 原文出处 https://www.cnblogs.com/iBinary/
请读者看到之后去原文观看.

X64Dbg 介绍->表达式

一丶 字符串格式与Log指令

1.1 前言

x64Dbg是一个开源的ring3层的调试器,其内置了很多强大的命令供我们使用.

而在调试的过程中熟悉它的命令以及脚本语法则会让我们逆向的时候事半功倍.

二丶字符串格式指令学习

2.1 Log指令

在学习X64Dbg的时候最好优先学习Log指令. 因为其可以帮我们最快的将指令结果进行输出. 便于我们验证命令是否正确.

log指令格式为如下:

log "{?:expression}"

其中 ? 是类型的意思我们可以自定义的类型以及复杂的类型. 或者说可以省略不写,而着重于后面的expression表达式,expression(表达式)可以使用X64dbg的命令.也可以是C语言的表达式.

在命令中 {}里面可以视为是命令或者说表达式. 而 : 前边则可以视为是显示的数据类型.可以胜

最简单的log输出命令则为如下:

log "HelloWorld"   ===> HelloWorld

如果是C语言的表达式则可以使用 括号 位运算 << >> + -* /等来进行表示

如下:

log "{(1+2)}"   ==> 3  //这里省略的显示部分,也就是 ?: 可以直接在{}写自己的表达式

Log 配合字符串格式指令则能产生强大的效果. 请先下面小节.

2.2 log-简单指令-字符串格式

类型 含义 应用 输出结果
d 表示输出后面命令的结果为有符号的10进制类型  log  "{d:0x00401000}" 4198400
u 同上,表示无符号十进制 同上,改成d查看 同上
p 表示零前缀开头的指针类型(pointer) log "{p:0x00401000}" 00401000
s 表示是string的指针 log "{s:0x0040004E}" "This program cannot be run in Dos mode.\r\r\n$"
x 表示16进制 log "{x:0x00400000}" 400000
a 表示地址信息 log "{a:0x00400000}" exeName.00400000
i 输出汇编指令,按照汇编解析 log "{i:0x00400000}" dec ebp
f 单精度浮点的指针或者寄存器 log "{f:0x00400000}" 1.32567e-038
F 双精度的浮点指针或者寄存器 log "{F:0x00400000}" 6.37066138261923e-314

补充:

  1. log里面输出的地址 可以换成寄存器

    log "{x:eax}" 则是以16进制输出eax里面的值 设eax = 0xA7 而输出 A7
    
  2. x 和 a不同

    x输出的是纯净的16进制地址,而a输出的是带着模块名字的.

    假设我们的主模块为 abc.exe

    则使用a输出的结果为

    log "{a:0x00400000}"  ==> abc.00400000
    
  3. 注意单精度与双精度的大小写

    大小写是不同的,请注意.

    且说过是可以使用寄存器的,所以可以使用 XMM寄存器和YMM寄存器来进行解析. 但是请注意,如果使用的是f 那么如果寄存器是XMM 那么只会使用 XMM的 31->0这个区间 如果是YMM 则会使用 255-224区间.

  4. s指令

    s指令后面跟着的地址是一个字符串的开始的首地址,s是吧这个地址视为了0结尾的字符串地址,然后直接解析的.

2.3 log-复杂指令-字符串格式

语法 含义 应用 结果 补充
{winerror@code} 输出windows错误码,并且format格式化后输出结果 log "{winerror@5}" ERROR_ACCESS_DENIED:拒绝访问
` 输出NTSTATUS类型的错误码 log "{ntstatus@0}" STATUS_SUCCESS:STATUS_SUCCESS
从address地址开始打印size个字节 log "{mem;2@0x00400000}" 4D5A 与之对应我们还可以使用 log "{2:[0x00400000]}"来进行打印,带引的结果为 5A4D
{ascii[;length]@address}
从address地址开始打印length长度.以ascii编码解析 log "{ascii;20@0x0040004E}" This program cannot be run in DO
同上,只不过是以UTF-8类型打印 同上
同上只不过是utf-16来解析 同上

在复杂类型中可以看到 可以使用 xxx@xxxx的方式来进行结果的输出 而如果是以 string类型来进行输出的话 那么需要你加上一个 可选的 length长度 如果不加也可以,那么输出结果可能不是你想要的.

其它类型

语法 含义 应用 结果 补充
打印此地址的反汇编 log "{disasm@0x00400000}" dec ebp 此指令等价于 {i:address}
打印模块的名称 log "{modname@0x00400000}" abc.exe 为什么输出abc.exe参考2.1log指令介绍
打印此地址处的(自动)标签 log "{label@76570A50}" MessageBoxA 0x76570A50是MessageBox的地址,其标签也是命名的MessageBoxA
打印此地址处的自动注释 同上 自动的意思可能是默认的意思,默认的注释.
反转字节 log "{bswap;4@0x12345678}" 78654321 bswap是反转字节的意思,可以理解为是函数,如果是函数那么后面跟着; 分号后面则是参数,比如要反转的字节的长度是4个字节,要反转的值是0x12345678

2.4 命令例子

例子 说明 结果 补充
log "aaaa:{eax}" 输出eax的值,显示名称为aaaa {}括号里面的内容则是命令 aaaa:123
log "passWorld:{s:0x0040004e}" 显示名称为passWorld 以s类型解析0x0040004e passWorld:"This is xxxx"
log "passWorld:{ansi;20@0x0040004e}" 以ansi解析20个字节,从0x00400004e位置 passWorld:This program cannot be run in DO  注意它解析的字符串没有双引号
log "要进行反转了:{bswap(0x12345678)}" 反转0x12345678 78654321 这里使用的bswap是表达式的函数,后面会讲解x64提供的表达式函数有哪些.

三丶X64Dbg输入命令介绍

3.1 内存的应用

函数 说明 应用 结果
[addr] 带方括号可以直接从里面读取直接地址,默认省略了大小 [0x00400000] 00905A4D
n:[addr] 同上,上面默认省略的的大小,这里可以显示给定 1:[0x00400000] 4D
seg:[addr] 从所处的段中读取 DWORD/QWORD,取值可以是gs es cs fs ds ss fs gs cs:[0x00401000] CCCCCCCC

可以看到地址的访问可以用 中括号[] 来进行访问, 前边加上 大小则是代表在这个地址里面取出多少字节来用. 不常用的就是 seg:[addr] 在32/64下已经没有 "段的概念了"

3.2 标志伪指令

在x64dbg中想要直接访问标志的值可以使用 _标志名 来进行访问

如下:

_cf_pf_af_zf_sf_tf_if_df_of_rf_vm_ac_vif_vip_id

使用log可以进行输出

log "CF的值是:{_cf}"  ===> CF的值是1

3.3 模块的应用

模块分为如何访问加载的模块以及解析函数的地址.

  • 加载模块的应用

    这些命令很常见,因为在X64DBG里面跳转地址的话可以直接使用模块基地址+偏移(RVA)来进行跳转.

    命令如下:

    命令 说明 应用 结果
     module :0 访问模块基地址 :0 或者[abc.exe]:0 0x00400000
    module :base 同上,可以省略[] :base或[abc.exe]:base 0x00400000
    module :imagebase 同上
    module :header 同上

为了不让MarkDown给将[]翻译成网址,所以在[]后面加了个空格,然后加了一个: 其实原指令为如下:

[module]:0/base/imagebase/header
  • RVA偏移的使用

    根据上面所属我们可以获取模块的基地址 所以后面我们直接加 RVA(偏移即可)

    [module]:0 + [rva/offset]   应用--->  :0 + 0x0010000  = 0x004010000
    [module]:0$[rva/offset]     同上
    [module]:0#[rva/offset]     同上
    
  • 模块入口点

    命令如下:

    ```[module]:entry
    

    --->
    ?oep
    ?entry
    ?ep

```cpp
可以使用Log输出

```cpp
log "base =:0x{:0} oep = :0x{abc.exe.oep}"
--->
base = 0x00400000 oep = 0x00401520

?的方式
log "base =:0x{:0} oep = :{?oep}"
--->
结果同上

在CTRL+G中也可以使用这个 oep(ep entry)命令直接跳转到OEP.

  • 函数或者模块导出函数的应用.
[module].dll.[api]
[module]:[api]
[module]:[ordinal]

使用:

user32.dll.MessageBoxA    ---> 显示对应的地址
user32.MessageBoxA
user32.dll:MessageBoxA
--->log使用方式
log "user32.MessageBoxA Addr= 0x{user32.MessageBoxA}"
----->结果
user32.MessageBoxA Addr= 0x76570A50

其实也可以配合 OEP entry等命令使用. 这里本质就是获取 每个模块的导出函数的地址.

编译我们的操作.

四丶表达式

4.1 C语言表达式

调试器是允许基本的表达式进行命令操作的. 我们只需要在命令窗口中使用表达式即可.结果则会显示到控制台中. 表达式除了计算之外还可以使用类似C的语法快速更改变量.

C表达式可以使用如下:

()        括号
[]        中括号
-1 ~1 !0  负数 取反 逻辑非
+-*/  %    加减乘除取余
<< >> <<< >>> 无符号左移(shl无符号 sal有符号) 无符号右移(shr sar)
> < <= >= != 大于小于 等运算
& | ! ^ || && -> 等符号

4.2 字符串方法

既然有表达式,那么表达式函数也是我们要掌握的.这个是 x64dbg为我们提供的. 可以使用.

它分为如下几个模块的命令.需要单独展开说明.

或者去官网查看.

表达式函数 — x64dbg 文档

4.2.1 字符串函数

字符串表达式函数我们在Log讲解的时候已经接触过了. 其命令如下:

utf8(addr)             -->以utf-8解析地址输出utf-8字符串
utf16(addr)            -->同上,只不过是utf16
strstr(str1,str2)      -->同C库函数,查找A字符里面是否包含B字符串
streq(str1,str2)       -->比较两个字符串
strlen(str)            -->计算字符串的长度

这些表达式函数都可以作为参数用作其它函数的的输入参数. 所有表达式计算的结果都必须为一个有效的数字,因此不是有效的expresion,因此不能作例为跟踪条件.如 utf8 utf16 str utf8(rax)

实战如下:

strstr(utf8(reg/addr),"This")  ---> 结果返回1
streq(utf8(00040004E),"abc")   ---> 结果返回0
strlen(utf8(0040004E))         ---> 结果返回0x2B
strlen("abc")            ---> 结果返回3

4.2.2 模块方法

mod.party(addr) ---> 获取模块的模式编号, addr = 0则是用户模块,1则是系统模块
mod.base(addr)  --->  获取模块基址
mod.size(addr)  --->  返回模块大小
mod.hash(addr)  --->  返回模块hash
mod.entry(addr) --->  返回模块入口
mod.system(addr)--->  如果addr是系统模块则为true否则则是false
mod.user(addr)  --->  如果是用户模块则返回true 否则为false
mod.main()      --->  返回主模块基地址
mod.rva(addr)   --->  如果addr不在模块则返回0,否则返回 addr所位于模块的 RVA偏移
mod.offset(addr)--->  获取地址所对应的文件偏移量,如果不在模块则返回0
mod.isexport(addr) ---> 判断该地址是否是从模块导出的函数,true是 false则不是
mod.fromname(str)  ---> 获取模块的基址,注意是str是str
str 0 mod.fromname("ntdll.dll")

使用:

mod.party(0x77960000) --> 返回1 0x77960000是NTDLL,属于系统模块
mod.party(0x00401000) --> 返回0 0x00401000是用户模块
mod.base(0x00401111)  --> 返回  0x00400000 
mod.size(0x00401111)  --> 返回模块大小 
mod.hash(0x00401111)  --> 返回哈希值  16进制
mod.entry(0x00401111) --> 返回OEP入口点
下面同上 不演示

4.2.3 进程方法

peb()   ---> 获取PEB的地址
teb()   ---> 获取TEB的地址
tid()   ---> 获取当前线程的ID
kusd()  ---> 查询X64Dbg 应该是获取用户共享数据 地址

使用:

log "Peb Addr = 0x{peb()} teb addr = 0x{teb()} tid = 0x{tid()}"
--->
peb Addr = 0x268000
teb addr = 0x26b000
tid      = 0x1E30

如果想要显示10进制的 tid(或者其它十六进制值可以改成如下命令)

tid = {d:tid()}
tid == 7728

4.2.4 一般常用方法

bswap(value) 进行字节交换,也就是反转.
ternary(condition,val1,val2) 判断参数1的条件,如果条件为0则返回,否则返回val1 val2
GetTickCount(); 获取刻度值 

用法:

bswap(0x12345678)  --->  0x78563412

4.2.5 内存方法

mem.valid(addr) 判断addr是否有效,有效则返回True
mem.base(addr)  或者当前addr的基址
mem.size(addr)  获取当前addr内存的大小
mem.iscode(addr) 判断当前 addr是否是可执行页面,成功返回TRUE
mem.decodepointer(ptr) 解密指针,相当于调用了API. DecodePointer ptr

4.2.6 内存指针方法

ReadByte(addr / reg); 从addr或者寄存器中读取一个字节内存并且返回
Byte(addr) byte(addr)  同上

-------------
ReadWord(addr)  Word(addr) word(addr) 同上 读取两个字节
ReadDDword(addr) Dword(addr) dword(addr) 同上 读取四个字节
ReadQword(addr) Qword(addr) qword(addr) 同上 读取8个字节,但是只能是64位程序方可使用
ReadPtr(addr) 从地址中读取指针(4/8字节)并返回读取的指针值
ReadPointer(addr) ptr(addr) Pointer(addr) pointer(addr) 都同上

例子:
读取主模块中的内容.

ptr(mod.main()) --> 00905A4D
byte(mod.main()) --> 0x0000004D

4.2.7 反汇编方法

dis.len(addr)      获取addr处的指令长度。
dis.iscond(addr)   判断当前addr位置是否是条件指令(比如jxx) 返回值: 是的话True 否则False
dis.isbranch(addr) 判断当前地址是否是分支指令   返回值: 同上
dis.isret(addr)    判断是否是ret指令          返回值: 同上  
dis.iscall(addr)   判断是否是call指令         返回值: 同上  
dis.ismem(addr)    判断是否是内存操作数        返回值: 同上
dis.isnop(addr)    判断是否是nop             返回值: 同上
dis.isunusual(addr)判断当前地址是否指示为异常地址 返回值: 同上
dis.branchdest(addr):将指令的分支目标位于(如果按 Enter 键,它将遵循什么)。addr
dis.branchexec(addr):如果 分支 at 要执行,则为 true。addr
dis.imm(addr)       获取当前指令位置的立即数(这一行指令中出现的立即数)
dis.brtrue(addr):指令在 的分支目标。addr
dis.brfalse(addr):下一条指令的地址(如果指令 at 是条件分支)。addr
dis.next(addr):    获取addr的下一条地址
dis.prev(addr):    获取addr上一条低地址
dis.iscallsystem(addr) 判断当前指令是否是系统模块指令
dis.mnemonic(addr)返回addr的助记符号,可以当作参数给 字符串函数使用 straddrstr.streq(dis.mnemonic(cip), "cpuid")
dis.text(addr):以字符串的形式返回指令文本。可用作与条件,或者其它函数的输入参数.
dis.match(addr, str):正则表达式匹配,str是正则表达式,判断addr是否命中
例:您可以使用 来查看可以匹配的内容。dis.match(rip,"text.+,0x1")

4.2.8 "方法/Call"" 相关方法

1.引用相关

我们知道 可以在 X64Dbg中 对一个函数 按快捷键 CTRL+R 可以看到有多少地址引用了它.

然后进行分析. 那么 自然我们也有表达式函数供我们使用.

ref.count() 获取当前参考视图中引用的个数
ref.addr(index); 获取当前EIP位置引用此函数的第几个index函数地址. 

ref.addr其实是将所有引用函数列成了一个数组,而我们传入的index就是获取第几个引用函数的索引.

2.参数相关

我们可以使用表达式函数来对我们传入的参数进行操作.

arg.get(index); 获取当前函数堆栈中的第几个参数,假设返回地址在堆栈上,并且我们在函数内部.
arg.set(index,value);设置的索引位置的值为

4.2.9 异常相关

异常在我们调试的时候也很常用, 比如有时候需要获取异常发生的地址.以及异常代码.等信息.

那么表达式函数一样提供了.

ex.firstchance():最后一个异常是否为第一次机会异常。
ex.addr():最后一个异常地址。例如,导致异常的指令的地址。
ex.code():最后一个异常代码。
ex.flags():最后一个异常标志。
ex.infocount():上次异常信息计数(参数数)。
ex.info(index):最后一个异常信息,如果索引超出范围,则为零。

关于异常我们最应该熟悉的就是 异常记录结构体 EXCEPTION_RECORD

这个结构体里面存储了我们的异常信息, 比如异常代码 异常的地址等信息.

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;
  DWORD                    ExceptionFlags;
  struct _EXCEPTION_RECORD *ExceptionRecord;
  PVOID                    ExceptionAddress;
  DWORD                    NumberParameters;
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

请参考MSDN
EXCEPTION_RECORD结构

五丶变量

5.1 保留的变量

在X64Dbg中有一些保留的变量. 分别为如下:

$res / $result    ---> 保存了常规结果的变量
$resN / $resultN  ---> 可选的其它结果变量 N的取值为1 - 4 不能超过5 也不能为0
$pid              ---> 保存了当前进程的Pid
$hp / $hprocess   ---> 保存了调试的可执行句柄
$lastalloc        ---> 保存了最后一次使用脚本命令alloc之后的结果
$breakpointcondition ---> 控制条件断点命令中的暂停行为
$breakpointcounter  ---> 在计算条件断点的条件之前 设置的断点的命中计数器
$breakpointlogcondition  --->条件断点的日志条件
$breakpointerexceptionaddress  --->异常断点设置的异常地址
$breakpointlogcondition        --->日志条件变量

除了上述变量 还有由系统创建的变量 如果是System 创建的 那么只能读取和写入 不能删除.

如果是只读变量,那么由系统创建的变量则只能读取,不能写入或者删除.

使用例子 打开脚本界面(Alt+S) -> 加载脚本 -->脚本中的内容如下:

可以按TAB 单步执行脚本, 也可以按空格直接运行起来脚本

mov $res,1
mov $res,1
mov $res1,2
mov $res2,3
mov $res3,4
mov $res4,5
log "{$res}{$res1}{$res2}{$res3}{$res4}"
log "pid = {$pid}"
log "最后一次申请的内存地址为{$lastalloc}"
log "条件断点暂停条件变量 {$breakpointcondition}"
log "条件断点的计数变量 {$breakpointcounter}"
log "条件断点的日志条件变量 {$breakpointlogcondition}"
ret-->
12345
pid = 9B8
最后一次申请的内存地址为0
条件断点暂停条件变量 0
条件断点的计数变量 0
条件断点的日志条件变量 0

六丶条件断点的应用

6.1 命中断点时x64执行的应用

  1. 如果断点是异常断点,那么变量 $breakpointerexceptionaddress则会被设置

  2. 如果不是那么就递增 计数器($breakpointcounter)

  3. 如果设置的中断条件 那么就计算 表达式的值

    3.1 如果设置的是日志条件 则计算表达式

    3.2 如果设置的是命令条件 则计算表达式

    3.3 表达式的结果只会有两种情况 要么为 1(True) 要么为0 (False)

  4. 日志的条件表达式如果计算为0 那么就会按照日志文本格式打印,(字符串格式参考第二大章)

    4.1 如果设置的命令条件计算结果为1 那么就更新中断条件变量($breakpointcondition)

    并且更新 日志条件变量 ($breakpointlogcondition)

    如果脚本修改了 $breakpointcondition 这个中断条件的系统变量则脚本能控制调试对象是否会被中断.

因为计数器用的可能比较多.所以我们可以使用 命令中的条件断点控制(Api)来清除计数器.

 ResetBreakpointHitCount(addr,newCount) 对addr地址设置为新的计数变量

6.2 条件断点的常规应用.

  • 寄存器值判断

    eax == 1 && ebx == 2; 如果eax == 1 并且 ebx == 2断下 
    
  • 内存值判断

    byte(addr) == 0x4d    如果取出的内存结果为0x4d 则断下 (可以不加前缀0)
    
  • 断点次数命中判断

    $breakpointcounter == 3  
    ($breakpointcounter % 3) == 0
    
  • 线程id断下

    tid() == 1C0
    
  • 重要 断点为字符串的时候断下

以CreateFile为例,如果我们想命中它打开指定文件的时候断下 我们则可以进行如下操作.

strstr(utf8(ecx),"d:\\123") 
或
strstr(utf16([esp + 4]),"123")  == 1   包含123得时候断下
strstr(utf16([esp + 4]),"123") != 1    不包含123得时候断下

其中uft8 utf16在上面已经介绍过了. 我们可以使用这些表达式函数.

七丶内存应用

7.1 将指定内存dump下来到文件中.

使用的指令是 savedata
例子:

savedata c:\1.txt,reg|memaddr, size.

将内存保存到1.txt中. 参数2可以是寄存器,也可以是内存地址. 参数三则是要保存的大小.

posted @ 2022-06-09 14:21  iBinary  阅读(5527)  评论(0编辑  收藏  举报