iOS逆向工程整理 HOOK微信抢红包

原理

Objective-C 是一门动态语言,我们可以利用OC的Runtime动态的替换App原有的函数,来达到我们(不可告人)的目的。OC 中对某个对象的方法的调用并不像 C++ 一样直接取得方法的实现的偏移值来调用,所以 C++ 方法与实现的关系在编译时就可确定。而 OC 中方法和实现的关系是在运行时决定的。在调用某个对象的方法时,实际上是调用了 obj_msgsend 向对象发送一个名称为方法名的消息,而我们可以替换这个响应这个消息的实现内容。OC 中比较有力的动态特性 Method Swizzing 就是建立在这个基础之上。
简单来说就是以下三句代码:

Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method newMethod = class_getInstanceMethod(theClass, newSelector);
    method_exchangeImplementations(originalMethod, newMethod);
 
 
  • 1
  • 2
  • 3

这样一来,原来调用originalSelector函数,都会指向我们的newSelector函数,以此实现HOOK。

工欲善其事,必先利其器

要进行iOS的HOOK开发,首先要准备一台Mac电脑(废话),一台越狱的iphone或者ipad。针对这类的需求,大神们开发了很多NB的工具,专门帮我们更好的去HOOK,我们都是站在大神的肩膀上。 
先介绍以下越狱开发常见的工具OpenSSH,Dumpdecrypted,class-dump、Theos、Reveal、IDA,Hopper

OpenSSH

这个工具是通过命令行工具访问苹果手机,执行命令行脚本。在Cydia中搜索openssh,安装。具体用法如下: 
1、打开mac下的terminal,输入命令ssh root@192.168.2.2(越狱设备ip地址) 
2、接下来会提示输入超级管理员账号密码,默认是alpine 
3、回车确认,即可root登录设备 
你也可以将你mac的公钥导入设备的/var/root/.ssh/authorized_keys文件,这样就可以免密登录root了。

Cycript

Cycript是大神saurik开发的一个非常强大的工具,可以让开发者在命令行下和应用交互,在运行时查看和修改应用。它可以帮助你HOOK一个App。Cycript最为贴心和实用的功能是它可以帮助我们轻松测试函数效果,整个过程安全无副作用,效果十分显著,实乃业界良心! 
安装方式:在Cydia中搜索Cycript安装 
使用方法: 
1、root登录越狱设备 
2、cycript-p 你想要测试的进程名 
3、随便玩,完全兼容OC语法比如cy# [#0x235b4fb1 hidden] 
Cycript有几条非常有用的命令: 
choose:如果知道一个类对象存在于当前的进程中,却不知道它的地址,不能通过“#”操作符来获取它,此时可以使用choose命令获取到该类的所有对象的内存地址 
打印一个对象的所有属性 [obj _ivarDescription].toString() 
打印一个对象的所有方法[obj _methodDescription].toString() 
动态添加属性 objc_setAssociatedObject(obj,@”isAdd”, [NSNumbernumberWithBool:YES], 0); 
获取动态添加的属性 objc_getAssociatedObject(self, @”isAdd”)

Reveal

Reveal是由ITTY BITTY出品的UI分析工具,可以直观地查看App的UI布局,我们可以用来研究别人的App界面是怎么做的,有哪些元素。更重要的是,可以直接找到你要HOOK的那个ViewController,贼方便不用瞎猫抓耗子一样到处去找是哪个ViewController了。 
安装方法: 
1、下载安装Mac版的Reveal 
2、iOS安装Reveal Loader,在Cydia中搜索并安装Reveal Loader 
在安装Reveal Loader的时候,它会自动从Reveal的官网下载一个必须的文件libReveal.dylib。如果网络状况不太好,不一定能够成功下载这个dylib文件,所以在下载完Reveal Loader后,检查iOS上的“/Library/RHRevealLoader/”目录下有没有一个名为“libReveal.dylib”的文件。如果没有就打开mac Reveal,在它标题栏的“Help”选项下,选中其中的“Show Reveal Library in Finder”,找到libReveal.dylib文件,使用scp拷贝到 iOS的/Library/RHRevealLoader/目录下。至此Reveal安装完毕!

Dumpdecrypted

Dumpdecrypted就是著名的砸壳工具,所谓砸壳,就是对 ipa 文件进行解密。因为在上传到 AppStore 之后,AppStore自动给所有的 ipa 进行了加密处理。而对于加密后的文件,直接使用 class-dump 是得不到什么东西的,或者是空文件,或者是一堆加密后的方法/类名。 
使用步骤如下: 
1、设备中打开需要砸壳的APP。 
2、SSH连接到手机,找到ipa包的位置并记录下来。 
3、Cycript到该ipa的进程,找到App的Documents文件夹位置并记录下来。 
4、拷贝dumpdecrypted.dylib到App的Documents 的目录。 
5、执行砸壳后,并拷贝出砸壳后的二进制文件。 
具体执行命令: 
1、ssh root@192.168.2.2 (iP地址为越狱设备的iP地址) 
2、 ps -e (查看进程,把进程对应的二进制文件地址记下来) 
3、cycript -p 进程名 
4、 [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory 
inDomains:NSUserDomainMask][0] (找到程序的documents目录) 
5、scp ~/dumpdecrypted.dylib root@192.168.2.2:/var/mobile/Containers/Data/Application/XXXXXX/Documents 
6、DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/ 
Application/XXXXXX/xxx.app/xxx 
然后就会生成.decrypted的文件,这个就是砸壳后的文件。接下来各种工具都随便上了class-dump、IDA、Hopper Disassembler

class-dump

class-dump就是用来dump二进制运行文件里面的class信息的工具。它利用Objective-C语言的runtime特性,将存储在Mach-O文件中的头文件信息提取出来,并生成对应的.h文件,这个工具非常有用,有了这个工具我们就像四维世界里面看三维物体,一切架构尽收眼底。 
class-dump用法: 
class-dump –arch armv7 -s -S -H 二进制文件路径 -o 头文件保存路径

IDA

IDA是大名鼎鼎的反编译工具,它乃逆向工程中最负盛名的神器之一。支持Windows、Linux和Mac OS X的多平台反汇编器/调试器,它的功能非常强大。class-dump可以帮我们罗列出要分析的头文件,IDA能够深入各个函数的具体实现,无论的C,C++,OC的函数都可以反编译出来。不过反编译出来的是汇编代码,你需要有一定的汇编基础才能读的懂。 
IDA很吃机器性能(我的机器经常卡住不动),还有另外一个反编译工具Hopper,对机器性能要求没那么高,也很好用,杀人越货的利器。

LLDB

LLDB是由苹果出品,内置于Xcode中的动态调试工具,可以调试C、C++、Objective-C,还全盘支持OSX、iOS,以及iOS模拟器。LLDB要配合debugserver来使用。常见的LLDB命令有: 
p命令:首先p是打印非对象的值。如果使用它打印对象的话,那么它会打印出对象的地址,如果打印非对象它一般会打印出基本变量类型的值。当然用它也可以申明一个变量譬如 p int a=10;(注lldb使用a = 10; (注lldb使用在变量前来声明为lldb内的命名空间的) 
po 命令:po 命令是我们最常用的命令因为在ios开发中,我们时刻面临着对象,所以我们在绝大部分时候都会使用po。首先po这个命令会打印出对象的description描述。 
bt [all] 打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈。 
br l 是breakpoint list的简写,列出所有的断点 
image list -o -f 列出模块和ASLR偏移,以及偏移后的地址,可以通过偏移后的地址-ASLR偏移来得出模块的基地址。 
b NSLog给函数设置断点 
br s -a IDA中偏移前的地址+ASLR偏移量 给内存地址设置断点 
p (char *)$r1打印函数名称 
br dis、br en和br del表示禁用、启用和删除断点 
nexti(ni)跳过一行 
stepi(si)跳入函数 
c继续执行直到断点 
register write r0 1修改寄存器的值

usbmuxd

很多人都是通过WiFi连接使用SSH服务的,因为无线网络的不稳定性及传输速度的限制,在复制文件或用LLDB远程调试时,iOS的响应很慢,效率不高。iOS越狱社区的知名人士Nikias Bassen开发了一款可以把本地OSX/Windows端口转发到远程iOS端口的工具usbmuxd,使我们能够通过USB连接线ssh到iOS中,大大增加了ssh连接的速度,也方便了那些没有WiFi的朋友。使用usbmuxd能极大提升ssh的速度,用LLDB远程连接debugserver的时间被缩短至15秒以内,强烈建议大家把usbmuxd作为ssh连接的首选方案

Theos

以上都是App的分析工具,而Theos是一个越狱开发工具包(具体写代码),由iOS越狱界知名人士Dustin Howett开发并分享到GitHub上。Theos与其他越狱开发工具相比,最大的特点就是简单:下载安装简单、Logos语法简单、编译发布简单,可以让使用者把精力都放在开发工作上去。就是让你省去了繁琐的原始代码编写,简化了编译和安装过程。同样有一款工具iOSOpenDev是整合在Xcode里的,可以直接在Xcode中配置开发、运行越狱程序,不过iOSOpenDev的安装,过程真的是让人要吐血(我有一台mac死活都装不上)。 
用法,theos有多种模板可以选择,最常用的就是tweak插件了:/opt/theos/bin/nic.pl

NIC 2.0 - New Instance Creator

[1.] iphone/application 
[2.] iphone/library 
[3.] iphone/preference_bundle 
[4.] iphone/tool 
[5.] iphone/tweak 
打包编译安装,需要按照固定格式编写Makefile文件,然后执行命令 
make package install,自动编译打包安装到iOS设备。 

如果你用的是IOSOpenDev就更简单了,配置好iOS设备ip地址,直接执行product->Bulid for->profiling,自动打包安装好。

原理上篇已经说过了,利用iOS的runtime特效,替换方法的实现来达到HOOK的目的。本篇是一个实战教程,讲解怎么来Hook微信的自动抢红包。

第1步:砸壳

首先ssh连上越狱iOS设备,执行ps -e查找微信的app地址
这里写图片描述
这里的app路径是/var/mobile/Containers/Bundle/Application/690828B8-B63F-4FAC-B7CC-DEFD8E23AF46/WeChat.app/WeChat,记下来先。
然后进入Cycript,查找出微信的Documents目录
这里写图片描述
把这个路径记下来/var/mobile/Containers/Data/Application/2B65DCC1-9FAD-41C8-A5EC-C3D9326EE3D6/Documents/
然后退出cycript,cd到documents目录下,执行以下命令:

adminde-iPhone:~ root# 
adminde-iPhone:/var/mobile/Containers/Data/Application/2B65DCC1-9FAD-41C8-A5EC-C3D9326EE3D6/Documents root# DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/690828B8-B63F-4FAC-B7CC-DEFD8E23AF46/WeChat.app/WeChat

mach-o decryption dumper

DISCLAIMER: This tool is only meant for security research purposes, not for application crackers.

[+] detected 32bit ARM binary in memory.
[+] offset to cryptid found: @0xd7a4c(from 0xd7000) = a4c
[+] Found encrypted data at address 00004000 of length 49463296 bytes - type 1.
[+] Opening /private/var/mobile/Containers/Bundle/Application/690828B8-B63F-4FAC-B7CC-DEFD8E23AF46/WeChat.app/WeChat for reading.
[+] Reading header
[+] Detecting header type
[+] Executable is a FAT image - searching for right architecture
[+] Correct arch is at offset 16384 in the file
[+] Opening WeChat.decrypted for writing.
[+] Copying the not encrypted start of the file
[+] Dumping the decrypted data into the file
[+] Copying the not encrypted remainder of the file
[+] Setting the LC_ENCRYPTION_INFO->cryptid to 0 at offset 4a4c
[+] Closing original file
[+] Closing dump file
adminde-iPhone:/var/mobile/Containers/Data/Application/2B65DCC1-9FAD-41C8-A5EC-C3D9326EE3D6/Documents root# ls 
WeChat.decrypted

WeChat.decrypted文件就是我们砸壳后的二进制文件,我们可以拷贝到Mac上,各种工具都可以用上了。

第2步:导出头文件

使用class-dump,将我们砸壳出来的二进制文件的头文件导出来:

admindeMac-mini:微信6.5.5 admin$ class-dump --arch armv7  -s -S -H WeChat.decrypted -o headers/
admindeMac-mini:微信6.5.5 admin$ ls headers/
AAAlertItem.h
AACloseNotifyReq.h
AACloseNotifyRes.h
AACloseReq.h
AACloseRes.h
AALaunchByMoneyReq.h
AALaunchByMoneyRes.h
AALaunchByPersonReq.h
AALaunchByPersonRes.h
AALaunchItem.h
AAListRecord.h
AAOperationReq.h
AAOperationRes.h
AAPayReq.h
AAPayRes.h

可以看到微信的头文件有8500多个(汗),如此茫茫代码海出如何找出我们需要的那一部分呢?

第2步:精准定位我们想要HOOK的代码

iOS开发是遵循MVC模式的,所以我们需要找到这个M模型层来找到我需要的业务逻辑代码。抢红包的原理是通过截取微信收到了红包消息,然后直接调用打开红包的接口来实现自动抢红包。
通过查找发现CMessageMgr这个类就是用来管理各种消息的,查看CMessageMgr.h头文件

- (void)AddAppMsg:(id)arg1 MsgWrap:(id)arg2 Data:(id)arg3 Scene:(unsigned long)arg4;
- (void)AddAppMsg:(id)arg1 MsgWrap:(id)arg2 DataPath:(id)arg3 Scene:(unsigned long)arg4;
- (BOOL)AddBackupMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddEmoticonMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddFloatBottle:(id)arg1 MsgWrap:(id)arg2;
- (void)AddHelloMsg:(id)arg1 MsgWrap:(id)arg2 HelloUser:(id)arg3 OpCode:(unsigned long)arg4 DES:(unsigned long)arg5 checkCreateTime:(BOOL)arg6 status:(unsigned long)arg7;
- (void)AddHelloMsgList:(id)arg1 MsgList:(id)arg2;
- (void)AddLocalMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddLocalMsg:(id)arg1 MsgWrap:(id)arg2 fixTime:(BOOL)arg3 NewMsgArriveNotify:(BOOL)arg4;
- (void)AddLocalMsg:(id)arg1 MsgWrap:(id)arg2 fixTime:(BOOL)arg3 NewMsgArriveNotify:(BOOL)arg4 Unique:(BOOL)arg5;
- (void)AddMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddMsgPattern:(id)arg1;
- (void)AddPimMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddRecordMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddShortVideoLocalMsg:(id)arg1 ToUsr:(id)arg2 VideoInfo:(id)arg3 MsgType:(unsigned long)arg4;
- (void)AddShortVideoMsg:(id)arg1 ToUsr:(id)arg2 VideoInfo:(id)arg3;
- (void)AddUniqueLocalMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AddVideoMsg:(id)arg1 ToUsr:(id)arg2 VideoInfo:(id)arg3;
- (void)AddVideoMsg:(id)arg1 ToUsr:(id)arg2 VideoInfo:(id)arg3 MsgType:(unsigned long)arg4;
- (void)AsyncOnAddMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AsyncOnAddMsgForSession:(id)arg1 MsgWrap:(id)arg2;
- (void)AsyncOnAddMsgForSession:(id)arg1 MsgWrap:(id)arg2 NewMsgArriveNotify:(BOOL)arg3;
- (void)AsyncOnAddMsgListForSession:(id)arg1 NotifyUsrName:(id)arg2;
- (void)AsyncOnCheckQQ;
- (void)AsyncOnDelMsg:(id)arg1;
- (void)AsyncOnDelMsg:(id)arg1 DelAll:(BOOL)arg2;
- (void)AsyncOnDelMsg:(id)arg1 MsgWrap:(id)arg2;
- (void)AsyncOnModMsg:(id)arg1 MsgWrap:(id)arg2;

通过猜想假设,再加上编写Tweak脚本测试,发现我们想要的接受消息的函数是AsyncOnAddMsg这个函数。找到接受消息的函数后接下来,我们要找打开红包的函数。
在头文件目录下搜索OpenRedEnvelope

grep -nR 'OpenRedEnvelope' ./
.//WCAtomicRedEnvReceiveHomeView.h:15:    UIButton *m_oOpenRedEnvelopesButton;
.//WCAtomicRedEnvReceiveHomeView.h:31:- (void)OnOpenRedEnvelopes;
.//WCAtomicRedEnvReceiveHomeViewDelegate-Protocol.h:12:- (void)WCAtomicRedEnvReceiveHomeViewOpenRedEnvelopes:(_Bool)arg1;
.//WCFestivalRedEnvFinishView.h:28:- (void)OnOpenRedEnvelopes;
.//WCFestivalRedEnvReceiveHomeView.h:31:- (void)OnOpenRedEnvelopes;
.//WCFestivalRedEnvReceiveHomeViewDelegate-Protocol.h:12:- (void)WCFestivalRedEnvReceiveHomeViewOpenRedEnvelopes:(_Bool)arg1;
.//WCFestivalRedEnvShareView.h:28:- (void)OnOpenRedEnvelopes;
.//WCRedEnvelopesControlData.h:31:    NSDictionary *m_structDicAfterOpenRedEnvelopesInfo;
.//WCRedEnvelopesControlData.h:49:@property(retain, nonatomic) NSDictionary *m_structDicAfterOpenRedEnvelopesInfo; // @synthesize m_structDicAfterOpenRedEnvelopesInfo;
.//WCRedEnvelopesEnterpriseControlLogic.h:39:- (void)WCFestivalRedEnvReceiveHomeViewOpenRedEnvelopes:(_Bool)arg1;
.//WCRedEnvelopesGreetingReceiveControlLogic.h:37:- (void)OnOpenRedEnvelopesRequest:(id)arg1 Error:(id)arg2;
.//WCRedEnvelopesGreetingReceiveControlLogic.h:52:- (void)WCRedEnvelopesReceiveHomeViewOpenRedEnvelopes;
.//WCRedEnvelopesLogicMgr.h:42:- (void)OpenRedEnvelopesRequest:(id)arg1;

我们猜想WCRedEnvelopesLogicMgr.h里面的OpenRedEnvelopesRequest函数就是我们要找的目标函数,经常反复的测试和验证,这个函数确实是我们要的打开红包的函数。

第3步:写代码完成开发

经历了以上分析和验证的过程,写代码对于HOOK来说反而是很简单的水到渠成。具体抢红包的代码如下:

@class CMessageMgr;

CHDeclareClass(CMessageMgr);

CHOptimizedMethod(2, self, void, CMessageMgr, AsyncOnAddMsg, id, arg1, MsgWrap, id, arg2) {
    CHSuper(2, CMessageMgr, AsyncOnAddMsg, arg1, MsgWrap, arg2);
    Ivar uiMessageTypeIvar = class_getInstanceVariable(objc_getClass("CMessageWrap"), "m_uiMessageType");
    ptrdiff_t offset = ivar_getOffset(uiMessageTypeIvar);
    unsigned char *stuffBytes = (unsigned char *)(__bridge void *)arg2;
    NSUInteger m_uiMessageType = * ((NSUInteger *)(stuffBytes + offset));

    Ivar nsFromUsrIvar = class_getInstanceVariable(objc_getClass("CMessageWrap"), "m_nsFromUsr");
    id m_nsFromUsr = object_getIvar(arg2, nsFromUsrIvar);

    Ivar nsContentIvar = class_getInstanceVariable(objc_getClass("CMessageWrap"), "m_nsContent");
    id m_nsContent = object_getIvar(arg2, nsContentIvar);

    switch(m_uiMessageType) {
        case 1: {
        }
            break;

        case 49: {
            // 49=红包
            //微信的服务中心
            Method methodMMServiceCenter = class_getClassMethod(objc_getClass("MMServiceCenter"), @selector(defaultCenter));
            IMP impMMSC = method_getImplementation(methodMMServiceCenter);
            id MMServiceCenter = impMMSC(objc_getClass("MMServiceCenter"), @selector(defaultCenter));
            //红包控制器
            id logicMgr = ((id (*)(id, SEL, Class))objc_msgSend)(MMServiceCenter, @selector(getService:), objc_getClass("WCRedEnvelopesLogicMgr"));
            //通讯录管理器
            id contactManager = ((id (*)(id, SEL, Class))objc_msgSend)(MMServiceCenter, @selector(getService:),objc_getClass("CContactMgr"));

            Method methodGetSelfContact = class_getInstanceMethod(objc_getClass("CContactMgr"), @selector(getSelfContact));
            IMP impGS = method_getImplementation(methodGetSelfContact);
            id selfContact = impGS(contactManager, @selector(getSelfContact));

            if ([m_nsContent rangeOfString:@"wxpay://"].location != NSNotFound)
            {
                NSString *nativeUrl = m_nsContent;
                NSRange rangeStart = [m_nsContent rangeOfString:@"wxpay://c2cbizmessagehandler/hongbao"];
                if (rangeStart.location != NSNotFound)
                {
                    NSUInteger locationStart = rangeStart.location;
                    nativeUrl = [nativeUrl substringFromIndex:locationStart];
                }

                NSRange rangeEnd = [nativeUrl rangeOfString:@"]]"];
                if (rangeEnd.location != NSNotFound)
                {
                    NSUInteger locationEnd = rangeEnd.location;
                    nativeUrl = [nativeUrl substringToIndex:locationEnd];
                }

                NSString *naUrl = [nativeUrl substringFromIndex:[@"wxpay://c2cbizmessagehandler/hongbao/receivehongbao?" length]];

                NSArray *parameterPairs =[naUrl componentsSeparatedByString:@"&"];

                NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithCapacity:[parameterPairs count]];
                for (NSString *currentPair in parameterPairs) {
                    NSRange range = [currentPair rangeOfString:@"="];
                    if(range.location == NSNotFound)
                        continue;
                    NSString *key = [currentPair substringToIndex:range.location];
                    NSString *value =[currentPair substringFromIndex:range.location + 1];
                    [parameters setObject:value forKey:key];
                }

                //红包参数
                NSMutableDictionary *params = [@{} mutableCopy];

                [params setObject:parameters[@"msgtypes"]?:@"null" forKey:@"msgType"];
                [params setObject:parameters[@"sendid"]?:@"null" forKey:@"sendId"];
                [params setObject:parameters[@"channelid"]?:@"null" forKey:@"channelId"];

                id getContactDisplayName = objc_msgSend(selfContact, @selector(getContactDisplayName));
                id m_nsHeadImgUrl = objc_msgSend(selfContact, @selector(m_nsHeadImgUrl));

                [params setObject:getContactDisplayName forKey:@"nickName"];
                [params setObject:m_nsHeadImgUrl forKey:@"headImg"];
                [params setObject:[NSString stringWithFormat:@"%@", nativeUrl]?:@"null" forKey:@"nativeUrl"];
                [params setObject:m_nsFromUsr?:@"null" forKey:@"sessionUserName"];

                ((void (*)(id, SEL, NSMutableDictionary*))objc_msgSend)(logicMgr, @selector(OpenRedEnvelopesRequest:), params);

                return;
            }

            break;
        }
        default:
            break;
    }
}

以上就是微信抢红包的分析和代码过程,才有的是越狱方式开发,打包的dylib动态链接库文件是直接放入到系统文件目录里面的。至于非越狱开发,要更复杂一点,涉及到原始app文件的解包,修改,添加动态链接库,然后再签名打包的一系列复杂过程。仅供参考和学习用,请勿用于商业用途,否则后果自负!

转自: http://blog.csdn.net/dlmlzz09/article/details/62232254

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值