Cycript

作者: HotPotCat | 来源:发表于2021-05-21 09:32 被阅读0次

    Cycript是由Cydia创始人Saurik推出的一款脚本语言,Cycript混合了OCJavaScript语法的解释器,这意味着我们能够在一个命令中使用OC或者JavaScript,甚至两者并用。它能够挂钩正在运行的进程,能够在运行时修改程序。

    一、Cycript安装

    Cycript官网,目前最新版本0.9.594。直接下载SDK解压放在/opt目录中:

    image.png

    验证./cycript

    ➜  ./cycript
    dyld: Library not loaded: /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/libruby.2.0.0.dylib
      Referenced from: /opt/cycript_0.9.594/./Cycript.lib/cycript-apl
      Reason: image not found
    [1]    45603 abort      ./cycript
    

    出现image not found发现需要Ruby 2.0,进入目录只有2.6

    image.png
    2.6拷贝一份改名为2.0就可以了,在某些版本的MAC OSX在该目录不能修改则将Monkey/bin中的cycript拷贝到cycript中替换cycript文件。 image.png

    再次执行就可以了:

    ➜  ./cycript
    cy#
    

    进入cy#表示配置成功。

    接着配置环境变量:

    export CY=/opt/cycript_0.9.594/
    export PATH=/opt/MonkeyDev/bin:$PATH:$CY
    

    配置环境变量后在任意路径输入cycript就可以进入cycript了。如果已经安装了Monkey则不需要配置和安装cycript了。(monkey自带)。

    二、Cycript常用命令

    只要将cycript注入到应用中,我们就可以调用其中的命令了,Monkey重签名注入的时候帮我们注入了:

    image.png
    相当于开启端口,可以监听。

    进入Cycript环境

    直接在终端输入cycript就进入cycript环境了:

    ➜  ~ cycript
    cy#
    

    附加进程

    连接进程进入Cycript环境

    ➜  ~ cycript -r 192.168.3.127:6666
    cy#
    

    这里手机和电脑要在同一wifi,并且进入应用程序进程。ip为手机的ip

    退出cycript环境

    control + d
    

    cycript调试命令

    UIWindow.keyWindow()获取keyWindow

    cy# UIWindow.keyWindow()
    #"<iConsoleWindow: 0x11344a220; baseClass = UIWindow; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x2812ea8b0>; layer = <UIWindowLayer: 0x281c928c0>>"
    

    这个时候进程并没有被中断。为了方便我们可以定义变量keyWd:

    cy# var keyWd = UIWindow.keyWindow()
    #"<iConsoleWindow: 0x11344a220; baseClass = UIWindow; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x2812ea8b0>; layer = <UIWindowLayer: 0x281c928c0>>"
    

    后续直接调用keyWd就能拿到keyWindow。即使control + d/ 退出终端下次进入这个变量仍然有效。应用程序重启就失效了。

    UIApp获取单类对象

    cy# UIApp
    #"<UIApplication: 0x111d15e60>"
    

    获取rootVC

    cy# [keyWd rootViewController]
    #"<MMUINavigationController: 0x1140d4800> ChildViewControllers:(\n    \"<WCAccountLoginFirstViewController: 0x1138c4600>\"\n)"
    

    #对象地址
    拿到该对象,可用于调用方法。

    cy# #0x1140d4800
    #"<MMUINavigationController: 0x1140d4800> ChildViewControllers:(\n    \"<WCAccountLoginFirstViewController: 0x1138c4600>\"\n)"
    

    *对象地址
    可以取出对象的成员变量。

    image.png

    recursiveDescription() 循环打印子视图

    cy# keyWd.recursiveDescription()
    
    image.png

    toString() 格式化打印(遇到\n换行)

    cy# keyWd.recursiveDescription().toString()
    
    `<iConsoleWindow: 0x1134535e0; baseClass = UIWindow; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x281210fc0>; layer = <UIWindowLayer: 0x281cf9600>>
       | <UITransitionView: 0x113459e70; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281cfa820>>
       |    | <UIDropShadowView: 0x11345aa60; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281cf9980>>
       |    |    | <UILayoutContainerView: 0x1134565c0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x281211f20>; layer = <CALayer: 0x281cf9f80>>
       |    |    |    | <UINavigationTransitionView: 0x113457e30; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281cf9b80>>
       |    |    |    |    | <UIViewControllerWrapperView: 0x11347b300; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281c38c40>>
       |    |    |    |    |    | <UIView: 0x11342afc0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281cf1d20>>
       |    |    |    |    |    |    | <UIView: 0x11342d440; frame = (0 20; 375 732); autoresize = W; layer = <CALayer: 0x281cf2ba0>>
       |    |    |    |    |    |    |    | <UIImageView: 0x1135125b0; frame = (0 -20; 375 667); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x281c096a0>>
       |    |    |    |    |    |    | <UIView: 0x113558ea0; frame = (0 582; 375 65); autoresize = W+TM; layer = <CALayer: 0x281cffaa0>>
       |    |    |    |    |    |    |    | <FixTitleColorButton: 0x1134553a0; baseClass = UIButton; frame = (20 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = RM; layer = <CALayer: 0x281cffd20>>
       |    |    |    |    |    |    |    |    | <UIButtonLabel: 0x113433470; frame = (60.5 12.5; 37 22); text = '\u767b\u5f55'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283fa25d0>>
       |    |    |    |    |    |    |    | <FixTitleColorButton: 0x113535180; baseClass = UIButton; frame = (197.5 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = LM; layer = <CALayer: 0x281c20d40>>
       |    |    |    |    |    |    |    |    | <UIButtonLabel: 0x1135374c0; frame = (60.5 12.5; 37 22); text = '\u6ce8\u518c'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283fa5cc0>>
       |    |    |    |    |    |    | <UIButton: 0x1135385c0; frame = (287 20; 88 49); opaque = NO; autoresize = LM; layer = <CALayer: 0x281c20f80>>
       |    |    |    |    |    |    |    | <UIButtonLabel: 0x11345c7c0; frame = (15 16; 58 17); text = '\u7b80\u4f53\u4e2d\u6587'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283fa2990>>
       |    |    |    | <MMUINavigationBar: 0x113456740; baseClass = UINavigationBar; frame = (0 20; 375 44); opaque = NO; autoresize = W; layer = <CALayer: 0x281cf9dc0>>
       |    |    |    |    | <_UIBarBackground: 0x113456b90; frame = (0 -20; 375 64); userInteractionEnabled = NO; layer = <CALayer: 0x281cf9c20>>
       |    |    |    |    |    | <UIImageView: 0x11347ce10; frame = (0 0; 375 64); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x281c39380>>
       |    |    |    |    |    | <_UIBarBackgroundShadowView: 0x113458380; frame = (0 64; 375 0); layer = <CALayer: 0x281cfa060>> clientRequestedContentView effect=none
       |    |    |    |    |    |    | <_UIBarBackgroundShadowContentImageView: 0x1134586f0; frame = (0 0; 375 0); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x281cfa120>>
       |    |    |    |    | <_UINavigationBarContentView: 0x113456d70; frame = (0 0; 375 44); layer = <CALayer: 0x281cf9b60>> layout=0x113456ff0
       |    |    |    |    |    | <_UITAMICAdaptorView: 0x11346ce60; frame = (187 4; 1 36); autoresizesSubviews = NO; layer = <CALayer: 0x281cf1d40>>
       |    |    |    |    |    |    | <MMTitleView: 0x113562990; frame = (0 0; 1 36); layer = <CALayer: 0x281ce17a0>>
       |    |    |    |    |    |    |    | <MMUILabel: 0x1135620f0; baseClass = UILabel; frame = (0 0; 0 36); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283faad00>>
       |    |    |    |    | <UIView: 0x11352e9c0; frame = (0 44; 375 0.5); hidden = YES; autoresize = W+TM; layer = <CALayer: 0x281c3e840>>
       |    |    |    |    | <UIView: 0x113452c50; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CALayer: 0x281cf9ae0>>`
    

    choose(类名)
    查询当前进程中该类型的对象。

    cy# choose(UIButton)
    
    "<FixTitleColorButton: 0x113535180; baseClass = UIButton; frame = (197.5 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = LM; layer = <CALayer: 0x281c20d40>>,<UIButton: 0x1135385c0; frame = (287 20; 88 49); opaque = NO; autoresize = LM; layer = <CALayer: 0x281c20f80>>,<FixTitleColorButton: 0x1134553a0; baseClass = UIButton; frame = (20 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = RM; layer = <CALayer: 0x281cffd20>>"
    

    LLDBsearch很像。

    三、脚本自动连接

    创建一个脚本cyConnect.sh,填入一下内容:

    cycript -r 192.168.3.127:6666
    

    配置环境变量:

    export HPShell=/Users/zaizai/HPShell/
    export PATH=/opt/MonkeyDev/bin:$HPShell:$PATH
    

    使用:

    ➜  ~ cyConnect.sh
    cy#
    

    这样就能自动连接了。

    四、Cycript 修改内存

    修改角标

    cy# [UIApp setApplicationBadgeString: @"999"]
    

    修改红包金额
    正常内容如下:

    修改前内容

    首先通过choose(UILabel)找到红包金额

    choose(UILabel).toString()
    

    搜索1.00得到地址0x149eaef00

    image.png
    修改金额:
    cy# #0x149eaef00.text = @"¥88888888.00"
    @"\xef\xbf\xa588888888.00"
    

    接着同样的方式找到红包中借款地址0x14c33b460

    cy# #0x14c33b460.text = @"还款"
    @"\xe8\xbf\x98\xe6\xac\xbe"
    

    修改完成后内容如下:


    修改后内容

    修改聊天内容
    我们可以直接通过cycript搜索控件进行修改,也可以通过Xcode view debug找到控件进行修改。

    image.png
    可以看到内容在accessibilitydescription中:
    image.png
    这个时候通过修改text已经无效了,Hook下代码调试下:
    %hook RichTextView
    
    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(id)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //    arg2 为文案
        return %orig;
    }
    
    %end
    

    经过排查发现arg2为文案,所以直接在这里写死试试:

    %hook RichTextView
    
    - (_Bool)setPrefixContent:(id)arg1 TargetContent:(NSString *)arg2 TargetParserString:(id)arg3 SuffixContent:(id)arg4 {
    //    arg2 为文案
        if([arg2 isEqualToString:@"钱已经借给你了。"]) {
            arg2 = @"钱已经换给你了,请查收!";
        }
        return %orig;
    }
    
    %end
    

    确认可以修改。
    至此整个内容就修改完成了:


    完整修改后

    所以微信截图什么的一个字也不能信。随便修改,想要什么样的内容都可以。

    五、Cycript高级用法

    APPID获取bundle identifier

    cy# APPID
    @"com.guaizai.MonkeyDemo"
    

    pviews()
    获取视图层级

    `<iConsoleWindow: 0x113790b60; baseClass = UIWindow; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x2811ce0d0>; layer = <UIWindowLayer: 0x281f12020>>
       | <UITransitionView: 0x113797170; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281f13120>>
       |    | <UIDropShadowView: 0x113798100; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281f13240>>
       |    |    | <UILayoutContainerView: 0x113793ba0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x2811cf060>; layer = <CALayer: 0x281f124c0>>
       |    |    |    | <UINavigationTransitionView: 0x1137950f0; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x281f12760>>
       |    |    |    |    | <UIViewControllerWrapperView: 0x11378b1d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281fd9400>>
       |    |    |    |    |    | <UIView: 0x113794990; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x281fe8180>>
       |    |    |    |    |    |    | <UIView: 0x113793980; frame = (0 20; 375 732); autoresize = W; layer = <CALayer: 0x281fe8ac0>>
       |    |    |    |    |    |    |    | <UIImageView: 0x113316830; frame = (0 -20; 375 667); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x281fec780>>
       |    |    |    |    |    |    | <UIView: 0x113316140; frame = (0 582; 375 65); autoresize = W+TM; layer = <CALayer: 0x281fecc60>>
       |    |    |    |    |    |    |    | <FixTitleColorButton: 0x113306f70; baseClass = UIButton; frame = (20 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = RM; layer = <CALayer: 0x281fecea0>>
       |    |    |    |    |    |    |    |    | <UIButtonLabel: 0x113328f90; frame = (60.5 12.5; 37 22); text = '\u767b\u5f55'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283c4acb0>>
       |    |    |    |    |    |    |    | <FixTitleColorButton: 0x1137562e0; baseClass = UIButton; frame = (197.5 18; 157.5 47); clipsToBounds = YES; opaque = NO; autoresize = LM; layer = <CALayer: 0x281fc70a0>>
       |    |    |    |    |    |    |    |    | <UIButtonLabel: 0x1137590e0; frame = (60.5 12.5; 37 22); text = '\u6ce8\u518c'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283c46710>>
       |    |    |    |    |    |    | <UIButton: 0x11375a080; frame = (287 20; 88 49); opaque = NO; autoresize = LM; layer = <CALayer: 0x281fc72e0>>
       |    |    |    |    |    |    |    | <UIButtonLabel: 0x113762910; frame = (15 16; 58 17); text = '\u7b80\u4f53\u4e2d\u6587'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283c46850>>
       |    |    |    | <MMUINavigationBar: 0x113793d20; baseClass = UINavigationBar; frame = (0 20; 375 44); opaque = NO; autoresize = W; layer = <CALayer: 0x281f124e0>>
       |    |    |    |    | <_UIBarBackground: 0x113794170; frame = (0 -20; 375 64); userInteractionEnabled = NO; layer = <CALayer: 0x281f12560>>
       |    |    |    |    |    | <UIImageView: 0x11378df60; frame = (0 0; 375 64); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x281fd9a80>>
       |    |    |    |    |    | <_UIBarBackgroundShadowView: 0x1137957a0; frame = (0 64; 375 0); layer = <CALayer: 0x281f129a0>> clientRequestedContentView effect=none
       |    |    |    |    |    |    | <_UIBarBackgroundShadowContentImageView: 0x113795b10; frame = (0 0; 375 0); opaque = NO; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x281f12a60>>
       |    |    |    |    | <_UINavigationBarContentView: 0x113794350; frame = (0 0; 375 44); layer = <CALayer: 0x281f12580>> layout=0x1137945d0
       |    |    |    |    |    | <_UITAMICAdaptorView: 0x11378e920; frame = (187 4; 1 36); autoresizesSubviews = NO; layer = <CALayer: 0x281fe81a0>>
       |    |    |    |    |    |    | <MMTitleView: 0x113338b90; frame = (0 0; 1 36); layer = <CALayer: 0x281f36fa0>>
       |    |    |    |    |    |    |    | <MMUILabel: 0x1133382f0; baseClass = UILabel; frame = (0 0; 0 36); userInteractionEnabled = NO; layer = <_UILabelLayer: 0x283c48a00>>
       |    |    |    |    | <UIView: 0x11373a720; frame = (0 44; 375 0.5); hidden = YES; autoresize = W+TM; layer = <CALayer: 0x281f5a3a0>>
       |    |    |    |    | <UIView: 0x1137903d0; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CALayer: 0x281f127a0>>`
    

    pvcs()
    获取视图控制器

    cy# pvcs()
    "<MMUINavigationController 0x11396f600>, state: appeared, view: <UILayoutContainerView 0x113793ba0>\n   | <WCAccountLoginFirstViewController 0x11400fa00>, state: appeared, view: <UIView 0x113794990>"
    

    这些命令并不是cycript自带的,那么这些命令哪里来的呢?
    Monkey工程我们可以在Config->MDConfig.plist中发现Cycript下有msmd两个.cy文件。

    image.png
    这些命令就封装在这两个文件中(工程运行的时候自动从网络上下载)。
    ms.cy
    md.cy

    六、封装cy文件

    Cycript是一门脚本语言,它可以加载封装好的.cy文件。我们会将常见的Cycript常用功能封装到.cy文件中,便于调试。
    创建一个HotpotCat.cy文件:

    //IIFE 匿名函数自执行表达式
    
    (function(exports){
     
     //不变的内容使用常量
     APPID = [NSBundle mainBundle].bundleIdentifier,
     APPPATH = [NSBundle mainBundle].bundlePath,
     APPHOME = NSHomeDirectory(),
     
     //变化的内容,就用function去定义!!
     HPRootVC = function(){
     return UIApp.keyWindow.rootViewController;
     };
     
     
     HPKeyWindow = function(){
     return UIApp.keyWindow;
     };
     
     
     
     HPGetCurrentVCFromRootVc = function(rootVC){
        var currentVC;
        if([rootVC presentedViewController]){
            rootVC = [rootVC presentedViewController];
        }
     
        if([rootVC isKindOfClass:[UITabBarController class]]){
                currentVC = HPGetCurrentVCFromRootVc(rootVC.selectedViewController);
            }else if([rootVC isKindOfClass:[UINavigationController class]]){
                currentVC = HPGetCurrentVCFromRootVc(rootVC.visibleViewController);
            }else{
                currentVC = rootVC;
        }
         return currentVC;
     };
     
     
     HPCurrentVC = function(){
         return HPGetCurrentVCFromRootVc(HPRootVC());
     };
     
     })(exports);
    
    • 对于不变的内容使用常量。
    • 变化的内容,用function定义。

    七 导入.cy文件

    7.1 非越狱环境导入cy文件

    1. 通过Framworks导入
      利用MonkeyDev工具导入.cy文件,MonkeyDev本身集成了Cycript,只需要将.cy文件通过xcode导入Framworks目录即可。
      image.png
    • HotpotCat.cy拖入工程。
    • Copy Files中添加HotpotCat.cy

    这个时候编译完工程后在Frameworks中就能找到HotpotCat.cy文件了:

    image.png
    1. 通过配置文件导入
      这里可以参考NSLog的方式:
      image.png
    • LoadAtLaunch:启动的时候是否加载脚本。
    • priority:优先级。数字越小优先级越高,适用于有依赖关系的脚本。
    • content/url:脚本可以直接写到content里面,也可以是网络的url会自动下载下来。

    7.1.1 使用自定义.cy内容

    ➜  ~ cyConnect.sh
    cy# @import HotpotCat
    {}
    cy# HPCurrentVC()
    #"<WCAccountLoginFirstViewController: 0x11612d200>"
    cy# APPHOME
    @"/var/mobile/Containers/Data/Application/A3FC3D2B-03D5-436F-BD35-5560BDE37ADD"
    cy# APPPATH
    @"/private/var/containers/Bundle/Application/A2E20EBA-7AF7-4825-B693-13A19F626174/MonkeyDemo.app"
    
    • 首先进入cycript环境。
    • 导入.cy文件,这里不需要后缀。如果是通过config配置并且默认启动加载则不需要。
    • 调用定义的常量或者方法。

    HotpotCat.cy是通过libcycript.dylib加载的。

    7.2 越狱手机中使用Cycript

    1. 越狱手机安装Cycript插件

      Cycript插件
      安装后手机环境下就有了Cycript
    2. 安装手机终端命令插件adv-cmds

      Screen Shot 2021-05-27 at 4.33.39 PM.png
      adv-cmds是手机终端命令插件,比如clear等命令。能够帮助我们更好的使用手机终端。
    3. 手机终端使用cycript

    zaizai:~ root# ps -A | grep WeChat
    21890 ??         0:01.80 /var/containers/Bundle/Application/8F382114-BBA7-4D81-AA3E-3CD02E03E23E/WeChat.app/WeChat
    21896 ttys000    0:00.01 grep WeChat
    zaizai:~ root# cycript -p 21890
    cy#
    
    • 找到进程id。
    • 通过cycript -p 进程id/进程名字进入cycript环境。如果有同名进程那么只能通过id。
    cy# UIApp
    #"<UIApplication: 0x110f10df0>"
    
    zaizai:~ root# cycript -p AlipayWallet
    cy# UIApp
    #"<DFApplication: 0x10b30cbf0>"
    

    这个时候就附加了微信了,可以直接在手机终端动态调试正版微信了。

    7.2.1 手机越狱环境使用Cy文件

    那么怎么导入cy文件呢?非越狱环境是导入到App里面的,越狱环境可以直接导入到手机中。
    在手机端有一个目录cd /usr/lib/cycript0.9/,我们的cy文件需要放入这个路径下才行。
    在这个目录下有文件目录如下:

    image.png
    可以看到MS.cy是在saurik目录下的。这么做是为了规范 避免冲突,有点像BundleId。那么我们自己的脚本也应该建立自己的目录com/HotpotCat
    zaizai:/usr/lib/cycript0.9 root# cd com
    zaizai:/usr/lib/cycript0.9/com root# ls
    saurik/
    zaizai:/usr/lib/cycript0.9/com root# mkdir HotpotCat
    zaizai:/usr/lib/cycript0.9/com root# cd HotpotCat
    zaizai:/usr/lib/cycript0.9/com/HotpotCat root# pwd
    /usr/lib/cycript0.9/com/HotpotCat
    zaizai:/usr/lib/cycript0.9/com/HotpotCat root#
    

    接着将自己的脚本拷贝到这个目录/usr/lib/cycript0.9/com/HotpotCat/:

    scp -P 12345 HotpotCat.cy root@localhost:/usr/lib/cycript0.9/com/HotpotCat/
    

    使用脚本
    导入:

    @import com.saurik.substrate.MS
    @import com.HotpotCat.HotpotCat
    
    image.png

    总结

    • cycript是一种脚本语言,混合了多种语法。(混合了多种语法的解释器),所以可以兼容。
    • 越狱手机安装cycript插件(依赖adv-cmds插件)。
    • cycript可以附加到进程,动态调试应用。
      • cycript -p 进程iD/名称
    • cy文件
      • /usr/lib/cycript0.9目录中
      • 为了不重名放入com目录中,创建自己的组织文件夹。
      • 加载的时候@import com.组织名称.文件名称

    相关文章

      网友评论

        本文标题:Cycript

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