articles
articles copied to clipboard
逆向简单随笔
逆向tips(简单随笔)
下面列出的就是一般程序中常用的从网络套接字中读取数据的 API 函数
read/write
recv/send
recvfrom/sendto
WSARecv/WSASend
WSARecvFrom/WSASendTo
ioctl octlsocket
WSARecvDisconnect/WSASendDisconnect
WSARecvEx/WSASendEx
recvmsg/sendmsg
WSARecvMsg/WSASendMsg
函数堆栈初始化
push ebp //栈底压栈
mov ebp,esp //栈底下移,更详细的请参考我关于ebp,esp的解释
sub esp,0CCh //局部变量预留空间
push ebx //保存ebx
push esi //保存esi
push edi //保存edi
lea edi,[ebp-0CCh] //下移edi到栈顶
mov ecx,33h //0CCh/4 = 33h
mov eax,0CCCCCCCCh //eax赋值
rep stos dword ptr es:[edi] //从edi开始做33h次赋值0CCCCCCCCh ,初始化栈内存
运行时错误检查
mov esi, esp
mov eax, [ebp+Test.m_nInt]
push eax
push offset Format ; "CTest: %d \r\n"
call ds:__imp__printf
add esp, 8
cmp esi, esp
call j___RTC_CheckEsp
call j_?eof@?$char_traits@D@std@@SAHXZ ; std::char_traits<char>::eof(void)
mov esi, esp
主要体现在
mov esi, esp
cmp esi, esp
c++ 类 this 指针使用
- 通过ecx传递(调用方式thiscall) c++ 源代码
class CTest {
public:
//void __stdcall SetNumber(int nNumber) {
void SetNumber(int nNumber) {
m_nInt = nNumber;
}
public:
int m_nInt;
};
int _tmain(int argc, _TCHAR* argv[])
{
CTest Test;
Test.SetNumber(5);
printf("CTest: %d \r\n", Test.m_nInt);
std::cin.ignore();
}
对应的反汇编代码
009513EE 6A 05 push 0x5
009513F0 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8] ; 获取this指针
009513F3 E8 F8FCFFFF call test.009510F0
009513F8 8BF4 mov esi,esp
009513FA 8B45 F8 mov eax,dword ptr ss:[ebp-0x8] ; kernel32.7649336A
009513FD 50 push eax ; kernel32.BaseThreadInitThunk
009513FE 68 30589500 push test.00955830 ; CTest: %d \r\n
00951403 FF15 50839500 call dword ptr ds:[<&MSVCR100D.printf>] ; msvcr100.printf
- 通过参数传递
c++ 源代码(调用方式__stdcall)
class CTest {
public:
void __stdcall SetNumber(int nNumber) {
m_nInt = nNumber;
}
public:
int m_nInt;
};
int _tmain(int argc, _TCHAR* argv[])
{
CTest Test;
Test.SetNumber(5);
printf("CTest: %d \r\n", Test.m_nInt);
}
对应的反汇编代码
011513EE 6A 05 push 0x5
011513F0 8D45 F8 lea eax,dword ptr ss:[ebp-0x8] ;this指针使用参数传递
011513F3 50 push eax ; kernel32.BaseThreadInitThunk
011513F4 E8 F7FCFFFF call test.011510F0
011514E0 test.CTest::SetNumberc_erro> 55 push ebp
011514E1 8BEC mov ebp,esp
011514E3 81EC C0000000 sub esp,0xC0
011514E9 53 push ebx
011514EA 56 push esi
011514EB 57 push edi
011514EC 8DBD 40FFFFFF lea edi,dword ptr ss:[ebp-0xC0]
011514F2 B9 30000000 mov ecx,0x30
011514F7 B8 CCCCCCCC mov eax,0xCCCCCCCC
011514FC F3:AB rep stos dword ptr es:[edi]
011514FE 8B45 08 mov eax,dword ptr ss:[ebp+0x8] ;取得this指针(第一个参数)
01151501 8B4D 0C mov ecx,dword ptr ss:[ebp+0xC] ;取得第二个参数
01151504 8908 mov dword ptr ds:[eax],ecx
01151506 5F pop edi ; kernel32.7649336A
01151507 5E pop esi ; kernel32.7649336A
01151508 5B pop ebx ; kernel32.7649336A
01151509 8BE5 mov esp,ebp
0115150B 5D pop ebp ; kernel32.7649336A
0115150C C2 0800 retn 0x8
TIPS
-
类中对象的数据成员传参顺序:最先定义的数据成员最后压栈,最后定义的数据成员最先压栈。
-
类中没有数据成员,而编译器为我们添加了vptr
-
在调试泡泡堂程序的过程中,在send设下了断点,一直提示设置的断点范围不在程序范围内,解决办法
在设置里
options->debug options->security->warn when breakpoint is outside the code section
问题
线程发包 ==> 跳出线程
心跳包的地址不固定,但是每次运行开始,到进入游戏,心跳发包的线程应该是一样的,因为socket id和相关的buff地址是一样的
ollydbg单线程调试,一个线程启动,别的线程挂起。
ollydbg自带的调用栈不可信,应该使用堆栈里面提供的调用栈,这个才是实时的调用栈。
如何从send函数中跳出
原理就是跟踪写入send参数buff的函数,可以这样解释
send(socket_id, len, buff, flags),为了程序的性能问题,一般的游戏会把send/recv放在线程中,加解密又是在另一部分,send函数肯定不会改变buff的数据值,那么能够改变buff值的一般也就是加解密函数了,所以我们使用内存断点在buff的地址设下写断点,这样就可以找到加解密改变buff数据的函数了,同样的也就成功的跳出了,发包线程,不会一直在线程中循环。
关于壳
壳一般是对程序进行压缩或者加密,无法进行动态或者静态调试
动态调试:
- 成功脱壳,之后进行调试
- 壳一般会检测调试软件的进程,发现了调试进程就直接退出,如果能够不让其发现调试进程或是骗过调试进程,就能够成功的进行程序的调试(刘庆民的od就是利用了这个方式),但是这里也有疑问,这个壳没有去除,是否会影响对于代码的检查和程序逻辑思路的推理呢?
可以根据刘庆民的绕过方法,进行进一步的探索延伸学习!
关于函数参数地址传递参数的理解
void f1 (int x, int y, int *sum, int *product)
{
*sum=x+y;
*product=x*y;
};
int sum, product;
int main()
{
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d", sum, product);
return 0;
};
对应的反汇编代码
sum DD 01H DUP (?)
product DD 01H DUP (?)
$SG5336 DB 'sum=%d, product=%d', 00H
EXTRN ___acrt_iob_func:PROC
EXTRN ___stdio_common_vfprintf:PROC
_main PROC
push ebp
mov ebp, esp
push OFFSET ?product@@3HA
push OFFSET ?sum@@3HA
push 456 ; 000001c8H
push 123 ; 0000007bH
call ?f1@@YAXHHPAH0@Z
add esp, 16 ; 00000010H
mov eax, DWORD PTR product
push eax
mov ecx, DWORD PTR sum
push ecx
push OFFSET $SG5336
call _printf
add esp, 12 ; 0000000cH
xor eax, eax
pop ebp
ret 0
_main ENDP
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
f1 PROC
push ebp
mov ebp, esp
sum DD 01H DUP (?)
product DD 01H DUP (?)
$SG5336 DB 'sum=%d, product=%d', 00H
EXTRN ___acrt_iob_func:PROC
EXTRN ___stdio_common_vfprintf:PROC
_main PROC
push ebp
mov ebp, esp
push OFFSET ?product@@3HA
push OFFSET ?sum@@3HA
push 456 ; 000001c8H
push 123 ; 0000007bH
call ?f1@@YAXHHPAH0@Z
add esp, 16 ; 00000010H
mov eax, DWORD PTR product
push eax
mov ecx, DWORD PTR sum
push ecx
push OFFSET $SG5336
call _printf
add esp, 12 ; 0000000cH
xor eax, eax
pop ebp
ret 0
_main ENDP
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
f1 PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
mov ecx, DWORD PTR _sum$[ebp]
mov DWORD PTR [ecx], eax
mov edx, DWORD PTR _x$[ebp]
imul edx, DWORD PTR _y$[ebp]
mov eax, DWORD PTR _product$[ebp]
mov DWORD PTR [eax], edx
pop ebp
ret 0
f1 ENDP
pop ebp
ret 0
f1 ENDP
这里有一个地方是我一直不理解的,其中有问号的地方
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
mov ecx, DWORD PTR _sum$[ebp] ???
mov DWORD PTR [ecx], eax
mov edx, DWORD PTR _x$[ebp]
imul edx, DWORD PTR _y$[ebp]
mov eax, DWORD PTR _product$[ebp] ???
mov DWORD PTR [eax], edx
f1函数中直接将参数的值给了ecx和eax,既然是地址传递,我一直觉得这里应该用lea命令,将参数的地址赋值出来,但是我们结合main函数的汇编代码,我们就能看出来了,main 函数里面push的就是地址,所以我们可以知道,ebp+n的参数值本身就是一个地址,我们要是利用lea,取地址之后再取地址,这里不是我们所期望的,所以正确的应该是将ebp+n的值(也就是参数的地址)给ecx/eax,之后再将计算的值放入该地址,也就完成了函数的指针传递。
for 循环反汇编基础代码
#include <stdio.h>
void f(int i)
{
printf ("f(%d)", i);
};
int main()
{
int i;
for (i=2; i<10; i++)
{
f(i);
}
return 0;
};
对应的反汇编代码
_i$ = -4
_main PROC
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _i$[ebp], 2 ; loop initialization
jmp SHORT $LN3@main
$LN2@main:
mov eax, DWORD PTR _i$[ebp] ; here is what we do after each iteration:
add eax, 1 ; add 1 to i value
mov DWORD PTR _i$[ebp], eax
$LN3@main:
cmp DWORD PTR _i$[ebp], 10 ; this condition is checked *before* each iteration
jge SHORT $LN1@main ; if i is biggest or equals to 10, let’s finish loop
mov ecx, DWORD PTR _i$[ebp] ; loop body: call f(i)
push ecx
call _f
add esp, 4
jmp SHORT $LN2@main ; jump to loop begin
$LN1@main: ; loop end
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
一般分为这几个部分
- 开始的初始化
- 从初始化跳转到循环的主体执行,主体的开始部分会先判断循环的条件
- 循环主题执行完毕,会跳入一个过程,主要是i++
关于fs寄存器
偏移 说明
000 指向SEH链指针
004 线程堆栈顶部
008 线程堆栈底部
00C SubSystemTib
010 FiberData
014 ArbitraryUserPointer
018 FS段寄存器在内存中的镜像地址
020 进程PID
024 线程ID
02C 指向线程局部存储指针
030 PEB结构地址(进程结构)
034 上个错误号
Win64 下 gs:[30] 是存放 TIB 块的 Self 指针,而在 Win32 下是 fs:[18]
反汇编数组
数组的计数一般存在ecx中,数组的基址靠近esp,msvc一般是[ebp-n+i],n值是其所需的所有的字节数,i的不断递增,访问整个数组。
dword ptr ds[eax-n]
eax并不会解引用,会直接用eax里面的值进行运算,其实细细类比一下就可以理解了,可以从两方面理解
- 有中括号的这种才会出现解引用
- 平时利用堆栈ss:[ebp-4],也是直接用ebp里面的地址,并不会解引用!
寄存器在参数传递中的作用
⚠️
lea ecx, ss:[ebp-n]
push ecx
call eax
很明显,ecx是通过指针的方式传递的,但是函数执行完之后,ecx的值很有可能会发生改变,我们传递这个地址,只是表明该地址在该函数中的数据会发生改变,不要简单的想着ecx的值不会改变,所以要注意一下这种地址的传递方式!
函数的查看、跟踪等,主要就是看参数和返回值,特别是传递地址时,因为c++会特别多的使用到指针。
对齐方式????
调用方式总结
调用方式 | 参数入栈方式 | 恢复栈平衡 | 使用语言 |
---|---|---|---|
_stdcall | 右—>左 | 子函数 | C++ |
_fastcall | 右—>左 | 子函数 | Delphi |
_cdcall | 右—>左 | 母函数 | C |
delphi 逆向
参数传递与c/c++不同点:
参数1------> EAX
参数2------> EDX
参数3------> ECX
其余参数通过栈的方式传递
在逆向delphi时,不能只看这几个参数所涉及到的寄存器或者是栈数据,比如
00457C96 FF75 F4 push dword ptr ss:[ebp-0xC]
00457C99 8D83 18030000 lea eax,dword ptr ds:[ebx+0x318]
00457C9F BA 04000000 mov edx,0x4
00457CA4 E8 93BFFAFF call CKme.00403C3C
如果仅仅只根据寄存器和栈,函数00403C3C的参数就只是这三个
ss:[ebp-0xc] 最先入栈,最右边的参数
eax 参数1
edx 参数2
在通过ida pro来看看伪代码
int v4; // ebx
int v6; // ST08_4
int v7; // ST04_4
int v8; // ecx
int v35; // [esp+2Ch] [ebp-Ch]
System::__linkproc__ LStrCatN(v4 + 792, 4, v8, v6, v7, v35);
这个是ida pro分析过后的代码,其实我们简单点分析,可以这样分析
sub_00403c3c(eax, edx, ss:[ebp-0x0c])
这样分析完全没有任何问题,函数内部是通过edx来连接几个字符串!
反汇编代码反应了代码的本质,一般也是逻辑最简洁的,感觉有疑问,直接跟进去,多运行几下就可以了
反汇编函数调用
被调用函数能够影响调用函数的方式主要有:
- lea 直接将地址(相当于指针)直接当参数传递给被调用函数
- ds数据段构成的全局变量
- 返回的eax
主要的几种调用方式stdcall,fastcall等都是子函数维持栈的平衡,所以子函数在返回前一般会保持栈平衡
在crackme160 004的逆向中遇到的函数00403c3c这样的“递归”函数(非真正意义上的递归函数),在最后也会保持栈平衡,但是在ida中如果想得到伪代码,还是需要更改汇编代码才行
根据寄存器的值设置条件断点
如果需要根据函数的参数设置断点,应该是记录esp的值,而不是ebp的值
逆向工具使用
IDA
-
*
查看结构信息 - N重命名标号
OD
####特定模块搜索
Ctrl+E->选中模块->右键->Follow entry,进入需要查找的模块,然后右键->中文搜索引擎->智能搜索
c++逆向重点代码
求字符串长度(release)
mov edi, [ebp+n] ; 获取参数数据
mov ecx, 0xffffffffh ; ecx=-1
xor eax, eax
repne scasb
not ecx
dec ecx
难记的汇编指令
-
leave相当于
mov esp, ebp pop ebp
-
repe/repne 相等则重复/不想等重复,一般连同scasb使用
repne scasb
scasb 比较al和di,相当于cmp al, di
-
Xor eax, ebx 用于判断两数是否相等
-
cdq 将一个32位有符合数扩展为64位有符号数,数据能表示的数不变,具体是这样实现的,比如eax=fffffffb(值为-5),然后cdq把eax的最高位bit,也就是二进制1,全部复制到edx的每一个bit位,EDX 变成 FFFFFFFF,这时eax与edx连起来就是一个64位数,FFFFFFFF FFFFFFFB ,它是一个 64 bit 的大型数字,数值依旧是 -5。
-
cmovx x指的是比较形式
IDA pro
快捷键
Ctl+l 搜索所有的函数名称
g 跳转到特定位置
Alt+t 搜索特定的汇编指令
Shift+F12 列出所有字符串
点windows API函数
-
SetupDiGetDeviceRegistryProperty 查看设备属性(vmware, vbox...)
BOOL SetupDiGetDeviceRegistryProperty( _In_ HDEVINFO DeviceInfoSet, _In_ PSP_DEVINFO_DATA DeviceInfoData, _In_ DWORD Property, _Out_opt_ PDWORD PropertyRegDataType, _Out_opt_ PBYTE PropertyBuffer, //指明设备名称 vmware, vbox, qemu _In_ DWORD PropertyBufferSize, _Out_opt_ PDWORD RequiredSize );
领悟
有很多情况下,遇到问题没法解决,想了很久也没有任何的思路,可以跳出这个问题,看别的,之后再来看,没准也就可以理解了,想通了。
主要有这种方式
([UNICODE[esp+10]] !="BAR") && ([UNICODE[esp]] =="FOO")
[[STRING[esp+8]] =="FOO"] && [[STRING[esp+4]] !="BAR"]
并且设置在条件满足是记录相对应的数据
proc near
push esi
mov esi, ecx
mov dword ptr [esi], offset off_40C0D0
mov dword ptr [esi+4], 0BBh
call sub_401EB0
add esp, 18h
pop esi
retn
endp
汇编栈的使用
局部数组变量的存储是连续的,但是如果是通过参数传递进来的,并且这些数据使用的是栈进行临时存储,那么从局部的函数看的话,这些数据是不连续的,局部的函数栈帧中只是存储了这些数组的首字节地址
-----
| | ===> 字符串首地址 这种字符串是通过参数传递进来的
-----
| | ===> 其他数据
-----
这种是局部数组
-----
| | ===> 字符串后四个字符
-----
| | ===> 字符串前四个字符
-----
问题
函数的入口:
mainctrstartup
_tmainctrstartup
windows字符串类型
type | Meaning in MBCS builds | Meaning in Unicode builds |
---|---|---|
WCHAR |
wchar_t |
wchar_t |
LPSTR |
zero-terminated string of char (char* ) |
zero-terminated string of char (char* ) |
LPCSTR |
constant zero-terminated string of char (const char* ) |
constant zero-terminated string of char (const char* ) |
LPWSTR |
zero-terminated Unicode string (wchar_t* ) |
zero-terminated Unicode string (wchar_t* ) |
LPCWSTR |
constant zero-terminated Unicode string (const wchar_t* ) |
constant zero-terminated Unicode string (const wchar_t* ) |
TCHAR |
char |
wchar_t |
LPTSTR |
zero-terminated string of TCHAR (TCHAR* ) |
zero-terminated string of TCHAR (TCHAR* ) |
LPCTSTR |
constant zero-terminated string of TCHAR (const TCHAR* ) |
constant zero-terminated string of TCHAR (const TCHAR* ) |
##重点消息数值
WM_CLOSE(16)
WM_INITDIALOG(272)
WM_COMMAND(273)
汇编串操作
移动串指令: **MOVSB**、**MOVSW**、**MOVSD** ;从 ESI -> EDI; 执行后, ESI 与 EDI 的地址移动相应的单位****
比较串指令: **CMPSB**、**CMPSW**、**CMPSD** ;比较 ESI、EDI; 执行后, ESI 与 EDI 的地址移动相应的单位****
扫描串指令: **SCASB**、**SCASW**、**SCASD** ;依据 AL/AX/EAX 中的数据扫描 EDI 指向的数据, 执行后 EDI 自动变化****
储存串指令: **STOSB**、**STOSW**、**STOSD** ;将 AL/AX/EAX 中的数据储存到 EDI 给出的地址, 执行后 EDI 自动变化****
载入串指令: **LODSB**、**LODSW**、**LODSD** ;将 ESI 指向的数据载入到 AL/AX/EAX, 执行后 ESI 自动变化****
其中的 B、W、D 分别指 ****Byte、********Word、********DWord, 表示每次操作的数据的大小单位.
上述指令可以有重复前缀:
**REP** ECX**** > 0**** 时
**REPE** (或 **REPZ**) ECX**** > 0**** 且 ZF=1**** 时
**REPNE**(或 **REPNZ**) ECX**** > 0**** 且 ZF=0**** 时
;重复前缀可以自动按单位(1、2、4)递减 ECX****
GetLastError()从 TEB 的 LastErrorValue 处得到错误码。
##调试子进程 建议用windbg
用命令
.childdbg 1
经典猜题思路
-
作者是懂汇编的,且程序是用汇编写的。因为3连call只能是人为设计的
-
write up中有意思的猜想:
- 作者修改了_scanf的代码,获取input数据,并且以此修改key处的数据。
- 利用缓冲区溢出漏洞,retn跳转到其他地方,在那里输入的值被获取,并且处理,然后影响注册的成败
我其实一开始以为的是第一种可能性,就差逆出scanf算法看他有什么猫腻了。。结果想想其实不是,因为IDA识别出了他是_scanf这个函数。如果作者修改了这个函数,IDA可能识别不出来这是个_scanf。
疑难点(知识盲点)
cdecl栈平衡
cdecl的栈平衡是通过母函数实现的,这句话主要的含义是这样的
push eax
push ecx
call xx.40xxxx
add esp, 0x8
并不是说其内部的栈是不平衡的,而是说参数的入栈是需要通过母函数来进行平衡的!