Windows Reverse (A Dumb Way)
Finally, Parallels Desktop
注: 虽然我不提倡, 但是毕竟难以耐住它贵啊. 一个可能可以的 链接. 我是学生, 请给我学生版 (bushi).
如果不考虑效果的话, 实际上使用 UTM 虚拟机的话我感觉还是很好用的. 能够模拟 PD 无法模拟的一些架构的系统, PD 目前只能模拟和 M1 一样的 arm 架构的一类系统.
坏, 太久没做逆向了, 完全都忘光了. 估计还要复习一下 Linux 系统的逆向.
About Windows
About Windows
对于 Windows 程序, 其中有一个比较要命的东西就是 Windows API. 相比 Linux 逆向遇到的那些用 C 写的简单直白的暴力实现的程序, 感觉在 Windows 程序里面遇到的更多的是一些意义不明的奇怪函数 (API) 调用.
下面的来源于《加密与解密》第 4 版:
通过动态链接库 DLL 来实现调用 API, 主要的 API 有:
API (DLL) | Usage |
---|---|
Kernel (KERNEL32.DLL) | 操作系统核心功能服务, 包含进程和线程控制, 内存管理, 文件访问 |
User (USER32.DLL) | 用户接口, 键盘鼠标, 窗口和菜单管理 |
GDI (GDI32.DLL) | 图形设备接口 |
关于函数的命名:
- 比如说遇到了两个函数
MessageBoxA
和MessageBoxW
(实际上都是MessageBox
), 那么有什么区别呢? - 以
A
结尾的是 ANSI 类型 (单字节方式) - 以
W
结尾的是 Widechars (Unicode) 宽字节方式 - 那么在调试的时候遇到的话, 就可以不用操心这些乱七八糟的东西
- 不过可能会有一个比较有意思的现象, 即在调用
MessageBoxA
的时候, 实际上做了一个变换, 最后将 ANSI 字串变成 Unicode 字串, 然后调用MessageBoxW
(什么皮套人).
一些系统的消息:
- Windows 通过消息驱动, 所以在调试程序的时候可以通过跟踪信息来
- 一些常用的信息函数:
Message Function Usage Return Value SendMessage
调用一个窗口的窗口函数, 将消息发送给窗口 根据具体消息是否投递成功来返回非零 ( TRUE
) 与否.WM_COMMAND
用户从菜单或按钮中选择命令, 或者控件发送消息给父窗口, 或者快捷键发送时 若程序处理消息则返回零 WM_DESTROY
窗口被销毁时发送信息 02h
如果程序处理消息则返回零 WM_GETTEXT
文本复制消息 0dh
返回被复制的字符数量 WM_QUIT
在程序调用 PostQuitMessage
时生成没有返回值 WM_LBUTTONDOWN
光标停在窗口的客户区且按下鼠标左键的时候, 若未被捕获, 则发送给光标下窗口; 否则发送已经捕获鼠标动作的窗口 如果处理, 返回值为零
虚拟内存:
- 实现方法和过程:
- 应用程序启动, 创建进程并分配一个 (2G?) 虚拟地址.
- 虚拟内存管理器将应用程序的代码映射到虚拟地址的某个位置, 将当前需要的代码读入物理地址
- 使用的 DLL 被映射到进程的虚拟地址空间里面, 在需要的时候才读入物理内存
- 堆栈和数据从物理内存开始分配并映射到虚拟地址里
- 应用程序通过使用虚拟地址空间中的地址开始执行, 然后虚拟内存管理器将内存访问映射到物理位置
- 应用程序不会直接访问物理地址
- 虚拟内存管理器通过虚拟地址的访问请求来控制所有的物理地址的访问
注: 我感觉可能需要先编几个 Windows 程序之后才能够学会这些麻烦的东西. 之后如果有了真正理解的话再重新写一遍吧. 现在因为基本就是抄写参考书, 所以注释掉了.
IDA Pro or Ghidra
关于 Ghidra 的话, 之前有一个 介绍.
尽管当时说应该尽量少用盗版, 但是读书人的事, 怎么能叫盗版呢…
IDA Free 或者 Ghidra 实际上就可以满足大部分的功能了, 尽管不一定能够做到尽善尽美就是了.
(但是我可以都要啊, 乐. 哪个更加好用就用哪个, 人没有必要如此专一 bushi. )
WinDbg x64dbg and ret-sync
在 Windows 上有各种的调试工具, 但是巨硬维护了一个叫 WinDbg 的调试工具.
界面花里胡哨的. 好用是好用, 但是有一个问题, 就是在本地调试的时候,
会有各种各样的限制. (甚至连断点也不能, 淦, 是我大意了)
注
好像也不是不行, 网上看到有一个做法是开 2 个 Parrel Desktop 来调试,
不过光开一个我的电脑估计就已经吃不消了. Oh! Mac Air! 你 TMD 可是快一万的电脑啊!
不过尽管不能够用 WinDbg 调试的话, 实际上它还能够用于查看一些符号之类的方便的操作. 用于辅助分析还是非常方便的.
于是我换成了 x64dbg, 其他还有的有 ollydbg 之类的. 各种各样的东西. 可以使用一个叫做 ret-sync 的工具来将这些工具都串联起来, 形成一个比较好用的工作流.
踩了太多坑了, 之后一定要记录一下.
在配置和安装 ret-sync 的过程里面, 还是有非常多的坑的:
- Ghidra 10.2.3: 需要按照 repo 里面的介绍, 先编译. 然后选择 File - Install Extensions…
- x64dbg, x32dbg: 实际上需要对 win32, x86 每个版本都编译一次
- 简单来说, 就是用 VS 打开
ext_x64dbg
文件夹中的x64dbg_sync.sln
文件. 然后选择对应的平台版本, 然后选择 Build 后将生成的文件放到对应的文件夹下. - 对于 x64dbg, 需要 x64 版本, 编译结果是
.dp64
后缀的文件, 放到x64/plugins
文件夹内. - 对于 x32dbg, 需要 Win32 版本, 编译结果是
.dp32
后缀的文件, 放到x32/plugins
文件夹内.
- 简单来说, 就是用 VS 打开
- windbg: 编译的方法和 x64dbg 一样, 需要放到 repo 里面指定的位置后,
在程序中使用
!load sync
后使用.
因为 ret-sync 通过服务器来传递信息, 所以需要配置好服务器地址.
服务器地址需要在 ~/.sync
里面说明. 对于 Windows 系统, ~
在 User/yourname
处.
一个小小的坑就是虚拟机和宿主机的 IP 是不一样的,
所以需要给虚拟机里面的程序指定一个宿主机的 IP:
比如我的电脑的 IP 是 233.333.333.3
, 开放的端口是 9100
, 于是 .sync
应该写成:
[INTERFACE] host=233.333.333.3 port=9100
(注: 一个方便的查看 ip
的方法 ifconfig -a | grep inet
.
尽管在校园网里面有时候挺麻烦的… )
一个展示的例子:
之后会将这些东西作为一个工作流来使用, 方便以后跑路换环境.
这里还是和 GDB 以及 LLDB 来联系在一起来学会比较方便:
Method | Ghidra ret-sync | x64dbg | WinDbg | GDB | LLDB |
---|---|---|---|---|---|
Documents and Help | ret-sync | x64dbg (EN), x64dbg (CN) 0.1 | 标准调试方法, 或者是 ? 获得基础命令列表, .help <cmd> 列出帮助信息 | Debugging with GDB | Tutorial LLDB |
Command Grammar | 以快捷键操作为主 | GUI 为主, 命令: cmd arg1, arg2, ... | 不区分大小写, 元命令 (调试器自带), 以 . 开头, 拓展命令以 ! 开头 | <noun> <verb> [-options [option-value]] [argument [argument ...]] | |
Load Program | sync 同步 | ||||
BreakPoint | F2 断点 | bp , SetBPX 设置; DeleteBPX , bpc 删除 | bp , bu , bm 可以用于设置断点; bl 列出断点 | break [name/addr] 下断点, info break 列出断点列表 | br s [-n name/-a addr] 下断点, br l 列出断点列表 |
Examining Variables | var , varlist 显示变量 | x Module!Symbol 显示符号 | info [args/locals] 显示环境参数与局部变量 p var | fr v [-a/v_name] 列出符号, ta v name | |
List Functions | lm 列出符号表 | info function 列出函数 | image lookup -r -s <FUNC_REGEX> | ||
Run | F5 运行 | g 或者 F5 运行程序 | r 或者 run 运行程序 | process launch 或者 r 或者 run | |
Step | F10 单步步过 | st , StepOver 单步步过 | p 或者 F10 单步执行, 跳过 call 命令 | s 或者 step | s 或者 thread step-in |
Next | F11 单步跟踪 | sti , StepInto 单步步进 | t 或者 F8 或者 F11 , 跟进 call 指令 | n 或者 next | n 或者 next , thread step-over |
Stack | GUI 可以直接看 | k 系列的命令, 给出 stack trace |
就是一些非常简单的操作, 不过我觉得只看操作列表可能根本不知道是什么, 还是写点代码来试试看会比较好一点.
人难免烂俗
尽管我非常讨厌一口气就要吃掉我好多空间的 VS, 但是它毕竟是微软亲儿子 (bushi), 于是忍痛割爱, 删掉了 80G 的地铁. 感到十分惋惜 (bushi).
在 官网 下载 内存毁灭者, 选择安装 Desktop Development with C++.
新建工程选择空白工程… 然后在 Source Files 里面添加代码并编译.
(注: 貌似最新的 Windows 有转译的功能, 可以让 Arm 架构的系统运行 x86 的程序. 唯一需要注意的是, 需要在 VS 的窗口选择 Release (模拟更加真实的环境), x86 (目标架构), 最后选择编译. )
Hello World Example
Hello World Example (Too Easy Skipped)
#include <stdio.h>
int main(void) {
printf("Hello World\n");
return 0;
}
一个吐槽
关于工程这件事, 可能你会认为啊, 我 TMD 就写一个傻逼 Hello World
,
为什么还要搞这么麻烦.
但是还是很有必要的, 项目版本管理, 工程文件管理等等, 都是一个完整项目不应该缺少的东西.
尤其是后期想要把项目做大做强的话 (bushi).
那么使用 Ghidra 来对 VS 编译生成的文件进行逆向:
************************************************************** * FUNCTION * ************************************************************** int __cdecl main(int _Argc, char * * _Argv, char * * _Env) assume FS_OFFSET = 0xffdff000 int EAX:4 <RETURN> int Stack[0x4]:4 _Argc char * * Stack[0x8]:4 _Argv char * * Stack[0xc]:4 _Env _main XREF[1]: __scrt_common_main_seh:00401218( main 00401040 68 00 21 PUSH s_Hello_World_ = »Hello World « 40 00 00401045 e8 c6 ff CALL printf int printf(char * _Format, ...) ff ff 0040104a 83 c4 04 ADD ESP,0x4 0040104d 33 c0 XOR EAX,EAX 0040104f c3 RET
(注: 定位程序的方法还是原来那一套, 根据字符串来找函数的位置. )
那么我们可在打印前和打印后下断点. (注: 这里有一个坑爹的地方, 需要让 x32dbg 进入管理员模式才能够和 Ghidra 进行沟通. )
Simple Branch
Simple Branch
#include <stdio.h>
int check(int passwd) {
int total = 45;
if (passwd < 0) return 0;
if (passwd > 10) return 0;
while (passwd--) total -= passwd;
if (total) return 0;
return 1;
}
int main(void) {
int passwd;
printf("Oh my load, plz give me your passwd:");
scanf("%d", &passwd);
if (check(passwd)) {
puts("Yes, you're my load.");
} else {
puts("Oh wait, who are you?");
}
return 0;
}
这个程序的源代码非常的简单, 之后会试试用这种简单的东西来做一些有趣的事情.
看看它的编译结果
实际上没什么好看的
说实话, 我又不会肉眼编译, 体系结构课程也根本没学, 跟我说什么看汇编代码来理解程序的话, 对现在的我可能有点太难了.
************************************************************** * FUNCTION * ************************************************************** int __cdecl main(int _Argc, char * * _Argv, char * * _Env) assume FS_OFFSET = 0xffdff000 int EAX:4 <RETURN> int Stack[0x4]:4 _Argc char * * Stack[0x8]:4 _Argv char * * Stack[0xc]:4 _Env undefined4 Stack[-0x8]:4 local_8 XREF[2]: 0040108d(W), 004010dd(R) undefined4 Stack[-0xc]:4 local_c XREF[2]: 0040109a(*), 004010a8(R) _main XREF[1]: __scrt_common_main_seh:004012b8( main 00401080 55 PUSH EBP 00401081 8b ec MOV EBP,ESP 00401083 83 ec 08 SUB ESP,0x8 00401086 a1 00 30 MOV EAX,[__security_cookie] = BB40E64Eh 40 00 0040108b 33 c5 XOR EAX,EBP 0040108d 89 45 fc MOV dword ptr [EBP + local_8],EAX 00401090 68 08 21 PUSH s_Oh_my_load,_plz_give_me_your_pa = "Oh my load, plz give me your 40 00 00401095 e8 86 ff CALL printf int printf(char * _Format, ...) ff ff 0040109a 8d 45 f8 LEA EAX=>local_c,[EBP + -0x8] 0040109d 50 PUSH EAX 0040109e 68 30 21 PUSH s_%d = "%d" 40 00 004010a3 e8 a8 ff CALL scanf int scanf(char * _Format, ...) ff ff 004010a8 8b 45 f8 MOV EAX,dword ptr [EBP + local_c] 004010ab 83 c4 0c ADD ESP,0xc 004010ae b9 2d 00 MOV ECX,0x2d 00 00 004010b3 85 c0 TEST EAX,EAX 004010b5 78 1b JS LAB_004010d2 004010b7 83 f8 0a CMP EAX,0xa 004010ba 7f 16 JG LAB_004010d2 004010bc 85 c0 TEST EAX,EAX 004010be 74 12 JZ LAB_004010d2 LAB_004010c0 XREF[1]: 004010c5(j) 004010c0 48 DEC EAX 004010c1 2b c8 SUB ECX,EAX 004010c3 85 c0 TEST EAX,EAX 004010c5 75 f9 JNZ LAB_004010c0 004010c7 85 c9 TEST ECX,ECX 004010c9 75 07 JNZ LAB_004010d2 004010cb 68 34 21 PUSH s_Yes,_you're_my_load. = "Yes, you're my load." 40 00 004010d0 eb 05 JMP LAB_004010d7 LAB_004010d2 XREF[4]: 004010b5(j), 004010ba(j), 004010be(j), 004010c9(j) 004010d2 68 4c 21 PUSH s_Oh_wait,_who_are_you? = "Oh wait, who are you?" 40 00 LAB_004010d7 XREF[1]: 004010d0(j) 004010d7 ff 15 b8 CALL dword ptr [->API-MS-WIN-CRT-STDIO-L1-1-0.DLL:: = 00002834 20 40 00 004010dd 8b 4d fc MOV ECX,dword ptr [EBP + local_8] 004010e0 83 c4 04 ADD ESP,0x4 004010e3 33 cd XOR ECX,EBP 004010e5 33 c0 XOR EAX,EAX 004010e7 e8 04 00 CALL __security_check_cookie void __security_check_cookie(uin 00 00 004010ec 8b e5 MOV ESP,EBP 004010ee 5d POP EBP 004010ef c3 RET
那么直接看反编译的结果吧:
main 函数
int __cdecl main(int _Argc,char **_Argv,char **_Env)
{
int iVar1;
char *_Str;
int local_c;
uint local_8;
local_8 = __security_cookie ^ (uint)&stack0xfffffffc;
printf("Oh my load, plz give me your passwd:");
scanf("%d",&local_c);
iVar1 = 0x2d;
if (((-1 < local_c) && (local_c < 0xb)) && (local_c != 0)) {
do {
local_c = local_c + -1;
iVar1 = iVar1 - local_c;
} while (local_c != 0);
if (iVar1 == 0) {
_Str = "Yes, you\'re my load.";
goto LAB_004010d7;
}
}
_Str = "Oh wait, who are you?";
LAB_004010d7:
puts(_Str);
iVar1 = 0;
__security_check_cookie(local_8 ^ (uint)&stack0xfffffffc);
return iVar1;
}
实际上大部分时间并不会关心这个函数, 看代码的顺序基本上就是:
_Str = "Yes, you\'re my load.";
哦, 原来成功分支在这里iVar1
实际上编译器直接做了优化, 把函数调用给优化掉了的样子. 关于iVar1
的部分的代码实际上就是check
函数里面的内容. 于是想要达成成功分支, 就需要让iVar1
满足等于零的条件.尽管这个可能比较容易求解 (毕竟就只是一个等差数列求和, 也就是
iVar1 == (local_c + 1) * local_c / 2 => local_c = 9
). 如果对于比较困难的问题的话, 可以考虑尝试巨硬的 Z3 或者别的方式来实现. 之后可以找一个比较困难的例子来试试.
Simple Functions
来点稍微复杂一点的程序:
#include <stdio.h>
一些其他的例子
Radare 2
虽然但是, r2 不是一个只面向 Windows 的一个逆向工具.
并且相比其他的几个工具, 它好像不是那么的好学…
你说的对, 但是我希望一个能够不用切出编辑器就能够在终端里面用的工具, 方便我上课作笔记的同时摸鱼
digraph {
rankdir = LR;
node [shape = rect];
}
?
显示帮助命令, 之后的命令就用命令里面的文字来记录- 静态分析部分
aaa
perform deeper analysis, most common useafl
list functions,ii
importss func/addr/sym
seek commandpdf
disassemble function. 如果安装了 r2ghidra 的话, 使用pdg
: Decompile current function with the Ghidra decompiler.VV
程序框图v
visual-mode 可视化模式 Visual-Mode
- 动态调试部分
r2 -d progn
来进行本地的调试db
debug breakpoint,dc
debug continue,ood
restart debug
更加好用的一些较完整的 Cheatsheet:
一些训练
Just in Time 康复训练
关于我被被 Windows 程序薄纱, 选择去做一些简单的 Linux 程序来安慰自己. (Difficulty = 2.0)
例子来源于 crackmes 的 Just in Time.
因为是 stripped
的程序, 所以不太好找入口, 字符串查找大法可能不是那么好用了.
(因为里面做了一个小操作). 于是使用 ltrace
试图碰碰运气:
... putchar(32, 0x55d19180c4c0, 0xc0c0ffffe1ff0000, 0) = 32 strlen("\224\346\362\324\356J\352\314\360\360\370\350\356\322~J") = 16 fgets(Enter password: ...
于是确定应该是通过 fgets
之类的函数来作为入口的节点.
在 gdb
里面用 b putchar
来对 putchar
做断点, r
运行后,
使用 fin
跳出断点, 于是就来到了一些合理的函数里面,
就容易控制程序了.
控制程序的一个抄写:
int check_input(char *input) {
// time = localtime(time);
int shift = ((time % 10) % 3) & 0xf; // code is 0x1f
if (input[0] == '%' && /* % */
input[1] << shift == 200 && /* 0xc8 >> shift */
input[2] << shift == 0xd4 && /* 0xd4 >> shift */
input[3] == 'k' && /* k */
input[4] << shift == 0x50 && /* 0x50 >> shift */
input[5] == '9' && /* 9 */
input[6] == '^' && /* ^ */
input[7] << shift == 0xf6 && /* 0xf6 >> shift */
input[8] == '.' && /* . */
input[9] == 'f' && /* f */
input[10] << shift == 0x80 && /* 0x80 >> shift */
input[11] == '1' && /* 1 */
input[12] == 'F' && /* F */
input[13] << shift == 0x68 /* 0x68 >> shift */
) {
return 0;
} else {
return 1;
}
}
那么差不多就好了吧… 大概吧.
KataVM Level 1 还是康复训练
例子来源于 Crackmes KataVM1
还是 Linux 端的一个例子, 尽管嘲笑我吧, 只会挑简单的来做…
实际上感觉这个问题可能更加适合静态分析而不是动态调试. 不过实际上感觉题目名称已经给了很多的提示了: VM, 不就是虚拟机么. 对于虚拟机类型的问题, 实际上就只要搞清楚每个指令部分在做什么即可. 不过我觉得目前如果让我遇到一个没有提示的 VM 的话, 估计我没法从代码里面看出来.
使用 radare2 列出信息并进入 main
函数部分, 然后用 pdg
查看反汇编:
┌────────────────────────────────────────────────────┐ │ [0x1170] │ │ ; DATA XREF from entry0 @ 0x11e1(r) │ │ 74: int main (int argc, char **argv, char **envp); │ │ endbr64 │ │ sub rsp, 8 │ │ call fcn.000012d0;[oa] │ │ test al, al │ │ jne 0x11ad │ └────────────────────────────────────────────────────┘ f t │ │ │ └────────────────────┐ ┌────────┘ │ │ │ ┌──────────────────────────┐ ┌───────────────────────────────────┐ │ 0x1181 [od] │ │ 0x11ad [oi] │ │ ; "\n[+] Correct!" │ │ ; CODE XREF from main @ 0x117f(x) │ │ lea rdi, [0x00006b22] │ │ ; "\n[!] Invalid. Try harder." │ │ call fcn.000010c0;[oc] │ │ lea rdi, [0x00006b30] │ │ jmp 0x1195 │ │ call fcn.000010c0;[oc] │ └──────────────────────────┘ │ jmp 0x11a6 │ └───────────────────────────────────┘
注: 未完结, 下周先去做别的事情先.