Android hook、检测及对抗相关

news2024/11/26 11:33:07

frida——hook 内存访问断点 

        环境:app:arm64      python 3.10   frida 15.2.2   

        简单的内存访问断点代码,可能还有些bug,根据apk需要自己改,下文为在apk中指定的地址调用函数时内存断点才被激活,以下需要改动:

                var str_name_so = "********";                           // 需要hook的so名
                var n_addr_func_offset = ********;                   // 需要hook的函数的偏移
                var ret_addr = *******;                                     //hook函数的返回地址

                process = frida.get_remote_device().attach('*****')  # 进程名

        frida hook原理:通过python代码将jscode插入到指定函数的起始位置,通过b x16跳转到插入代码,首先执行Interceptor.attach,离开被hook函数时执行onLeave:function,app执行过程中触发异常时进入Process.setExceptionHandler,剩余根据自己使用情况可精简。

        details.address(出现异常的地址)

        details.memory.address(访问了哪里触发异常)

import frida, sysjscode = """
Java.perform(function(){var str_name_so = "libVuforia.so";                   // 需要hook的so名var n_addr_func_offset = ********;                   // 需要hook的函数的偏移var n_addr_so = Module.findBaseAddress(str_name_so); // 获取so模块基址var ret_addr = *******;                             //hook函数的返回地址send('n_addr_so:' + n_addr_so.toString(16));var addr = 0;             // 内存断点地址地址var size = 0x8c;          // 内存断点大小var bpt_addr = 0;         // 需要下断点的地址var o_code = 0;           // 原机器码var tmp = 0;//输出bpt_addr处的硬编码//console.log(hexdump(bpt_addr, {offset: 0,length: 48,header: true,ansi: false}));// 需要hook 的地址 = 模块基址 + 函数偏移var n_addr_func = parseInt(n_addr_so, 16) + n_addr_func_offset; // 创建NativePointer对象var ptr_func = new NativePointer(n_addr_func);// 处理异常回调函数Process.setExceptionHandler(function(details){// 输出异常信息send("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");send("触发异常偏移地址:" + (details.address - n_addr_so).toString(16));send("常规断点偏移地址:" + (bpt_addr - n_addr_so).toString(16));send("异常类型:" + details.type.toString(16));// 判断异常是否由正常断点(0xcc)触发if(details.type == 'illegal-instruction'){send("常规断点触发");// 修改为可写权限var ret1 = Memory.protect(ptr(bpt_addr), size, '-w-');if (ret1 == false){send("设置软件断点后内存断点权限 修改 失败");return false;};// 写回原机器码bpt_addr.writeU64(o_code);// 权限改回var ret2 = Memory.protect(ptr(bpt_addr), size, 'r-x');if (ret2 == false){send("设置软件断点后内存断点权限 改回 失败");return false;};send("常规断点取消");bpt_addr = 0;// 再次修改内存断点区域权限var ret3 = Memory.protect(ptr(addr), size, '--x');// 权限修改失败则输出提示if(ret3 == false){send(addr.toString(16) + "处权限修改失败:" + ret3.toString(16))}else(send(addr.toString(16) + "处内存断点再次激活:" + ret3.toString(16)));return true;            };// 判断异常类型是否是访问异常if(details.type == 'access-violation'){send("异常操作类型:" + details.memory.operation.toString(16));        send("异常访问的地址:" + details.memory.address.toString(16));//  访问  的位置是否在范围内if(details.memory.address - addr <= size && details.memory.address - addr >= 0){//  当前指令是否在文件的代码段if(details.address - n_addr_so > 0 && details.address - n_addr_so < 14276064){send(details.address - n_addr_so);send("目标地址已找到:" + (details.address - n_addr_so).toString(16));send("触发异常的地址:" + details.address);send("异常访问的地址:" + details.memory.address);send("内存访问断点地址:" + addr);send("基质:" + n_addr_so);return false;}else{send("########超出文件的访问地址##########");console.log(hexdump(details.address, {offset: 0,length: 48,header: true,ansi: false}));return false;}}  if(details.memory.address - addr > size || details.memory.address - addr < 0 ){// 计算需要在哪里下软件断点bpt_addr = ptr(parseInt(details.address, 16) + 4 );// 保存原机器码o_code = bpt_addr.readU64();// 修改为可写权限var ret4 = Memory.protect(ptr(bpt_addr), size, '-w-');if(ret4 == false){send("常规断点设置失败:写入异常失败!");return false;};// 写入异常bpt_addr.writeS64(0xcccccccc);// 权限改回var ret5 = Memory.protect(ptr(bpt_addr ), size, 'r-x');if(ret5 == false){send("常规断点设置失败:还原权限失败!");send(bpt_addr);send("发生异常的地址:" + (details.address - n_addr_so));return false;};send("设置常规断点");// 将内存断点处的权限改回来if(details.memory.operation == 'read'){var ret6 = Memory.protect(ptr(addr), size, 'r--');if (ret6 == false){send("设置软件断点后内存断点权限修改失败");return false;};}else if(details.memory.operation == 'write'){var ret7 = Memory.protect(ptr(addr), size, '-w-');if (ret7 == false){send("设置软件断点后内存断点权限修改失败");return false;};}else{send("不应该出现执行的情况" + details.memory.operation);return false;}send("内存断点暂时失效");return true;};          };        send("未知错误");});// 截获对函数的调用Interceptor.attach(ptr_func,{ // onEnter 回调函数:该函数在被hook函数之前执行,其参数args可用于读取或修改目标函数的参数onEnter: function(args) {//判断函数调用 被hook函数执行后ret的地址tmp = this.context.lr - n_addr_so;if(tmp != ret_addr) return true;send("目标调用");// 从寄存器中取出要下内存断点的地址addr = parseInt(args[7], 16) + 376;}, onLeave: function(retval){if(0 == addr ) return true;send("--------------");// 修改目标地址的权限var ret = Memory.protect(ptr(addr), size, 'x');// 权限修改失败则输出提示if(ret == false){send(addr.toString(16) + "处权限修改失败:" + ret.toString(16))}else(send(addr.toString(16) + "处权限修改成功:" + ret.toString(16)));}});
});
"""
def printMessage(message, data):        # js中执行send函数后要回调的函数if message['type'] == 'send':print('[*] {0}'.format(message['payload']))else:print(message)process = frida.get_remote_device().attach('*****')  # 进程名
script = process.create_script(jscode)  # 创建脚本
script.on('message', printMessage)      # 加载回调函数,也就是js中执行send函数规定要执行的python函数
script.load()                           # 加载脚本
sys.stdin.read()

Inline-Hook和SandHook 

        都是基于inline-hook基础,通过修改函数的跳转地址实现hook,br  mycode_addr;据说inline主要用于32位hook,sandhook主要用于64位hook。

PLT/GOT hook

        全局偏移表(GOT)和动态链接表(PLT),主要通过解析so文件,将导出表函数地址替换为自己的native函数地址实现hook,导入表hook本质上和导出表hook原理相同,只不过hook的是系统so或者其他so的导出表,优点是相比于Inline-hook稳定性更好,缺点是只能hook导入导出表

Unicorn hook(没太看懂,貌似很牛逼)

        跨平台的模拟框架,通过模拟不同平台的cpu实现,内部并没有函数的概念,只是一个单纯执行指令的cpu,可以实现指令级hook

 Xposed hook

        原理:Java函数执行时会调用到dvmCallMethodV判断是Java层还是native层函数,xposed通过修改dvmCallMethod的accessFlags值来欺骗虚拟机为调用native层函数,再将nativeFunc指针指向自己实现的native方法();

                Android版本高于5.0时,使用xposed需要刷入框架,替换系统的

        1.重写系统的Zygote,加入一段代码,所有从zygote fork出的进程都注入XposedBridge.jar ,使用XposedHelpers.findAndHookMethod 方法hook系统函数。

        2.通过反射机制找到Method,判断安卓版本,初始化参数与返回值类型。

        3.修改函数指针。

万物皆可 Hook,探究 Xposed 框架 Hook 原理 - 知乎 (zhihu.com) (hook代码及xposed检测)

Frida hook

        原理:和Xopsed类似,也是欺骗虚拟机执行自己的native函数,修改accessflags,函数指针,函数执行模式。

        1.函数是否 为AOT 编译的热点函数,是则直接执行。

        2.Java层函数调用,未经过AOT编译,ARTMethod 的值为artQuickToInterpreterBridge(快速编译代码的入口点),从二进制执行模式(AOT)切换到边解释边执行(JIT)模式

        3.Native层函数调用,未经过AOT编译,ARTMethod的值为artQuickGenericJniTrampoline 

        所以Frida会更改ARTmethod结构中的:

        access_flags_(执行的是Java层还是Native层)entry_point_from_jni_art_quick_generic_jni_trampolineartInterpreterToCompiledCodeBridge

        检测:frida开启的端口、被注入进程中是否存在frida-agent-32.so模块、检测进程是否有frida-server、探测端口通信D-bus协议。

Xposed 与 frida 的区别:

        Xposed是通过修改字节码实现hook,因此只能搞Java层函数,且安装需要root权限,稳定性较好。

        frida是通过向程序注入JavaScript脚本,动态二进制插桩实现,所以native层函数也可以搞,不用root,上去就是干,但是稳定性较差,没事就崩给你看。

APK简单对抗

1.Java层混淆:jadx -> 工具 -> 反混淆

2.资源加密:特点 直接用Android killer打开,反编译失败,资源文件报错;在手机MT管理器中更改代码。

3.签名校验:

         java层:关键API getPackageManager(获取包管理器)  getPackageInfo(获取包信息)  packageInfo.signatures(包签名信息)   

        so层:先找到执行签名校验的so文件,拖入IDA,查看导出表,check_sign jni_onload(动态注册) java_(静态注册)等关键函数,分析校验逻辑更改so或者java层文件。 

通过Java反射机制调用签名校验Java反射机制详解_杨 戬的博客-CSDN博客。

PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);Signature[] signs = packageInfo.signatures;

4.模拟器检测:检测蓝牙,电话信息权限,已安装的APK,CPU框架等。

5.类抽取:将dex文件中的函数指令转为NOP,程序跑起来时在so层填充该函数指令。

正向还原:第一种是通过读取/proc/<pid>/maps文件获取加载的DEX文件地址,然后对DEX文件进行解析,找到NOP指令进行还原;第二种方法就是通过Hook系统函数(最简单的就是dexFindClass函数)然后进行指令还原。

逆向还原:最终解密出的Java函数还是要在内存中的,使用IDA动态调试拿到原本的函数字节码,填充回dex,或者同样hook系统函数,等程序填充后在dump dex。

dex方法还原:

        使用 010Editor 找到该方法的类(在class区段中找)

         选中并打开类数据标签(2),打开方法列表(3,下图中有两个列表),在列表中找到参数类型与返回值类型与咱们的目标函数相同的方法,此次实验目标为public void org.a.a.m.c.a(char[], int, int)方法。

         按照下图打开选项卡,找到方法对应的16进制数据,进行填充或者删除,最后还需要更改dex文件头中的SHA1(或者其他的?)校验信息。

 

6.第四代VMP壳:程序启动时,将加密后的dex文件解密,加载到内存;在内存中创建一个虚拟机,将dex转换为vmp虚拟指令集;再将指令集转换为Dalvik字节码执行(用到那个dex中的方法就解密哪个,不是一次性将dex转为Dalvik字节码)

7.函数级加密:

        在应用程序运行时,解密函数会被调用来解密每个被加密的函数,然后将其加载到内存中,并在执行时动态解密。

        静态函数级加密:每个函数加密方式同,拿到解密函数还原。

         动态函数级加密:每个函数加密方式同;动态函数级加密中,每个函数使用不同的密钥进行加密,获取每个函数对应的密钥;找出解密函数,解密函数使用密钥对每个函数进行解密。

8.不落地加载壳:

        先执行壳程序,在内存中解密程序本身的dex,在so层调用系统 libdvm.so 中的 openDexFile 函数加载内存中的dex,该函数的返回类型为int类型的cookie。

9.so加壳:

        通过加壳器对原始so文件进行混淆与加密,运行时在解密,详见 Android杂项 中的自定义linker

10.ollvm混淆:

        控制流平坦化:听起来好像很难懂,其实就是把原本直接的函数调用,前面加上各种 if 、for、switch、while等分支判断的逻辑,加大逆向分析的时间开销,函数逻辑变得混乱。

  • (1)函数的开始地址为序言的地址;
  • (2)序言的后继块为主分发器;
  • (3)后继为主分发器的块为预处理器;
  • (4)后继为预处理器的块为真实块;
  • (5)无后继的块为retn块;
  • (6)剩下的为无用块。

下图为经过ollvm控制流平坦化处理的函数

         去混淆的思路就是去除无用块及确定真实块执行的先后顺序。

        指令替换:将程序原本简单的二元运算(加 减 乘 除 异或 与 等等),替换为更加复杂的运算,但结果和原本一样(PS:纯纯有病,技术不行,cpu来凑?)

        控制流伪造:和平坦化差不多,就是 if 、for、switch、while 循环的替换,加大分析难度,让cpu更容易冒烟。

Android反调试

1.关键文件检测:改文件名。

2.调试端口检测:更改ida,frida等默认连接端口;

3.进程名检测:android_server gdbserver gdb等进程名检测;父进程名检测,桌面启动的父进程名应为zygote,hook 或修改smali 

4.Java层反调试:android.os.Debug.isDebuggerConnected(),killer中搜索函数更改;xml中applicationInfo属性中添加android:debuggable=“true”。hook该函数让其返回1;

5.ptrace检测(相当于Windows中的attch):手动跟踪到ptrace函数修改返回值;编写新的ptrace函数so文件放到app的lib下并设置环境变量;hook大法,

6.TracerPid 检测:基本同上,检测/proc/self/status的TracerPid 值。不为0就是被调试了,通过修改内核文件/dev/block/mmcblk0/boot中TracerPid函数硬编码实现;程序建立子线程附加自己,如果TracerPid值为0则退出;。

7.断点检测:rtld_db_dlactivity函数:这个函数默认情况下为空函数,这里的值应该为0,而当有调试器时,这里会被改为断点指令;获取该函数地址,将其nop。

8.时间检测,单步调试陷阱:app主动设置断点,并在代码中注册断点信号处理函数,未被调试(执行断点发出信号—进入处理信号函数—NOP替换断点—退回PC),被调试,执行调试程序的处理函数,他会恢复目标处指令失败,然后回退PC,进入死循环。

9.利用ida先截获信号的特性:将关键函数放在信号处理函数中,未被执行则被调试。

10.双进程守护:主进程a,fork子进程b,b进程ptrace a的所有线程;修改安卓源码;

Java层的双进程通过xposed hook应该可以解决(xposed是通过重写zygote进程实现hook,理论上可以拦截所有Java层的操作);so层的可以在该so刚加载时就附加,然后修改fork操作(未实践)。或者编写一个so库,里面重写ptrace和dlopen函数,重写的函数里面最后在调用真正的系统函数,加载该库。

Android风控

        风险控制,为了解决和预防将要发生,或者可能发生的一些危险情况,从而减轻损失。

        注:并不完全是新内容,选读,因为近期面试遇到过不少,所以自己学学之后打算写个笔记加深一下印象。

蜜罐数据:当发现作弊以后返回的数据是非正常的数据,可能存在埋点等信息,比如在视频的随机帧中添加标识,水印等,返回错误,重复的数据。

IP限制:当一个IP请求过量或过于频繁时,返回错误数据或者蜜罐数据。

设备指纹:设备指纹的核心是使用设备的唯一识别码,就像每个人的身份证号码一样。

java函数获取:Settings. Secure / Global .getString(context.getContentResolver(),Settings.Secure.ANDROID_ID)

黑灰产通过协议逆向,实现批量注册等“薅羊毛”行为,可通过网络数据包中的设备指纹判断,是否为同一设备,短时间内大量注册账号,从而判定是否遭到黑产攻击。

App环境信息:可以通过该设备运行环境的信息,将用户划分为 可疑设备 与 黑产设备。

1.root检测:su权限,文件检测;扫描 magisk 关键字;maps检测等。

2.Hook检测:主流hook框架Frida与xposed检测上报。

3.沙箱检测:判断APK的私有路径是否为 /data/data/包名;

4.自定义rom检测:需要有手机的驱动源码才能自定义rom,通过对比md5值等检测是否存在自定义rom。

5.查杀分离:当当前设备被服务器判定为黑产时,不会立即封杀,而是延时几小时或者几天在封杀,防止黑产一次一步步摸清风控判断依据。

6.心跳包&用户行为检测:在app刚跑来就与服务器建立socket连接,每隔一段时间发送一个心跳包,如果通过单纯的逆向网络协议实现算法接口,再通过PC发送数据的话,服务端在长时间没有收到用户的心跳包后,就可以将此设备判定为黑产设备。用户通过逆向字节写的点击框架,可能没有一点多余的操作,如正常用户的滑动屏幕,点击时间间隔,屏幕点击坐标等,也可以以此作为风控判定标准。

7.异常&行为埋点:正常用户点击登入接口时,在之前肯定会打开app的登入页面,可以在登入页面设置埋点,发送特定的数据包给服务器,如果用户只发送了登入的数据包,或者发送的埋点数据不足时,可以此判定该设备为黑产设备。

Android 进程注入

动态注入      

        由于Linux的进程机制,每个进程在安装后都会分配一个独享的UID,所以要想达到进程注入,需要拿到系统的root权限,之后总体流程和PC端大同小异;

获取进程pid

附加进程                    :Attch(pid)

保存目标进程寄存器  :GetRegs(pid,&SaveRegister)

目标进程内申请空间  :通过mmap映射申请,与binder差不太多

写入shell                    :

修改PC寄存器,

执行shell。

静态注入

        通过修改ELF文件实现,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/701216.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【GESP】2023年03月图形化二级 -- 快乐时光

文章目录 快乐时光1. 准备工作2. 功能实现3. 设计思路与实现&#xff08;1&#xff09;角色、舞台背景设置a. 角色设置b. 舞台背景设置 &#xff08;2&#xff09;脚本编写a. 角色&#xff1a;小猫b. 角色&#xff1a;小猴 4. 评分标准 快乐时光 1. 准备工作 &#xff08;1&am…

CSS知识点汇总(十)--移动端适配

文章目录 怎么做移动端的样式适配&#xff1f;1、方案选择2. iPhoneX 适配方案 怎么做移动端的样式适配&#xff1f; 在移动端虽然整体来说大部分浏览器内核都是 webkit&#xff0c;而且大部分都支持 css3 的所有语法。但手机屏幕尺寸不一样&#xff0c;分辨率不一样&#xff0…

jenkins共享库配置及设计

jenkins共享库做模块封装时遇到的问题总结&#xff1a; 背景描述:使用jenkins共享库对SCM subversion操作进行封装时&#xff0c;使用了Checkout插件&#xff0c;生成的检出脚本代码为 checkout([$class: SubversionSCM, additionalCredentials: [], excludedCommitMessages: …

【Dashy安装使用】本地Linux 部署 Dashy 并远程访问

文章目录 简介1. 安装Dashy2. 安装cpolar3.配置公网访问地址4. 固定域名访问 转载自cpolar极点云文章&#xff1a;本地Linux 部署 Dashy 并远程访问 简介 Dashy 是一个开源的自托管的导航页配置服务&#xff0c;具有易于使用的可视化编辑器、状态检查、小工具和主题等功能。你…

spring源码编译笔记

下载源码 地址https://github.com/spring-projects/spring-framework/tree/v5.2.9.RELEASE 查看gradle对应版本 spring-framework-5.2.9.RELEASE/gradle/wrapper/gradle-wrapper.properties # 其他配置暂未了解具体用处&#xff0c;一切默认值 distributionBaseGRADLE_USE…

vue3案例

代码 <template><div class"app-page"><divv-for"title in data.titleArr":key"title.id"class"title-box"click"onClick(title)"><span>{{ title.name }}</span><img v-if"title…

Java基础——正则表达式

1 概述 正则表达式用于匹配规定格式的字符串。 除了上面的以外&#xff0c;还有一个符号就是括号&#xff0c;括号括起来的表示一个捕获组&#xff0c;一个捕获组可以作为一个重复单位来处理。 2 使用 2.1 判断是否匹配 String自带了一个可以使用正则表达式判断字符串是…

卸载 Navicat:正版 MySQL客户端,真香!

最近看到一款数据库客户端工具&#xff0c;DataGrip&#xff0c;是大名鼎鼎的JetBrains公司出品的&#xff0c;就是那个出品Intellij IDEA的公司。DataGrip是一款数据库管理客户端工具&#xff0c;方便连接到数据库服务器&#xff0c;执行sql、创建表、创建索引以及导出数据等。…

【Python从入门到进阶】25、urllib获取快餐网站店铺数据

接上篇《24、urllib获取网站电影排行》 上一篇我们讲解了如何使用urllib的get请求抓取某某电影排行榜信息。本篇我们来讲解如何使用urllib的post请求抓取某某快餐网站店铺数据。 一、某某快餐网站介绍 1、某某快餐网站 某某快餐店网址为&#xff1a;http://www.kfc.com.cn/k…

SciencePub学术 | 国人友好类重点SCIEEI征稿中

SciencePub学术 刊源推荐&#xff1a;国人友好类重点SCIE&EI征稿中&#xff01;信息如下&#xff0c;录满为止&#xff1a; 一、期刊概况&#xff1a; 二、期刊要求 1. 论文为原创&#xff0c;未公开发表&#xff0c;初稿可提交中文版本&#xff0c;终稿必须是英文版本; …

Redis 跳表skiplist

跳跃表 在单链表中查询一个元素的时间复杂度为O(n)&#xff0c;即使该单链表是有序的&#xff0c;我们也不能通过2分的方式缩减时间复杂度。 跳跃表(skiplist)是一种有序数据结构&#xff0c;它通过在每个节点中维持多个指向其他节点的指针(注&#xff1a;可以理解为维护了多条…

【计算机网络】数据链路层之随机接入-CSMA/CA协议(无线局域网)

1.概念 2.无线局域网可否实现碰撞检验CD 3.方案 CSMA/CA 碰撞避免 4. 两种帧间间隔 IFS 为什么需要等待DIFS? 为什么需要等待SIFS? 为什么还要退避一段时间才能使用信道&#xff1f; 5.退避算法 使用退避算法的情况 退避算法 举例 6.信道预约 7.虚拟载波监听 8.题目 9.解析 …

分享一个上传按钮

先看效果&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>上传</title><link rel"stylesheet" href"https://fonts.googleapis.com/css2…

面对Android开发高薪诱惑,如何拿到大厂offer?

前言 出来打工最看重的就是薪资&#xff0c;作为一名5年开发经验的Android人员。在如今的大环境中薪资一降再降&#xff0c;还没人要。技术还还算可以但是面试一直被刷下来&#xff1f;这是为什么&#xff0c;本篇我们来聊聊Android开发面试中的关键。 面试过程 通常面试过程…

【Vue3】学习笔记-watch函数

与Vue2.x中watch配置功能一致 两个小“坑”&#xff1a; 监视reactive定义的响应式数据时&#xff1a;oldValue无法正确获取、强制开启了深度监视&#xff08;deep配置失效&#xff09;。监视reactive定义的响应式数据中某个属性时&#xff1a;deep配置有效。 <template&…

数据分析三大件

一、jupyter的基本使用 二、Numpy 2.1 numpy的创建 #使用array&#xff08;&#xff09;创建一个多维数组 import numpy as np arrnp.array([1,2,3])2.2 numpy的属性 修改数组的元素类型 2.3 索引和切片 &#xff08;1&#xff09;行切片 &#xff08;2&#xff09;列切片 注…

高压线路距离保护程序逻辑原理(二)

二、选相子程序原理 距离保护的故障处理程序逻辑的第一步是判别故障相&#xff0c;即选相。只有判定了故障的种类及相别&#xff0c;才能确定阻抗计算应取用什么相别的电流和电压&#xff0c;例如BC相故障取和&#xff0c;A相接地故障取和十3&#xff08;详见第二章第二节解微…

远程桌面连接已开启无法连接?快解析助力远程访问

一、如何开启远程桌面 查询并记录远程计算机的IP&#xff0c;点击“开始——运行”&#xff0c;输入“cmd”命令后回车&#xff0c;准确查看并记录ipadress在计算机上右键&#xff0c;选择属性&#xff0c;点击远程设置&#xff0c;在弹出来的设置界面中&#xff0c;勾选“允许…

HFUT Data Structure Experiment: SkipList

写给我的学弟 如果你看到了这个题&#xff0c;赶快跑&#xff0c;千万别选。 这个题的图形化会让你非常痛苦。并且这道题只有小小85分&#xff0c;为啥不换个85分的更简单的&#xff1f;或者换个90分以上的题不好吗。 如果你单单想学习一下这个数据结构&#xff0c;那挺好的&…

xftp下载安装及简单使用

一、xftp简介 Xftp是一个功能强大的SFTP、FTP 文件传输软件。使用了 Xftp 以后&#xff0c;MS Windows 用户能安全地在 UNIX/Linux 和 Windows PC 之间传输文件。Xftp 能同时适应初级用户和高级用户的需要。它采用了标准的 Windows 风格的向导&#xff0c;它简单的界面能与其他…