美文网首页逆向 & 外挂
Android逆向分析概述

Android逆向分析概述

作者: oncealong | 来源:发表于2018-05-31 14:01 被阅读30次

    学习逆向的初衷是想系统学习Android下的hook技术和工具, 想系统学习Android的hook技术和工具是因为Android移动性能实战这本书. 这本书里用hook技术hook一些关键函数来计算关键函数的调用参数和调用时长, 从而确定性能问题发生的位置和原因. 但目前没有比较系统的讲解hook的书籍, 所以就系统的了解下逆向分析.

    在读了姜维的Android应用安全防护和逆向分析和丰生强的Android软件安全与逆向分析后, 准备分享下这方面知识. 在写文章时发现, 这两本书缺少对最新的逆向工具和加固工具的描述. 在查阅相关文献后补充了这一部分.

    本文从五个维度来讲解Android逆向, 每个维度尽量分'原理', '工具', '实例'三个方面.

    • 反编译

    • 静态分析

    • 动态分析

    • 重编译

    • Docker

    1.反编译

    1.1 原理

    Android App正向编译

    说到反编译, 先来看下正向编译, 如上图, 正向编译是

    java -> class -> dex -> apk

    反编译和正向编译稍有不同, 反编译可以分成两类:

    java <- smali <- dex <- apk

    这种方法是将dex文件转为smali, smali是Dalvik虚拟机的汇编语言, 可以用来动态调试程序.

    java <- class/jar <- dex <- apk

    这种方法中是将Dalvik字节码转化为等价的Java字节码, 然后用丰富的java分析工具分析源码.

    如何反反编译(即对抗反编译):

    • 阅读反编译工具源码查找缺陷

    • 压力测试找反编译工具bug(下载很多apk, 写个脚本调用ApkTool反编译这些apk, ApkTool因为某些bug无法反编译某个apk, 这时我们就通过压力测试找到了ApkTool的bug, 将发现的这个应用到我们的apk中, 即可保护我们的apk免受ApkTool反编译)

    如何反反反编译呢:

    • 阅读反编译源码修复缺陷

    1.2 工具

    反编译工具-dex-class

    上图的反编译工具走的java <- class/jar <- dex <- apk路线, 即先把apk里的dex找到, 然后使用Enjarify/dex2jar/classyshark/jadx反编译得到jar包, 然后使用jd-gui/CFR/Procyon阅读jar包里的java源码. 这些工具各有优缺点, 我们一般选择dex2jar+jd-gui, 相比其他工具, jd-gui虽然很久不更新了, 但是支持跳转, 方便查看代码. 特别说明下Bytecode-Viewer, 其是Procyon的一个前端, 同时集成了很多其他工具, 功能强大.

    反编译工具-dex-smali

    看下上图, 这些工具走的是java <- class/jar <- dex <- apk路线. 将dex文件转化为smali汇编, 然后直接阅读smali汇编语言, 或者smali再转为java(这里没有强大的工具, 可能经常无法成功转化).

    最常用反编译工具

    从上图可以看到有很多反编译工具, 我们平时最常用的是dex2jar+jd-guiApkTool.

    jd-gui不仅有不错的界面, 最关键的是支持类之间的跳转, 在混淆后的代码中跳转可以大大方便我们查看.

    ApkTool隐隐有无冕之王的声势, 可以反编译代码和资源, 修改后可以重编译成apk, 在Android Studio下使用smalidea插件还可以完成无源码调试, 十分强大.

    工具地址:

    https://github.com/Storyyeller/enjarify

    https://github.com/pxb1988/dex2jar

    https://github.com/google/android-classyshark

    https://github.com/skylot/jadx

    https://github.com/java-decompiler/jd-gui

    http://www.benf.org/other/cfr/

    https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler

    https://github.com/Konloch/bytecode-viewer

    https://github.com/deathmarine/Luyten

    http://www.secureteam.net/d4j

    https://github.com/iBotPeaches/Apktool

    https://github.com/demitsuri/smali2java

    https://www.pnfsoftware.com/

    1.3 实例

    这里以一个实例说明下反反编译和反反反编译:

    使用早期ApkTool反编译apk时,可能会遇到反编译失败, 出现如下问题:

    Exception in thread "main" brut.androlib.AndrolibException: Multiple res specs: attr/name
    at brut.androlib.res.data.ResTypeSpec.addResSpec(ResTypeSpec.java:78)
    at brut.androlib.res.decoder.ARSCDecoder.readEntry(ARSCDecoder.java:248)
    at brut.androlib.res.decoder.ARSCDecoder.readTableType(ARSCDecoder.java:212)
    at brut.androlib.res.decoder.ARSCDecoder.readTableTypeSpec(ARSCDecoder.java:154)
    at brut.androlib.res.decoder.ARSCDecoder.readTablePackage(ARSCDecoder.java:116)
    at brut.androlib.res.decoder.ARSCDecoder.readTableHeader(ARSCDecoder.java:78)
    at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:47)
    at brut.androlib.res.AndrolibResources.getResPackagesFromApk(AndrolibResources.java:544)
    at brut.androlib.res.AndrolibResources.loadMainPkg(AndrolibResources.java:63)
    at brut.androlib.res.AndrolibResources.getResTable(AndrolibResources.java:55)
    at brut.androlib.Androlib.getResTable(Androlib.java:66)
    at brut.androlib.ApkDecoder.setTargetSdkVersion(ApkDecoder.java:198)
    at brut.androlib.ApkDecoder.decode(ApkDecoder.java:96)
    at brut.apktool.Main.cmdDecode(Main.java:165)
    at brut.apktool.Main.main(Main.java:81)
    

    查看ApkTool代码发现, 是Apk利用了ApkTool的一个bug, Apk做了混淆,在编译时存入了重复id值,导致ApkTool crash.

    针对这个问题, 解决办法是create fake names to prevent abuse from duplicate key names, 其github提交如下:

    create fake names to prevent abuse from duplicate key names

    实例地址:

    https://github.com/iBotPeaches/Apktool/commit/567907b187ad2f78b3564d0a0405e3b207832e17

    2.静态分析

    2.1 原理

    什么是静态分析?

    不运行代码,采用反编译工具生成程序的反编译代码,然后阅读反编译代码来掌握程序功能.

    Android静态分析步骤:

    • 反编译apk程序

    • 查看Application类(在Activity启动之前, 一般加固/授权放在这里)

    • 查看MainActivity类

    • 找关键代码

    反静态分析:

    • 代码混淆(ProGuard等)

    • 使用NDK+STL编写

    • 手动注册native函数()

      • 默认情况, 使用javah, com.example.k12 -> java_完整包名类名方法名.
        但可以使用函数映射表 static JNINativeMethod methods[] = { {"dynamicGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void *) native_dynamic_key}}; RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)来注册native函数名,
        提高破解难度.
    • 加固(dex/so加壳,指令抽取等)

    反反静态分析:

    • 定位关键代码技巧

      • 信息反馈法(点击界面, 出现注册失败, 那么检查代码里哪里使用到了注册失败)

      • 特征函数法/关键系统调用(一般情况下, 最终都会调用到系统函数. 为了提升难度, 可以自制和系统函数功能相同的函数, 这样难以下断点)

      • Log代码注入法/栈跟踪法(动静分析结合, 在合适位置注入log, 编译运行时可以打印当前上下文信息和堆栈信息)

    • IDA分析汇编(asm->c, 虽然很多函数还没重定位, 但是c比汇编的表达力更强, 更便于分析)

    • 脱壳

      • IDA脱壳(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 无论如何, 最终都是要调用系统API加载dex, 在这里加断点, 然后dump出内存中的dex文件[现在一些加固工具都是自己写加载dex的函数, 这样简单在上述方法上加断点是无法命中的])

      • Xposed/VirtualXposed

    Dex文件格式

    这里不详细介绍,
    感兴趣参考"https://blog.csdn.net/jiangwei0910410003/article/details/50668549"

    Dalvik指令集

    空指令 寄存器数据操作指令 返回指令 数据定义指令 锁指令 实例操作指令

    数组/字段操作指令 异常指令 跳转指令 比较指令 方法调用指令 数据转换指令

    数据运算指令

    .field private isFlag:z  定义变量

    .method  方法

    .parameter  方法参数

    .prologue  方法开始

    .line 12  此方法位于第12行

    return-void  函数返回void

    .end method  函数结束

    new-instance  创建实例

    iput-object  对象赋值

    iget-object  调用对象

    invoke-static  调用静态函数条件跳转分支:

    invoke-super  调用父函数

    invoke-direct  调用函数

    "if-eq vA, vB, :cond_**" 如果vA等于vB则跳转到:cond_**
    "if-ne vA, vB, :cond_**" 如果vA不等于vB则跳转到:cond_**
    "if-lt vA, vB, :cond_**" 如果vA小于vB则跳转到:cond_**
    "if-ge vA, vB, :cond_**" 如果vA大于等于vB则跳转到:cond_**
    "if-gt vA, vB, :cond_**" 如果vA大于vB则跳转到:cond_**
    "if-le vA, vB, :cond_**" 如果vA小于等于vB则跳转到:cond_**
    "if-eqz vA, :cond_**" 如果vA等于0则跳转到:cond_**
    "if-nez vA, :cond_**" 如果vA不等于0则跳转到:cond_**
    "if-ltz vA, :cond_**" 如果vA小于0则跳转到:cond_**
    "if-gez vA, :cond_**" 如果vA大于等于0则跳转到:cond_**
    "if-gtz vA, :cond_**" 如果vA大于0则跳转到:cond_**
    "if-lez vA, :cond_**" 如果vA小于等于0则跳转到:cond_**

    这里主要关注跳转指令, 因为我们逆向Apk时, 一般只关注特殊的几点逻辑,
    注意跳转语句跳转到了哪些特殊函数.

    ELF文件格式和寻址方式

    这里不详细介绍,
    感兴趣的同学可以参考"https://blog.csdn.net/jiangwei0910410003/article/details/49336613"

    Arm汇编语法

    跳转指令 存储器访问指令 数据处理指令(加减乘除)

    空操作 软中断

    arm汇编里我们主要关注如下函数调用语句:

    BL 执行函数调用

    BLX执行函数调用, 可以在ARM和Thumb指令集间切换

    这里解释下ARM和Thumb指令集的区别:

    Thumb是ARM体系结构中一种指令集。

    Thumb指令只有16bit,可以减小代码量。

    Thumb指令功能并不完整,必要时仍需要使用ARM指令集。

    扩展下NEON/VFP知识点:

    VFP是一种浮点硬件加速器。

    NEON是一个SIMD(单指令多数据)协处理器。

    以加法指令为例,单指令单数据(SISD)的CPU对加法指令译码后,执行部件先访问内存,取得第一个操作数;之后再一次访问内存,取得第二个操作数;随后才能进行求和运算。而在SIMD型的CPU中,指令译码后几个执行部件同时访问内存,一次性获得所有操作数进行运算。这个特点使SIMD特别适合于多媒体应用等数据密集型运算。

    加固技术:

    第一代加固技术——混淆技术;

    第二代加固技术——加壳技术(落地与不落地脱壳);

    第三代加固技术——指令抽离;

    第四代加固技术——指令转换,即VMP(虚拟软件保护)加固技术。

    二代加固:

    加壳是指给可执行文件加个外衣, 这个外衣就是壳程序. 壳程序先取得程序的控制权, 之后把加密的可执行程序在内存中解开为真正的程序并运行.

    可执行文件加固示意图

    三代加固:

    抽取dex文件中DexCode的部分结构,即虚拟机操作码。在虚拟机加载到此类的时候对DexCode结构进行还原。

    指令抽取-未抽取时

    比如此图中的getPwd方法很重要,需要抽取. 那么生成Dex文件后, 找到Dex文件中的getPwd的方法体, 将对应的方法体抽取出来放到so文件或者特定位置. 然后Hook住系统的FindClass方法, 当系统查找CoreUtils类时, 找到getPwd在内存中的位置, 然后将抽取出来的方法重新写入. 这样即使被破解拿到Dex, 这个Dex也是残缺的, 没有关键的函数.这时候如果我们查看Dex, 会发现getPwd的方法是个空方法.

    指令抽取-抽取完成 指令抽取-hook-findClass

    该方法的流程如下:

    指令抽取流程

    四代加固VMP技术:

    基于三代加固技术,把原本可执行文件中的机器指令代码转换成了它自己虚拟机的指令,而且还插入了大量的垃圾代码。

    这种方法将核心代码转化为虚拟机自己的指令, 破解apk的难度和破解虚拟机指令的难度一致. PC上存在类似的VMProtect, 号称无人一定能破.

    VMP加固原理

    从难度方面来说, 二代加固一般还有破解思路, 但到了四代加固这里, 一般的逆向脱壳技术全部失效, 你面对的是如何破解这个虚拟机.


    https://blog.csdn.net/jiangwei0910410003/article/details/78070610

    https://www.leiphone.com/news/201712/TABfBNU8x0lZIPoT.html

    https://bbs.pediy.com/thread-224921.htm

    2.2 实例

    apk加壳实例:

    apk加壳示例

    apk加壳实例可以用上图来说明, 我们把要加固的myapk.apk放到一个dex尾部. 这个dex有脱壳逻辑, 程序运行时, 首先运行这个脱壳dex, 脱壳dex从dex尾部获取到要加密的apk的大小, 然后从自己的dex中拷贝出这个myapk.apk, 最后调用Android系统API运行myapk.apk. 这样就算用ApkTool等逆向工具, 也无法直接获得我们加固的myapk.apk. 为了增大逆向难度, 我们可以把脱壳逻辑用c实现放到so文件中, 同时把加密的myapk.apk分段放到so文件中. 为了防止特征破解, 我们可以改写apk魔数. 这样下来, 一个简单的加固工具就完成了.

    这里提供一个demo, 只有最简单的把myapk.apk放到脱壳dex尾部的功能, git地址:

    https://github.com/oncealong/apk_dex_shell

    demo分为三个项目:

    • DexReinforcingTools

      • 给Apk加壳的工具, 可以用java或者cpp或者任何其他语言写成.
    • MyApk

      • 需要加固的Apk
    • ShellingMyApk

      • 脱壳Apk, 实际安装到用户手机上的是该Apk, 其在Application的attachBaseContext 时会解压得到实际的apk文件, 然后运行实际的Apk.

    这里再说下, 这种二代加壳是现在最简单的加壳方式, 也是最基本的加壳方式.

    参考文档:

    https://blog.csdn.net/jiangwei0910410003/article/details/48415225

    3.动态分析

    3.1 原理

    动态分析主要基于下面两个工具:

    JPDA(Java Platform Debugger Architecture)

    JPDA原理图

    JPDA分为三层, 分别是JVMTI,JDWP,JDI.

    JVMTI(Java Virtual Machine Tool Interface)是一套由虚拟机直接提供的 native接口,通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。

    JDWP(Java Debug Wire Protocol)是一个为 Java调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。

    JDI(Java Debug Interface)提供 Java API 来远程控制被调试虚拟机

    JPDA-JVM

    Android调试模型是一种JPDA框架的具体实现

    有两点主要区别:

    • JVM TI适配了Android设备特有的Dalvik虚拟机/ART虚拟机

    • JDWP的实现支持ADB和Socket两种通信方式

    JPDA-Android

    ptrace(process trace)

    ptrace原理

    ptrace()
    提供了跟踪和调试的功能。它允许一个进程(跟踪进程tracer)去控制另外一个进程(被跟踪进程tracee)。

    tracer可以观察和控制tracee的运行,可以查看和改变tracee的内存和寄存器。它主要用来实现断点调试和系统调用跟踪。

    tracer流程一般如下:

    tracer流程图

    其中PTRACE_ATTACH/PTRACE_GETREGS/PTRACE_POKETEXT/PTRACE_SETREGS/PTRACE_DETACH定义如下:

    PTRACE_ATTACH,表示附加到指定远程进程;

    PTRACE_DETACH,表示从指定远程进程分离

    PTRACE_GETREGS,表示读取远程进程当前寄存器环境

    PTRACE_SETREGS,表示设置远程进程的寄存器环境

    PTRACE_CONT,表示使远程进程继续运行

    PTRACE_PEEKTEXT,从远程进程指定内存地址读取一个word大小的数据

    PTRACE_POKETEXT,往远程进程指定内存地址写入一个word大小的数据

    ptrace是*nix系统上最常用的系统调用之一, 常见的gdb调试也是通过它实现的.

    gdb流程图

    检测ptrace

    当我们使用ptrace方式跟踪一个进程时,目标进程会记录自己被谁跟踪,可以查看/proc/pid/status来确认. 所以apk里为了防止被逆向, 一般都会新开一个线程, 对status做检测, 如果TracerPid不为0, 立刻退出apk.

    /proc/pid/status

    正常情况

    被ptrace时的status状态

    被ptrace时

    反动态分析:

    • 检查是否有调试

      • Debug.isDebuggerConnected();

      • 针对ptrace, 检查TracerPid是否为0

    • 检测是否在模拟器

      • getprop不同(虚拟机和真机的环境变量不同,
        比如虚拟机的ro.kernel.qemu=1而真机没有这个属性)

    反反动态分析:

    • 对抗反调试

      • java层:smali代码注释掉

      • native层 (nop掉so文件或内存中指令, 断点fopen/fget并修改内存)

    Android程序是否可调试:

    Android程序是否可调试

    开启调试:

    1.下载mprop, 注入init进程, 修改内存中属性值

    ./mprop ro.debuggable 1

    2.重启adbd

    stop;start

    tip:

    说到android:debuggable这个属性, 想到另一个属性android:allowBackup.
    android:allowBackup默认为true, 一定要显式设置android:allowBackup=false.
    否则adb backup/adb restore备份恢复数据
    微信6.0以前未设置此属性,可以备份恢复数据

    参考地址:

    https://tech.meituan.com/android-remote-debug.html

    http://burningcodes.net/%E7%90%86%E8%A7%A3ptrace%E8%B0%83%E8%AF%95%E5%8F%8A%E5%8F%8D%E8%B0%83%E8%AF%95/

    https://ops.tips/gists/using-c-to-inspect-linux-syscalls/

    https://www.nevermoe.com/?p=854

    https://github.com/wpvsyou/mprop

    3.2 工具

    动态分析工具

    这里特别推荐下VirtualXposed, 其基于VirtualApp和epic, 将Xposed安装到VirtualApp中, 可以不用root权限就使用Xposed, 而且安装插件后重启极快.

    Frida是一个DBI工具, 使用其进行动态分析时, 被分析进程的TracerPid仍为0. 下图是Frida原理, 其最初建立连接时通过ptrace向相关进程注入代码, 其后使用其特有的通道来通信, 如下图. Frida-Gadget支持Android下非root和iOS下非越狱的逆向.

    Frida原理

    IDA家喻户晓, 其支持dex和so的动态分析, 尤其是asm->c的转化, 可以大大方便分析.

    radare是一个比IDA还要强大的工具, 其起源是调查取证, 不过目前支持数不胜数的功能. 但是其学习曲线比Vim还要陡峭

    工具地址:

    https://forum.xda-developers.com/showthread.php?t=3034811

    https://github.com/android-hacker/VirtualXposed

    https://github.com/frida/frida

    https://www.hex-rays.com/products/ida/

    https://github.com/radare/radare2

    http://rada.re/r/cmp.html

    https://www.megabeets.net/a-journey-into-radare-2-part-1/

    3.3 实例

    无源码动态调试smali代码

    可以将apk用ApkTool反编译后, 使用AndroidStudio+smalidea插件来调试apk.

    这里来张图感受下无源码调试的强大.

    AndroidStudio+smalidea无源码调试

    分享一个小tip, 如何让程序暂停在启动界面.
    因为反逆向代码一般在Application的onCreate或更早就执行, 如果等到程序运行到MainActivity再attach进程, 时机就太晚了.

    可以用如下命令让app停在等待debug界面:

    等待debug一次: adb shell am set-debug-app -w com.oncealong.sample

    一次debug不一定能解决问题,多次调试则在所难免,如果每次调试都执行上述语句, 稍显啰嗦, 那么此时可以执行下述语句:

    一直等待debug: adb shell am set-debug-app -w --persistent com.oncealong.sample

    待debug完毕, 使用下述语句取消打开app时的等待.

    取消等待debug: adb shell am clear-debug-app

    这里的示例不在展开, 只说明这种方法和其效果, 对其感兴趣可以看下述链接.

    参考地址:

    http://www.cnblogs.com/goodhacker/p/5592313.html

    https://droidyue.com/blog/2017/05/14/a-little-but-useful-debug-skill_for_android/

    IDA动态调试

    IDA动态调试可以获得内存中的信息, 比如在dvmDexFileOpenPartial函数上加断点, 然后执行IDA脚本直接把内存中的dex拷贝出来以脱壳. 详情见Android应用安全防护和逆向分析相关章节. 这里也不做详细介绍,
    只用下图展示IDA的强大.

    IDA动态调试

    参考地址:

    https://blog.csdn.net/jltxgcy/article/details/50600241

    https://blog.csdn.net/qq1084283172/article/details/46872937

    VirtualXposed hook java

    VirtualXposed可以hook java, 相比Xposed安装插件需要重启手机, VirtualXposed只用重启下Xposed程序, 如果前者重启手机耗时1min, 后者重启Xposed程序只用1s不到. 对于一些简单的hook或者逆向, 或者验证Xposed插件逻辑, 这里强烈推荐VirtualXposed. 不过Xposed只支持hook java层, 如果需要hook native层, 可以使用下一个工具Frida.

    VirtualXposed hook 构造函数 VirtualXposed hook 方法

    参考地址:

    https://github.com/android-hacker/VirtualXposed

    https://github.com/ac-pm/Inspeckage

    http://www.cnblogs.com/lkislam/p/4859959.html

    Frida

    Frida支持java/native层的hook. 而且Frida支持脚本, 这样可以更方便的复现结果.

    比如Frida的这个Android示例. 将下面的代码放到一个py脚本中, 随时运行都可以获得结果. 不像IDA还需要恢复现场.

    Frida-hook

    参考地址:

    https://github.com/frida/frida/releases

    https://github.com/dweinstein/awesome-Frida

    https://www.anquanke.com/post/id/85758

    https://www.anquanke.com/post/id/85759

    https://koz.io/using-frida-on-android-without-root/

    https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool

    http://blog.mengy.org/how-valgrind-work/

    http://www.ninoishere.com/frida-learn-by-example/

    https://www.frida.re/docs/presentations/osdc-2015-the-engineering-behind-the-reverse-engineering.pdf

    http://dogewatch.github.io/2017/05/15/Hook-Native-Function-Use-Frida/

    4.重编译

    4.1 原理

    反重编译:

    运行时检查签名(signatures比较长,hash后比较)

    运行时校验保护(校验classes.dex的md5)

    反反重编译:

    查关键函数, 注释掉或nop掉

    如果到这一步, 光靠本地的检测基本无效, 可以考虑在http请求时加入对apk签名的检查, 如果不合法就不返回数据. 但是这样无法阻止app被非法本地运行, 逆向者也可以通过抓包正常apk的请求来模拟正常请求. 不过这样可以进一步提高破解门槛.

    5.Docker


    5.1 原理

    与逆向工具高内聚,与外界系统低耦合

    在Linux下, Docker性能不错, 还可以使用VNC连接桌面.

    # pull image
    
    docker pull cryptax/android-re:latest
    
    # run locally interactive
    
    docker run -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix
    cryptax/android-re:latest /bin/bash
    
    # run through ssh or VNC
    
    docker run -d -p SSH_PORT:22 -p VNC_PORT:5900 cryptax/android-re
    
    ## sample: docker run -d --privileged -p 5900:5900 -p 5022:22
    cryptax/android-re
    
    ssh -X -p SSH_PORT root@127.0.0.1
    
    ## sample: ssh -p 5022 -X root@127.0.0.1 #password: rootpass
    
    vncviewer HOST::VNC_PORT
    
    ##vncviewer 127.0.0.1::5900
    

    工具地址:

    https://github.com/cryptax/androidre/

    相关文章

      网友评论

        本文标题:Android逆向分析概述

        本文链接:https://www.haomeiwen.com/subject/atsqsftx.html