qwen2.5coder发布之后,觉得差不多可以实现离线模型辅助编程了,所以尝试在公司内网部署模型,配合vsocde插件continue实现代码提示、聊天功能。

目前使用qwen2.5coder的32b模型,体验上和gpt-4o差不多(都稀碎),适用于编写脚本,查一些简单问题,例如
flask如何把变量传到前端
,准确率还可以,但是补全功能稀碎。

硬件如下:

cpu gpu 内存
AMD Ryzen 9 5950X 16核 AMD Radeon TX 6900XT(需要安装最新驱动)/
16G显存直接吃满
64G 2600Mhz/
实际吃30G内存

跑起来不算快,和我阅读速度差不多,对这套硬件来说挺吃力的。GPU没怎么跑,似乎主要是cpu在发力吃到60%占用率

部署ollama

安装ollama客户端 && 选择模型

首先去
Download Ollama on Windows
下载ollama的windows版本,安装包非常大,基本上700-800M

在有网络的电脑上安装,然后在
Ollama
这里找到需要的模型,例如这里我选择qwen2.5code的0.5b模型

image

点击第二个箭头
Tags
可以选择不同的量化版本,然后复制第三个箭头的指令

按下
Win+R
快捷键,运行
cmd
,执行复制的命令,比如这里是
ollama run qwen2.5-coder:0.5b

没有魔法的情况下可能会失败,一般情况下多试几次,最差可能需要几十次才能开始下载

找到模型文件及Modelfile内容

image

搜索pull的时候的哈希字符,可以找到模型位置,一般在
C:\Users\Administrator\.ollama\models\blobs

按照时间排序,找到最大的那个文件,就是gguf格式的模型,复制出来,改名为
qwen2.5-coder0.5b.gguf

在命令行执行形如
ollama show qwen2.5-coder:0.5b --modelfile
的指令,可以得到模型的
Modelfile
文件内容,保存为
Modelfile
文件

现在有以下两个文件

image

其中,文件内容是默认提示词模板,可参考
模型文件参考 - Ollama 中文文档
进行修改,例如可以实现让
llama3.3
优先使用中文,这个可以通过在其中加入
请优先使用简体中文回复
,这样的字符实现,最好使用翻译软件翻译成英文再放进去(比如插入到第13行)

  • 修改第五行的FROM,将模型路径修改为模型的真实路径,例如这里是
    ./qwen2.5-coder0.5b.gguf

内网部署ollama

  • 在没有网络的内网电脑中安装第一步下载的ollama安装包
  • 复制上面准备的两个文件到内网

在两个文件所在目录的地址栏输入
cmd
,按下回车

image

命令行中输入
ollama create qwen2.5-coder0.5b -f Modelfile
,其中create后面是你自定义的模型名字(推荐和外网保持一样)

这样就导入进来了,接下来的使用和外网一模一样,输入
ollama list
命令可以看到导入的模型

默认情况下
ollama
会开机启动,如果没有启动,手动执行就行,右下角的托盘图表中应该有它

配置continue

本地使用

Releases · continuedev/continue
这里下载到最新的continue插件,复制到内网,在vscode中安装,可参考
VS Code 安装 VSIX 插件_.vsix-CSDN博客

image

现在,就可以使用模型了

局域网共享

如果项目组中只有一台电脑能运行模型,别的性能不够,需要局域网访问ollama,那么可以按照如下方式调整

ollama

默认它的服务监听
127.0.0.1:11434
端口,这会导致局域网其他机器访问不到,可以参考
Allow listening on all local interfaces · Issue #703 · ollama/ollama
实现监听所有端口

简单来说,就是设定环境变量
OLLAMA_HOST=0.0.0.0
,windows上也是一样的,如下

image

然后重启ollama即可,通过
netstat -ano | findstr 11434
查看是否监听了0.0.0.0

continue

可参考:
https://github.com/continuedev/continue/issues/1175#issuecomment-2081651169

简单来说,在远程主机上,把设置中的以下内容改为指定内容即可

image

    {
      "model": "AUTODETECT",
      "title": "Ollama (Remote)",
      "completionOptions": {},
      "apiBase": "http://192.168.1.100:11434",
      "provider": "ollama"
    }

其中apiBase就是部署了ollama的机器

同学们,大家好,今天开始开设一个新系列,R语言系列,对生信分析中涉及到的R语言代码及R语言绘图进行详细讲解,感兴趣的同学可以重点关注一下。

什么是R语言?

R语言是用于统计分析、绘图的语言和操作环境。R
语言是属于GNU系统的一个自由、免费、源代码开放的软件,它是一个用于统计计算和统计制图的优秀工具。

R语言是S语言的一个分支,可以认为R语言是S语言的一种实现。而S语言是由AT&T贝尔实验室开发的一种用来进行数据探索、统计分析和作图的解释型语言。最初S语言的实现版本主要是S-PLUS。S-PLUS是一个商业软件,它基于S语言,并由MathSoft公司的统计科学部进一步完善。后来新西兰奥克兰大学的Robert Gentleman和Ross Ihaka及其他志愿人员开发了一个R系统,由“R开发核心团队”负责开发。R可以看作贝尔实验室(AT&T BellLaboratories)的Rick Becker、John Chambers和Allan Wilks开发的S语言的一种实现。当然,S语言也是S-Plus的基础。所以,两者在程序语法上可以说是几乎一样的,可能只是在函数方面有细微差别。

为什么选择R语言?

除了R语言以外,市面上也有许多其他流行的统计和制图软件,如Microsoft Excel、SAS、IBM SPSS、Stata以及Minitab。为什么要选择R语言进行数据分析?

R有着非常多值得推荐的特性:

1、多数商业统计软件价格不菲,投入成千上万美元都是可能的。而R是免费的!

2、R是一个全面的统计研究平台,提供了各式各样的数据分析技术。几乎任何类型的数据分析工作皆可在R中完成。

3、R囊括了在其他软件中尚不可用的、先进的统计计算例程。事实上,新方法的更新速度是以周来计算的。

4、R拥有顶尖水准的制图功能。如果希望复杂数据可视化,那么R拥有最全面且最强大的一系列可用功能。

5、R是一个可进行交互式数据分析和探索的强大平台。举例来说,任意一个分析步骤的结果均可被轻松保存、操作,并作为进一步分析的输入。

6、R可以轻松地从各种类型的数据源导入数据,包括文本文件、数据库管理系统、统计软件,乃至专门的数据仓库。它同样可以将数据输出并写入到这些系统中。R也可以直接从网页、社交媒体网站和各种类型的在线数据服务中获取数据。

7、R是一个无与伦比的平台,在其上可使用一种简单而直接的方式编写新的统计方法。它易于扩展,并为快速编程实现新方法提供了一套十分自然的语言。

8、R的功能可以被整合进其他语言编写的应用程序,包括C++、Java、Python、PHP、Pentaho、SAS和SPSS。这让你在不同的语言环境中加入R的功能。

9、R可运行于多种平台之上,包括Windows、UNIX和Mac OS X。这基本上意味着它可以运行于你所能拥有的任何计算机上。

R语言获取及安装

R语言安装文件收存于R语言官方网站CRAN,其链接为:
https://www.r-project.org/


1、进入官方链接,点击
CRAN进行镜像选择(
一个镜像站点(或称镜像)是指另一个站点内容的拷贝。镜像通常用于为相同信息内容提供不同的源,特别是在下载量大的时候提供了一种可靠的网络连接。

2、镜像涉及到下载速度,不过R语言安装文件不大,所以不同国家的镜像或同一国家的不同镜像下载时间相近,这里选择China的清华大学的镜像

3、根据自己电脑系统选择不同的安装包

4、以windows为例,点击
base,然后点击
Download R-4.4.2 for Windows 下载即为R语言安装包

5、找到下载的安装包,双击安装包,按照软件提示即可完成安装

R语言界面

1、主界面打开R语言,其主界面如下,可以在>处输入命令行,按回车键(Enter)运行命令。

2、脚本窗口

由于主页面命令行,只能一行一行运行命令,也可点击File→New script创建一个脚本窗口,可以在窗口写多行命令,然后选中多行命令,点击鼠标右键,选择Run line or selection同时运行多行命令。

R语言界面还涉及其他很多功能和设置,例如设置界面文字大小及代码文字大小、加载R语言包等,不过R语言的IDE(Integrated
Development
Environment,集成开发环境)—Rstudio提供了更为便捷的界面和更多的功能,这里不对R语言界面进行过多介绍。在R语言实际使用过程中,更多情况下都是使用Rstudio进行编程和开发,所以接下来也会对Rstudio进行更详细的介绍。

这是小卷对分布式系统架构学习的第9篇文章,第8篇时只回答了注册中心的工作原理的内容,面试官的第二个问题还没回答,今天再来讲讲各个注册中心的原理,以及区别,最后如何进行选型

上一篇文章:
如何设计一个注册中心?以Zookeeper为例

还是先讲讲各个中间件的区别,zookeeper已经讲过了,这里开始讲其他中间件的工作原理

1. Eureka工作原理

Eureka的官方文档:
Netflix Eureka

不过只有对1.0版本的文档,2.0之后的没有了。

官方对Eureka的解释
:一种基于 REST(表述性状态转移)的服务,主要用于 AWS 云中定位服务,以实现中间层服务器的负载均衡和故障转移。称为 Eureka 服务器。

Eureka解决的需求
是:在AWS中,服务器经常上线/下线,因此AWS需要动态地注册/注销负载均衡器上的服务器,而Eureka就是这样作为中间层负载均衡器出现的。

1.1高可用架构

Eureka在多个机房部署的架构图如下,这也是它高可用的优势

解释说明:

  • 每个区域都部署一个 Eureka 集群,且该集群仅知道其区域内的实例,每个区域内至少有一个 Eureka 服务器,以处理区域故障;
  • 服务向 Eureka 注册,然后每 30 秒发送一次心跳,以续租;如果客户端没有续租,90s后就会从注册中心剔除;
  • 注册信息和续租信息会复制到集群中的所有Eureka节点;
  • 任意客户端可以每隔30s请求一次获取注册信息,用于定位服务提供者,并发起远程调用

1.2 客户端-服务端间的通信

(1)注册Register

Eureka 客户端将运行实例的信息注册到 Eureka 服务器,注册在第一次心跳时发生(30 秒后)

(2)续约机制Renew

客户端每隔30 秒发送一次心跳来续租,通知 Eureka 服务器实例,当前客户端仍然处于存活状态。如果服务器在 90 秒内没有收到续租,它将把实例移出注册表;

  • 续租方式是更新服务对象的最近续约时间,即lastUpdateTimestamp;

(3)获取注册表 Fetch Registry

  • 客户端从服务器获取注册表信息并将其缓存到本地,之后客户端使用该信息表查找其他服务;

  • 此信息会定期(每 30 秒)更新,通过获取上一个提取周期和当前周期之间的增量更新;

  • 增量更新时,如果客户端通过比较注册表信息不匹配,则会请求整个注册表信息全量更新

(4)下线Cancel

Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。下线请求不会自动完成,需手动调用:

DiscoveryManager.getInstance().shutdownComponent()

1.3自我保护机制

默认情况下,Eureka服务端在90s没有收到某个服务实例的心跳,就会注销该实例,将实例下线。如果出现大量实例心跳检测失败,Eureka就会认为是注册中心出现问题了,启动自我保护机制,
不再剔除这些失败实例
。触发条件阈值为:

  • 注册表中超过15%的实例心跳检测失败

1.4 小结

  • Eureka属于AP模型,即牺牲一致性,来换取高可用。在部分阶段失效时,系统仍然能正常运作。但是服务节点间的数据可能不一致
  • Eureka 客户端具备良好的弹性能力,即使与所有 Eureka 服务端的连接断开,它们依然能通过本地缓存机制正常工作
  • 适合跨多机房,对注册中心可用性要求高的场景

2. Nacos工作原理

Nacos官方文档地址:
Nacos架构 2.3版本
,注册中心设计原理文档:
Nacos注册中心

上面的图比较复杂,这里贴下其他人的关于注册中心这部分的架构图

整体流程也就是服务发现那套流程:

  • 服务提供者轮询注册中心集群节点地址,把自己的协议地址注册到Nacos server
  • 服务消费者需要从Nacos Server上去查询服务提供者的地址(根据服务名称)
  • Nacos Server需要感知到服务提供者的上下线的变化
  • 服务消费者需要动态感知到Nacos Server端服务地址的变化

Nacos采用了
Pull和Push同时运作
的方式来保证本地服务实例列表的动态感知。服务消费者通过定时任务的方式每10s Pull一次数据,Nacos Server在服务提供者出现变化时,基于UDP协议PUSH更新

2.1 数据模型

Zookeeper使用的是抽象的树形K-V组织结构,没有专门的数据模型。 Eureka 或者 Consul 都是做到了实例级别的数据扩展。Nacos使用的是
服务-集群-实例
的三层数据模型。

从上图的分级数据模型可以看到:

  • 服务级别:保存了健康检查开关、元数据、路由机制、保护阈值等设置
  • 集群保存了健康检查模式、元数据、同步机制等数据
  • 实例保存了该实例的ip、端口、权重、健康检查状态、下线状态、元数据、响应时间。

2.2 数据一致性协议选择(CP or AP)

Nacos 因为要支持多种服务类型的注册,并能够具有机房容灾、集群扩展等必不可少的能力,是支持AP 和 CP 两种一致性协议的,默认是AP模式

  • 如果注册Nacos的client节点注册时ephemeral=true,那么Nacos集群对这个client节点的效果就是AP,采用distro协议实现;
  • 而注册Nacos的client节点注册时ephemeral=false,那么Nacos集群对这个节点的效果就是CP的,采用raft协议实现。

根据client注册时的属性,AP,CP同时混合存在,只是对不同的client节点效果不同。

Distro 协议则是参考了内部 ConfigServer 和开源 Eureka ,在不借助第三方存储的情况下,实现基本大同小异。Distro 重点是做了一些逻辑的优化和性能的调优。

3.注册中心比较

对比项目 Nacos Eureka Consul Zookeeper
一致性协议 支持AP和CP模式 AP模式 CP模式 CP模式
健康检查 TCP/HTTP/MYSQL/Client Beat Client Beat TCP/HTTP/gRPC/Cmd Keep Alive
负载均衡策略 权重/metadata/Selector Ribbon Fabio -
幂等保护
自动注入实例 支持 支持 不支持 支持
访问协议 HTTP/DNS HTTP HTTP/DNS TCP
监视支持 支持 支持 支持 支持
多数据中心 支持 支持 支持 不支持
跨注册中心同步 支持 支持 不支持 不支持
SpringCloud集成 支持 不支持 支持 不支持
Dubbo集成 支持 不支持 不支持 不支持
k8s集成 支持 不支持 不支持 不支持

3.1选型场景

Nacos

适用场景包括:

  • 微服务架构
    :微服务架构,尤其是需要动态服务发现和配置管理时,Nacos 是一个不错的选择。
  • 云原生应用
    :Nacos 提供了良好的 Kubernetes 支持,适合运行在云环境中的应用。
  • 弹性功能
    :如果系统需要负载均衡和服务治理功能,Nacos 提供强大的支持。

Eureka

  • Spring Cloud 生态系统
    :如果您的项目是基于 Spring Cloud 的,Eureka 是最常用的注册中心,集成非常简单。
  • AP 模式需要
    :适合对一致性要求不高的场景,可以承担部分服务不可用的风险。

Consul

没写关于consul的工作原理,简单列下适用场景:

  • 多数据中心
    :适合大型分布式系统,尤其是需要在多个数据中心之间提供服务发现和注册的场景。

Zookeeper

  • 适合对一致性要求非常高的场景,例如分布式协调、分布式锁等。
  • 复杂的分布式应用
    :在需要严格一致性系统中,如 Hadoop 和 Kafka,Zookeeper 是常见的选择。

.NET 响应式编程 System.Reactive 系列文章(二):深入理解 IObservable<T> 和 IObserver<T>


引言:为什么我们调整了学习顺序?

在上一篇文章的结尾,我原本计划在本篇介绍
System.Reactive
的基础操作符,比如如何创建、转换和过滤数据流。但在撰写内容时,我意识到,对于刚接触
System.Reactive
的读者来说,直接介绍操作符可能有些仓促,因为
操作符的使用必须建立在对
IObservable<T>

IObserver<T>
这两个核心接口的深刻理解之上

正如在传统编程中,你需要先理解
集合(Collection)

迭代器(Iterator)
的本质,才能更好地使用
LINQ
操作符一样。而在 Rx 中,
IObservable<T>
是数据流的生产者,
IObserver<T>
是数据流的消费者

,理解这两个接口是掌握 Rx 的基础。

因此,我决定调整顺序,在本篇文章中,
深入介绍
IObservable<T>

IObserver<T>
的核心概念、方法和使用方式

,为后续学习操作符打下坚实的基础。


IObservable<T> 和 IObserver<T> 的关系

在 Rx 中,数据流的生产和消费是通过
观察者模式(Observer Pattern)
实现的。这种模式定义了两种角色:

  • IObservable<T>
    (可观察对象/数据流的生产者)
  • IObserver<T>
    (观察者/数据流的消费者)

二者的关系可以简单理解为:

  • IObservable<T>
    负责“推送”数据项

  • IObserver<T>
    负责“接收”数据项

订阅(Subscribe)
是连接这两者的桥梁。当
IObserver<T>
订阅一个
IObservable<T>
时,数据流开始传递。


1.
IObservable<T>
的定义和职责

IObservable<T>
接口定义

public interface IObservable<out T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

IObservable<T>
的职责:

  • 代表一个
    数据流
    ,它可以产生零个、一个或多个数据项。
  • 当一个观察者(
    IObserver<T>
    )订阅这个数据流时,它会调用
    Subscribe
    方法,并开始推送数据。
  • 数据流可能会因为
    正常完成

    发生错误
    而终止。


2.
IObserver<T>
的定义和职责

IObserver<T>
接口定义

public interface IObserver<in T>
{
    void OnNext(T value);
    void OnError(Exception error);
    void OnCompleted();
}

IObserver<T>
的职责:

  • 代表一个
    数据的消费者
    ,它对
    IObservable<T>
    提供的数据流做出响应。
  • IObserver<T>
    需要实现三个方法:
    • OnNext(T value)
      :当有新的数据项时调用。
    • OnError(Exception error)
      :当数据流发生错误时调用。
    • OnCompleted()
      :当数据流正常结束时调用。


3.
IObservable<T>

IObserver<T>
的交互流程

让我们通过一个实际的交互流程图来直观地理解
IObservable<T>

IObserver<T>

的关系:

  1. 观察者(Observer)通过
    Subscribe
    方法订阅可观察对象(Observable)。
  2. 可观察对象(Observable)调用 Observer 的
    OnNext
    方法推送数据。
  3. 如果发生错误,可观察对象(Observable)调用
    OnError
    方法终止数据流。
  4. 如果数据流正常结束,可观察对象(Observable)调用
    OnCompleted
    方法终止数据流。

sequenceDiagram
participant Observable as IObservable<T>
participant Observer as IObserver<T>

Observer ->> Observable: Subscribe()
Observable ->> Observer: OnNext(T value)
Observable ->> Observer: OnNext(T value)
alt 数据流正常结束
Observable ->> Observer: OnCompleted()
else 发生错误
Observable ->> Observer: OnError(Exception error)
end


4. 示例代码:实现一个简单的 Observable 和 Observer

为了更好地理解这两个接口,我们从零开始,手动实现一个简单的
IObservable

IObserver

实现自定义 Observable

using System;
using System.Threading;
using System.Threading.Tasks;

public sealed class SimpleObservable : IObservable<int>
{
	IDisposable IObservable<int>.Subscribe(IObserver<int> observer)
	{
		SimpleDisposable disposable = new();

		Task.Run(() =>
		{
			// 模拟数据的生产,以及假设每次生产都需要时间,消费者可以随时调用Dispose方法取消订阅
			for (int i = 1; i <= 5; i++)
			{
				if (disposable.IsDisposed)
				{
					return;
				}
				observer.OnNext(i);
                // 模拟产生数据需要耗时50毫秒
				Thread.Sleep(50);
			}

			observer.OnCompleted();
		});

		return disposable;
	}

	private sealed class SimpleDisposable : IDisposable
	{
		internal bool IsDisposed { get; private set; }
		void IDisposable.Dispose()
		{
			IsDisposed = true;
			Console.WriteLine("Subscription disposed.");
		}
	}
}

实现自定义 Observer

using System;

public sealed class SimpleObserver : IObserver<int>
{
	void IObserver<int>.OnNext(int value) => Console.WriteLine($"Received: {value}");

	void IObserver<int>.OnError(Exception error) => Console.WriteLine($"Error: {error.Message}");

	void IObserver<int>.OnCompleted() => Console.WriteLine("Sequence Completed.");
}

订阅和运行

using System;
using System.Threading;

class Program
{
	static void Main(string[] args)
	{
		IObservable<int> observable = new SimpleObservable();
		IObserver<int> observer = new SimpleObserver();

		IDisposable subscription = observable.Subscribe(observer);

        // 模拟消费数据100毫秒后取消订阅
		Thread.Sleep(100);
		subscription.Dispose();
	}
}

输出结果:

Received: 1
Received: 2
Subscription disposed.


5. 常见问题解答

Q1:为什么
Subscribe
方法返回
IDisposable

Subscribe
方法返回一个
IDisposable
对象,允许订阅者在不再需要数据流时
取消订阅
,以释放资源,避免内存泄漏。

Q2:
OnError

OnCompleted
可以同时调用吗?

不能。
数据流要么以错误终止,要么正常结束
,二者是
互斥的

Q3:
IObservable<T>
可以被多个
IObserver<T>
订阅吗?

可以。一个
IObservable<T>
可以被
多个观察者
订阅,每个观察者都会接收到数据流的推送。


总结

在本篇文章中,我们深入探讨了
IObservable<T>

IObserver<T>

这两个核心接口的定义和职责,并通过代码示例展示了它们如何交互。

核心要点:

  1. IObservable<T>
    是数据流的生产者

    ,它负责推送数据。
  2. IObserver<T>
    是数据流的消费者

    ,它负责接收和处理数据。
  3. Subscribe
    方法将生产者和消费者连接起来

    ,并返回一个
    IDisposable
    对象,用于取消订阅。


下一篇文章预告

《.NET 响应式编程 System.Reactive 系列文章(三):Subscribe 和 IDisposable 的深入理解》
在下一篇文章中,我们将重点探讨
Subscribe
方法的内部工作机制


IDisposable
的作用

,以及如何
优雅地管理订阅的生命周期
。敬请期待!

模板配置

跟着网上的教程使用
evilashz师傅的模板
,下载模板解压至vs的模板目录:

%UserProfile%\Documents\Visual Studio 2022\Templates\ProjectTemplates


image

创建新项目选择刚刚新增的类型:
Beacon Object File
​。


image

环境适配

生成时报错,我使用的是2022版本的,模板有点老了他这里的是vs2019。


image

根据底下的提示从
项目
​ ->
重定目标解决方案
​, 接着确定更新即可


image

但是一进来模板会报错没有引入库, 干脆就用最小测试代码:将
Source.cpp
​重名为
Source.c
​并修改为如下:

#include <stdio.h>
#include <Windows.h>
#include "beacon.h"
void go(char* buff, int len) {
	BeaconPrintf(CALLBACK_OUTPUT, "Hello BOF");
}

编译配置

在上方的生成中勾选BOF配置, 配置管理器的编译环境也一样的,就可以生成64位版本的。


image

但最好还是使用批生成同时生成32位和64位版本:
生成
​ ->
批生成
​ 在BOF那两项勾选
Win32
​和
x64
​。


image

创建项目时没有勾选
将解决方案和项目放在同一目录下
​,那么生成的
.obj
​文件(编译未链接的目标文件)就在
/bin/BOF
​中。


image

测试如果用cs的话可以使用
inline-execute E:\TARGET\timestamp.obj
​。我这里执行成功但发现有乱码:


image

乱码问题

尝试了加上
\n
​换行来终止字符串刷新缓冲区但是不行,找到使用格式化输出宏的办法,将可变参数展开。比如这里的
INFO_FORMAT("Hello BOF");
​会被展开成
BeaconPrintf(CALLBACK_OUTPUT, "[*] Hello BOF\n");
​。

#include <stdio.h>
#include <Windows.h>
#include "beacon.h"

#define INFO_FORMAT(fmt, ...)    BeaconPrintf(CALLBACK_OUTPUT, "[*] " fmt "\n", ##__VA_ARGS__)

void go(char* buff, int len) {
    INFO_FORMAT("Hello BOF");
}

原先的内存中可能是这样:
"Hello BOF" <未知内存内容>
​,但使用宏之后就是这样的:
"[*] Hello BOF\n" <确定的字符串终止>
​,最后测试也没有乱码了。


image

功能实现

实现一个修改文件时间戳的功能, BOF不能直接调用Windows API, 而是通过cs提供的函数来交互。但我这里并不是为cs编写,所以要使用Windows API函数的话,首先需要进行声明:

Windows API声明

要修改文件时间戳, 就要用到
SetFileTime
​。它用于设置文件的创建时间、访问时间和修改时间。文档中原型如下:

BOOL SetFileTime(
  [in]           HANDLE         hFile,
  [in, optional] const FILETIME *lpCreationTime,
  [in, optional] const FILETIME *lpLastAccessTime,
  [in, optional] const FILETIME *lpLastWriteTime
);

  • hFile: 文件句柄,必须有FILE_WRITE_ATTRIBUTES访问权限
  • lpCreationTime: 文件的创建时间
  • lpLastAccessTime: 文件的最后访问时间
  • lpLastWriteTime: 文件的最后修改时间

那么在bof的声明中要注意这个函数是属于哪个dll, 比如这里是
kernel32.dll
​的话那要定义和调用它时就写成
KERNEL32$SetFileTime
​,完整如下:

DECLSPEC_IMPORT BOOL WINAPI KERNEL32$SetFileTime(HANDLE, const FILETIME*, const FILETIME*, const FILETIME*);

cs使用这种前缀可以让BOF直接调用DLL中的原生函数, 就不需要再在导入表中声明了,这样也可以缩小BOF体积。类似的使用
CreateFileA
​来创建或打开文件时, 其原型如下:

HANDLE CreateFileA(
  [in]           LPCSTR                lpFileName, 			  // 文件名
  [in]           DWORD                 dwDesiredAccess,  	  // 访问模式
  [in]           DWORD                 dwShareMode,			  // 共享模式
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,  // 安全描述符
  [in]           DWORD                 dwCreationDisposition, // 创建方式
  [in]           DWORD                 dwFlagsAndAttributes,  // 文件属性
  [in, optional] HANDLE                hTemplateFile		  // 模板文件句柄
);

BOF中声明则如下:

DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);

以及其他要用到的api可以这样声明:

// 其他必要的API
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$CloseHandle(HANDLE); // 关闭一个内核对象(如文件)的句柄
DECLSPEC_IMPORT VOID WINAPI KERNEL32$GetSystemTime(LPSYSTEMTIME); // 获取当前系统时间(UTC时间)
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$SystemTimeToFileTime(LPSYSTEMTIME, LPFILETIME); // 将SYSTEMTIME结构转换为FILETIME结构。

参数处理

BOF的入口函数就是这里的go,
inline-execute
​执行BOF时先调用这个。其中先定义并初始化一个解析器来解析传入的参数,
timestamp
​这个至少也是要一个参数路径的,先从一个来:

void go(char* buff, int len) {
    datap parser;
    char* filepath;

    // 解析Beacon传入的参数
    BeaconDataParse(&parser, buff, len);
    filepath = BeaconDataExtract(&parser, NULL);

    // 参数验证
    if (!filepath) {
        BeaconPrintf(CALLBACK_ERROR, "[-] please provide file path");
        return;
    }
}

那解析多个参数呢, 一样的:

BeaconDataParse(&parser, buff, len);
sourceFile = BeaconDataExtract(&parser, NULL);
targetFile = BeaconDataExtract(&parser, NULL);

if (!sourceFile || !targetFile) {
	BeaconPrintf(CALLBACK_ERROR, "[!] Error: Two file paths required\n");
    BeaconPrintf(CALLBACK_ERROR, "[-] Usage: inline-execute timestamp.o \"source_file\" \"target_file\"\n");
    return;
}

BeaconPrintf(CALLBACK_OUTPUT, "[-] Source: %s\n", sourceFile);
BeaconPrintf(CALLBACK_OUTPUT, "[-] Target: %s\n", targetFile);

时间处理

接着继续,获取系统时间然后修改成我们希望的时间,比如
2020年1月1日 00:00:00
​。然后把他转换为文件时间格式:

SYSTEMTIME st;
FILETIME ft;
KERNEL32$GetSystemTime(&st);

st.wYear = 2020;
st.wMonth = 1;
st.wDay = 1;
st.wHour = 0;
st.wMinute = 0;
st.wSecond = 0;

KERNEL32$SystemTimeToFileTime(&st, &ft);

文件操作

准备好了要修改的时间后就尝试打开文件获取句柄:

HANDLE hFile = KERNEL32$CreateFileA(
    filepath,                         			// 文件路径
    FILE_WRITE_ATTRIBUTES,           			// 只需要写属性权限
    FILE_SHARE_READ | FILE_SHARE_WRITE, 		// 允许其他进程读写
    NULL,                            			// 默认安全属性
    OPEN_EXISTING,                   			// 只打开已存在的文件
    FILE_ATTRIBUTE_NORMAL,          		    // 使用标准属性
    NULL                            			// 不使用模板
);

if (hFile == INVALID_HANDLE_VALUE) {
    BeaconPrintf(CALLBACK_ERROR, "[-] can not open file: %s", filepath);
    return;
}

时间戳修改

最后使用
SetFileTime
​修改三个时间属性:创建时间、访问时间、修改时间。结束后关闭句柄。

if (!KERNEL32$SetFileTime(hFile, &ft, &ft, &ft)) {
    BeaconPrintf(CALLBACK_ERROR, "[-] failed to change timestamp");
} else {
    BeaconPrintf(CALLBACK_OUTPUT, "[+] success: %s", filepath);
}

KERNEL32$CloseHandle(hFile);

这样就简单完成了修改一个文件时间戳的功能,完整代码如下:

#include <stdio.h>
#include <Windows.h>
#include "beacon.h"

// 声明Windows API函数
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$SetFileTime(HANDLE, const FILETIME*, const FILETIME*, const FILETIME*);
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$CreateFileA(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
DECLSPEC_IMPORT VOID WINAPI KERNEL32$GetSystemTime(LPSYSTEMTIME);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$SystemTimeToFileTime(LPSYSTEMTIME, LPFILETIME);

void go(char* buff, int len) {
    datap parser;
    char* filepath;

    BeaconDataParse(&parser, buff, len);
    filepath = BeaconDataExtract(&parser, NULL);

    if (!filepath) {
        BeaconPrintf(CALLBACK_ERROR, "[-] please provide file path");
        return;
    }

    SYSTEMTIME st;
    FILETIME ft;
    KERNEL32$GetSystemTime(&st);

    st.wYear = 2020;
    st.wMonth = 1;
    st.wDay = 1;
    st.wHour = 0;
    st.wMinute = 0;
    st.wSecond = 0;

    KERNEL32$SystemTimeToFileTime(&st, &ft);

    HANDLE hFile = KERNEL32$CreateFileA(
        filepath,
        FILE_WRITE_ATTRIBUTES,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        BeaconPrintf(CALLBACK_ERROR, "[-] can not open file: %s", filepath);
        return;
    }

    if (!KERNEL32$SetFileTime(hFile, &ft, &ft, &ft)) {
        BeaconPrintf(CALLBACK_ERROR, "[-] failed to change timestamp");
    }
    else {
        BeaconPrintf(CALLBACK_OUTPUT, "[+] sunccess: %s", filepath);
    }

    KERNEL32$CloseHandle(hFile);
}

测试

编译还是同上使用批生成,我这里测试的可以成功修改:


image

优化编译

为了更好的在苛刻环境下使用,我想继续压缩体积,找到的参数以及解释如下:

  • -Os: 优化大小(比-O2生成更小的代码)
  • -fno-asynchronous-unwind-tables: 禁用异常展开表
  • -fno-ident: 删除编译器版本信息
  • -fpack-struct=8: 结构体8字节对齐
  • -falign-functions=1: 函数1字节对齐
  • -s: 删除符号表
  • -ffunction-sections: 每个函数放入单独的段
  • -fdata-sections: 每个数据项放入单独的段
  • -fno-exceptions: 禁用异常处理
  • -fno-stack-protector: 禁用栈保护
  • -mno-stack-arg-probe: 禁用栈探测

64位使用的编译命令如下:

x86_64-w64-mingw32-gcc-8.1.0.exe -c .\Source.c -o timestamp.o -Os -fno-asynchronous-unwind-tables -fno-ident -fpack-struct=8 -falign-functions=1 -s -ffunction-sections -fdata-sections -fno-exceptions -fno-stack-protector -mno-stack-arg-probe

针对于编译32位版本的命令( 如果没有就用批生成, 重命名即可):

i686-w64-mingw32-gcc-8.1.0.exe -c .\Source.c -o timestamp.x86.o -Os -fno-asynchronous-unwind-tables -fno-ident -fpack-struct=8 -falign-functions=1 -s -ffunction-sections -fdata-sections -fno-exceptions -fno-stack-protector -mno-stack-arg-probe

注:这里生成的是.o而不是.obj只是自己的需求为了统一一下,obj是Windows平台的默认目标文件扩展名,而.o是Unix/Linux平台的扩展名。它们本质和功能上是一样的,只是命名习惯不同。

最后

这里只是简单的示例,要使用最好要有一个锚定文件,以他的时间作为目标来修改。细节不赘述,详细请跳转
Github
。最终版本的使用测试如下:


image

参考