一文弄懂什么是RPC

Posted wh柒八九

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文弄懂什么是RPC相关的知识,希望对你有一定的参考价值。

本文来说下rpc相关的知识与概念

文章目录


概述

远程过程调用似乎是一种有用的范式,用于在以高级语言编写的程序之间提供跨网络的通信。本文描述一个提供了远程调用工具的软件包,面对这样一个软件包时一个设计者拥有的选项,以及我们做出的选择。我们描述了我们的RPC机制的整体结构,用于绑定RPC客户端的工具,传输通信层协议,以及一些性能测量。包括用于实现高性能和最小化集群间负载的一些优化的描述。

远程过程调用(以下称RPC)的概念是非常简单的。它是基于这一观察:过程调用是一个众所周知且易于理解的机制,用于在单个计算机上运行的程序内的控制和数据的传输。因此提出扩展这种机制以提供跨网络的控制和数据的传输。但一个远程程序被调用时,该调用环境会被挂起,参数通过网络传输到该程序被调用的环境中(我们将之称为被调用者),并在那里执行相应的程序。当程序执行结束并产生结果时,结果再被传回调用环境中。此时调用环境中的执行恢复,仿佛是在单机上执行调用一样。当调用环境被挂起时,该机器上的其他进程可能仍然在执行——这取决于该环境的并行性和RPC的实现。


什么是rpc

RPC (Remote Procedure Call)即远程过程调用,是分布式系统常见的一种通信方法。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。

除 RPC 之外,常见的多系统数据交互方案还有分布式消息队列、HTTP 请求调用、数据库和分布式缓存等。其中 RPC 和 HTTP 调用是没有经过中间件的,它们是端到端系统的直接数据交互。

通俗点说

  • RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。
  • RPC会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯)。
  • 客户端发起请求,服务器返回响应(类似于Http的工作方式)RPC在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。


RPC架构设计

先说说 RPC 服务的基本架构吧。我们可以很清楚地看到,一个完整的 RPC 架构里面包含了四个核心的组件。

  • Client
  • Server
  • Client Stub
  • Server Stub(这个Stub大家可以理解为存根)

分别说说这几个组件:

  • 客户端(Client),服务的调用方。
  • 服务端(Server),真正的服务提供者。
  • 客户端存根,存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。

RPC调用流程

RPC调用流程时序图

具体实现步骤:

  1. 服务调用方(client)(客户端)以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;在Java里就是序列化的过程;
  3. client stub找到服务地址,并将消息通过网络发送到服务端;
  4. server stub收到消息后进行解码,在Java里就是反序列化的过程;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行处理逻辑;
  7. 本地服务将结果返回给server stub;
  8. server stub将返回结果打包成消息,Java里的序列化;
  9. server stub将打包后的消息通过网络并发送至消费方;
  10. client stub接收到消息,并进行解码, Java里的反序列化;
  11. 服务调用方(client)得到最终结果。

RPC框架的目标就是把2-10步封装起来,把调用、编码/解码的过程封装起来,让用户像调用本地服务一样的调用远程服务。要做到对客户端(调用方)透明化服务, RPC框架需要考虑解决如下问题:

通讯问题 : 主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。

寻址问题: A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。

序列化与反序列化 : 当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。同理,B服务器接收参数要将参数反序列化。B服务器应用调用自己的方法处理后返回的结果也要序列化给A服务器,A服务器接收也要经过反序列化的过程。


流行的RPC框架

目前流行的开源 RPC 框架还是比较多的。下面重点介绍三种


gRPC

gRPC 是 Google 最近公布的开源软件,基于最新的 HTTP2.0 协议,并支持常见的众多编程语言。

我们知道 HTTP2.0 是基于二进制的 HTTP 协议升级版本,目前各大浏览器都在快马加鞭的加以支持。

这个 RPC 框架是基于 HTTP 协议实现的,底层使用到了 Netty 框架的支持。


Thrift

Thrift 是 Facebook 的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的 IDL 定义文件自动生成服务代码框架。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, php, Ruby, Erlang, Perl, Haskell, C#, Cocoa, javascript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。

用户只要在其之前进行二次开发就行,对于底层的 RPC 通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。


Dubbo

Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。

同样的远程接口是基于 Java Interface,并且依托于 Spring 框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。


rpc调用与http调用

很长时间以来都没有怎么好好搞清楚RPC(即 Remote Procedure Call,远程过程调用)和 HTTP调用的区别,不都是写一个服务然后在客户端调用么?

先说一下他们最本质的区别,就是 RPC 主要是基于 TCP/IP 协议的,而 HTTP 服务主要是基于 HTTP 协议的

我们都知道 HTTP 协议是在传输层协议 TCP 之上的,所以效率来看的话,RPC 当然是要更胜一筹啦!下面来具体说一说 RPC 服务和 HTTP 服务。


本文小结

本文介绍了rpc相关的概念与知识,对后续dubbo的学习有极大的帮助,可以对dubbo有更加深入的理解与掌握。

一文让你完全弄懂Stegosaurus

国内关于 Stegosaurus 的介绍少之又少,一般只是单纯的工具使用的讲解之类的,并且本人在学习过程中也是遇到了很多的问题,基于此种情况下写下此文,也是为我逝去的青春时光留个念想吧~

Stegosaurus是什么?

在了解 Stegosaurus 是什么之前,我们首先需要弄清楚的一个问题是:什么是隐写术?

隐写术,从字面上来理解,隐是隐藏,所以我们从字面上可以知道,隐写术是一类可以隐藏自己写的一些东西的方法,可能我们所写的这些东西是一些比较重要的信息,不想让别人看到,我们会考虑采取一些办法去隐藏它,比如对所写的文件加解密,用一些特殊的纸张(比如纸张遇到水后,上面的字才会显示出来)之类的。隐写术这种手段在日常生活中用的十分广泛,我相信部分小伙伴们小时候曾经有过写日记的习惯,写完的日记可能不想让爸爸妈妈知道(青春期萌动的内心,咱们都是过来人,都懂这个2333),所以以前常常会买那种上了把锁的那种日记本,这样就不怕自己的小秘密被爸爸妈妈知道啦。

事实上,隐写术是一门关于信息隐藏的技巧与科学,专业一点的讲,就是指的是采取一些不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容的方法。隐写术的英文叫做 Steganography ,根据维基百科的解释,这个英文来源于特里特米乌斯的一本讲述密码学与隐写术的著作 Steganographia ,该书书名源于希腊语,意为“隐秘书写”。(这个不是重点)

所以今天呢,我们要给大家介绍的是隐写术的其中一个分支(也就是其中一种隐写的方法),也就是 StegosaurusStegosaurus 是一款隐写工具,它允许我们在 Python 字节码文件( pycpyo )中嵌入任意 Payload 。由于编码密度较低,因此我们嵌入 Payload 的过程既不会改变源代码的运行行为,也不会改变源文件的文件大小。 Payload 代码会被分散嵌入到字节码之中,所以类似 strings 这样的代码工具无法查找到实际的 PayloadPythondis 模块会返回源文件的字节码,然后我们就可以使用 Stegosaurus 来嵌入 Payload 了。

为了方便维护,我将此项目移至 Github 上:https://github.com/AngelKitty/stegosaurus

首先讲到一个工具,不可避免的,我们需要讲解它的用法,我并不会像文档一样工整的把用法罗列在一起,如果需要了解更加细节的部分请参考 Github上的详细文档,我会拿一些实际的案例去给大家讲解一些常见命令的用法,在后续的文章中,我会大家深入理解 python 反编译的一些东西。

Stegosaurus 仅支持 Python3.6 及其以下版本

拿到一个工具,我们一般会看看它的基本用法:

python3 stegosaurus.py -h
$ python3 -m stegosaurus -h
usage: stegosaurus.py [-h] [-p PAYLOAD] [-r] [-s] [-v] [-x] carrier

positional arguments:
  carrier               Carrier py, pyc or pyo file

optional arguments:
  -h, --help            show this help message and exit
  -p PAYLOAD, --payload PAYLOAD
                        Embed payload in carrier file
  -r, --report          Report max available payload size carrier supports
  -s, --side-by-side    Do not overwrite carrier file, install side by side
                        instead.
  -v, --verbose         Increase verbosity once per use
  -x, --extract         Extract payload from carrier file

技术分享图片

我们可以看到有很多参数选项,我们就以一道赛题来讲解部分参数命令吧~

我们此次要讲解的这道题是来自 BugkuQAQ

赛题链接如下:

http://ctf.bugku.com/files/447e4b626f2d2481809b8690613c1613/QAQ
http://ctf.bugku.com/files/5c02892cd05a9dcd1c5a34ef22dd9c5e/cipher.txt

首先拿到这道题,用 010Editor 乍一眼看过去,我们可以看到一些特征信息:

技术分享图片

可以判断这是个跟 python 有关的东西,通过查阅相关资料可以判断这是个 python 经编译过后的 pyc 文件。这里可能很多小伙伴们可能不理解了,什么是 pyc 文件呢?为什么会生成 pyc 文件? pyc 文件又是何时生成的呢?下面我将一一解答这些问题。

简单来说, pyc 文件就是 Python 的字节码文件,是个二进制文件。我们都知道 Python 是一种全平台的解释性语言,全平台其实就是 Python 文件在经过解释器解释之后(或者称为编译)生成的 pyc 文件可以在多个平台下运行,这样同样也可以隐藏源代码。其实, Python 是完全面向对象的语言, Python 文件在经过解释器解释后生成字节码对象 PyCodeObjectpyc 文件可以理解为是 PyCodeObject 对象的持久化保存方式。而 pyc 文件只有在文件被当成模块导入时才会生成。也就是说, Python 解释器认为,只有 import 进行的模块才需要被重用。 生成 pyc 文件的好处显而易见,当我们多次运行程序时,不需要重新对该模块进行重新的解释。主文件一般只需要加载一次,不会被其他模块导入,所以一般主文件不会生成 pyc 文件。

我们举个例子来说明这个问题:

为了方便起见,我们事先创建一个test文件夹作为此次实验的测试:

mkdir test && cd test/

假设我们现在有个 test.py 文件,文件内容如下:

def print_test():
    print('Hello,Kitty!')

print_test()

我们执行以下命令:

python3 test.py

不用说,想必大家都知道打印出的结果是下面这个:

Hello,Kitty!

我们通过下面命令查看下当前文件夹下有哪些文件:

ls -alh

技术分享图片

我们可以发现,并没有 pyc 文件生成。

‘我们再去创建一个文件为 import_test.py 文件,文件内容如下:

注: test.pyimport_test.py 应当放在同一文件夹下

import test

test.print_test()

我们执行以下命令:

python3 import_test.py

结果如下:

Hello,Kitty!
Hello,Kitty!

诶,为啥会打印出两句相同的话呢?我们再往下看,我们通过下面命令查看下当前文件夹下有哪些文件:

ls -alh

结果如下:

总用量 20K
drwxr-xr-x 3 python python 4.0K 11月  5 20:38 .
drwxrwxr-x 4 python python 4.0K 11月  5 20:25 ..
-rw-r--r-- 1 python python   31 11月  5 20:38 import_test.py
drwxr-xr-x 2 python python 4.0K 11月  5 20:38 __pycache__
-rw-r--r-- 1 python python   58 11月  5 20:28 test.py

诶,多了个 __pycache__ 文件夹,我们进入文件夹下看看有什么?

cd __pycache__ && ls

技术分享图片

我们可以看到生成了一个 test.cpython-36.pyc 。为什么是这样子呢?

我们可以看到,我们在执行 python3 import_test.py 命令的时候,首先开始执行的是 import test ,即导入 test 模块,而一个模块被导入时, PVM(Python Virtual Machine) 会在后台从一系列路径中搜索该模块,其搜索过程如下:

  • 在当前目录下搜索该模块
  • 在环境变量 PYTHONPATH 中指定的路径列表中依次搜索
  • python 安装路径中搜索

事实上, PVM 通过变量 sys.path 中包含的路径来搜索,这个变量里面包含的路径列表就是上面提到的这些路径信息。

模块的搜索路径都放在了 sys.path 列表中,如果缺省的 sys.path 中没有含有自己的模块或包的路径,可以动态的加入 (sys.path.apend) 即可。

事实上, Python 中所有加载到内存的模块都放在 sys.modules 。当 import 一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用 import 的模块的 Local 名字空间中。如果没有加载则从 sys.path 目录中按照模块名称查找模块文件,模块文件可以是 pypycpyd ,找到后将模块载入内存,并加入到 sys.modules 中,并将名称导入到当前的 Local 名字空间。

可以看出来,一个模块不会重复载入。多个不同的模块都可以用 import 引入同一个模块到自己的 Local 名字空间,其实背后的 PyModuleObject 对象只有一个。

在这里,我还要说明一个问题,import 只能导入模块,不能导入模块中的对象(类、函数、变量等)。例如像上面这个例子,我在 test.py 里面定义了一个函数 print_test() ,我在另外一个模块文件 import_test.py不能直接通过 import test.print_testprint_test 导入到本模块文件中,只能用 import test 进行导入。如果我想只导入特定的类、函数、变量,用 from test import print_test 即可。

技术分享图片

既然说到了 import 导入机制,再提一提嵌套导入和 Package 导入。

import 嵌套导入

嵌套,不难理解,就是一个套着一个。小时候我们都玩过俄罗斯套娃吧,俄罗斯套娃就是一个大娃娃里面套着一个小娃娃,小娃娃里面还有更小的娃娃,而这个嵌套导入也是同一个意思。假如我们现在有一个模块,我们想要导入模块 A ,而模块 A 中有含有其他模块需要导入,比如模块 B ,模块 B 中又含有模块 C ,一直这样延续下去,这种方式我们称之为 import 嵌套导入。

对这种嵌套比较容易理解,我们需要注意的一点就是各个模块的 Local 名字空间是独立的,所以上面的例子,本模块 import A 完了后,本模块只能访问模块 A ,不能访问 B 及其它模块。虽然模块 B 已经加载到内存了,如果要访问,还必须明确在本模块中导入 import B

那如果我们有以下嵌套这种情况,我们该怎么处理呢?

比如我们现在有个模块 A

# A.py
from B import D
class C:
    pass

还有个模块 B

# B.py
from A import C
class D:
    pass

我们简单分析一下程序,如果程序运行,应该会去从模块B中调用对象D。

我们尝试执行一下 python A.py

技术分享图片

ImportError 的错误,似乎是没有加载到对象 D ,而我们将 from B import D 改成 import B ,我们似乎就能执行成功了。

技术分享图片

这是怎么回事呢?这其实是跟 Python 内部 import 的机制是有关的,具体到 from B import DPython 内部会分成以下几个步骤:

  • sys.modules 中查找符号 B
  • 如果符号 B 存在,则获得符号 B 对应的 module 对象 <module B> 。从 <module B>__dict__ 中获得符号 D 对应的对象,如果 D 不存在,则抛出异常
  • 如果符号 B 不存在,则创建一个新的 module 对象 <module B> ,注意,此时 module 对象的 __dict__ 为空。执行 B.py 中的表达式,填充 <module B>__dict__ 。从 <module B>__dict__ 中获得 D 对应的对象。如果 D 不存在,则抛出异常。

所以,这个例子的执行顺序如下:

1、执行 A.py 中的 from B import D

注:由于是执行的 python A.py ,所以在 sys.modules 中并没有 <module B> 存在,首先为 B.py 创建一个 module 对象( <module B> ),注意,这时创建的这个 module 对象是空的,里边啥也没有,在 Python 内部创建了这个 module 对象之后,就会解析执行 B.py ,其目的是填充 <module B> 这个 dict

2、执行 B.py 中的 from A import C

注:在执行 B.py 的过程中,会碰到这一句,首先检查 sys.modules 这个 module 缓存中是否已经存在 <module A> 了,由于这时缓存还没有缓存 <module A> ,所以类似的, Python 内部会为 A.py 创建一个 module 对象( <module A> ),然后,同样地,执行 A.py 中的语句。

3、再次执行 A.py 中的 from B import D

注:这时,由于在第 1 步时,创建的 <module B> 对象已经缓存在了 sys.modules 中,所以直接就得到了 <module B> ,但是,注意,从整个过程来看,我们知道,这时 <module B> 还是一个空的对象,里面啥也没有,所以从这个 module 中获得符号 D 的操作就会抛出异常。如果这里只是 import B ,由于 B 这个符号在 sys.modules 中已经存在,所以是不会抛出异常的。

我们可以从下图很清楚的看到 import 嵌套导入的过程:

技术分享图片

Package 导入

(Package) 可以看成模块的集合,只要一个文件夹下面有个 __init__.py 文件,那么这个文件夹就可以看做是一个包。包下面的文件夹还可以成为包(子包)。更进一步的讲,多个较小的包可以聚合成一个较大的包。通过包这种结构,我们可以很方便的进行类的管理和维护,也方便了用户的使用。比如 SQLAlchemy 等都是以包的形式发布给用户的。

包和模块其实是很类似的东西,如果查看包的类型: import SQLAlchemy type(SQLAlchemy) ,可以看到其实也是 <type ‘module‘>import 包的时候查找的路径也是 sys.path

包导入的过程和模块的基本一致,只是导入包的时候会执行此包目录下的 __init__.py ,而不是模块里面的语句了。另外,如果只是单纯的导入包,而包的 __init__.py 中又没有明确的其他初始化操作,那么此包下面的模块是不会自动导入的。

假设我们有如下文件结构:

.
└── PA
    ├── __init__.py
    ├── PB1
    │?? ├── __init__.py
    │?? └── pb1_m.py
    ├── PB2
    │?? ├── __init__.py
    │?? └── pb2_m.py
    └── wave.py

技术分享图片

wave.pypb1_m.pypb2_m.py 文件中我们均定义了如下函数:

def getName():
    pass

__init__.py 文件内容均为空。

我们新建一个 test.py ,内容如下:

import sys
import PA.wave #1
import PA.PB1 #2
import PA.PB1.pb1_m as m1 #3
import PA.PB2.pb2_m #4
PA.wave.getName() #5
m1.getName() #6
PA.PB2.pb2_m.getName() #7

我们运行以后,可以看出是成功执行成功了,我们再看看目录结构:

.
├── PA
│?? ├── __init__.py
│?? ├── __init__.pyc
│?? ├── PB1
│?? │?? ├── __init__.py
│?? │?? ├── __init__.pyc
│?? │?? ├── pb1_m.py
│?? │?? └── pb1_m.pyc
│?? ├── PB2
│?? │?? ├── __init__.py
│?? │?? ├── __init__.pyc
│?? │?? ├── pb2_m.py
│?? │?? └── pb2_m.pyc
│?? ├── wave.py
│?? └── wave.pyc
└── test.py

技术分享图片

我们来分析一下这个过程:

  • 当执行#1 后, sys.modules 会同时存在 PAPA.wave 两个模块,此时可以调用 PA.wave 的任何类或函数了。但不能调用 PA.PB1(2) 下的任何模块。当前 Local 中有了 PA 名字。
  • 当执行 #2 后,只是将 PA.PB1 载入内存, sys.modules 中会有 PAPA.wavePA.PB1 三个模块,但是 PA.PB1 下的任何模块都没有自动载入内存,此时如果直接执行 PA.PB1.pb1_m.getName() 则会出错,因为 PA.PB1 中并没有 pb1_m 。当前 Local 中还是只有 PA 名字,并没有 PA.PB1 名字。
  • 当执行 #3 后,会将 PA.PB1 下的 pb1_m 载入内存, sys.modules 中会有 PAPA.wavePA.PB1PA.PB1.pb1_m 四个模块,此时可以执行 PA.PB1.pb1_m.getName() 了。由于使用了 as ,当前 Local 中除了 PA 名字,另外添加了 m1 作为 PA.PB1.pb1_m 的别名。
  • 当执行 #4 后,会将 PA.PB2PA.PB2.pb2_m 载入内存, sys.modules 中会有 PAPA.wavePA.PB1PA.PB1.pb1_mPA.PB2PA.PB2.pb2_m 六个模块。当前 Local 中还是只有 PAm1
  • 下面的 #5#6#7 都是可以正确运行的。

注:需要注意的问题是如果 PA.PB2.pb2_m 想导入 PA.PB1.pb1_mPA.wave 是可以直接成功的。最好是采用明确的导入路径,对于 ../.. 相对导入路径还是不推荐使用。

既然我们已经知道 pyc 文件的产生,再回到那道赛题,我们尝试将 pyc 文件反编译回 python 源码。我们使用在线的开源工具进行尝试:

技术分享图片

部分代码没有反编译成功???我们可以尝试分析一下,大概意思就是读取 cipher.txt 那个文件,将那个文件内容是通过 base64 编码的,我们的目的是将文件内容解码,然后又已知 key ,通过 encryt 函数进行加密的,我们可以尝试将代码补全:

def encryt(key, plain):
    cipher = ''
    for i in range(len(plain)):
        cipher += chr(ord(key[i % len(key)]) ^ ord(plain[i]))

    return cipher


def getPlainText():
    plain = ''
    with open('cipher.txt') as (f):
        while True:
            line = f.readline()
            if line:
                plain += line
            else:
                break

    return plain.decode('base_64')


def main():
    key = 'LordCasser'
    plain = getPlainText()
    cipher = encryt(key, plain)
    with open('xxx.txt', 'w') as (f):
        f.write(cipher)


if __name__ == '__main__':
    main()

结果如下:

YOU ARE FOOLED
THIS IS NOT THAT YOU WANT
GO ON DUDE
CATCH THAT STEGOSAURUS

提示告诉我们用 STEGOSAURUS 工具进行隐写的,我们直接将隐藏的payload分离出来即可。

python3 stegosaurus.py -x QAQ.pyc

技术分享图片

我们得到了最终的 flag 为:flag{fin4lly_z3r0_d34d}

既然都说到这个份子上了,我们就来分析一下我们是如何通过 Stegosaurus 来嵌入 Payload

我们仍然以上面这个代码为例子,我们设置脚本名称为 encode.py

第一步,我们使用 Stegosaurus 来查看在不改变源文件 (Carrier) 大小的情况下,我们的 Payload 能携带多少字节的数据:

python3 -m stegosaurus encode.py -r

技术分享图片

现在,我们可以安全地嵌入最多24个字节的 Payload 了。如果不想覆盖源文件的话,我们可以使用 -s 参数来单独生成一个嵌入了 Payloadpy 文件:

python3 -m stegosaurus encode.py -s --payload "flag{fin4lly_z3r0_d34d}"

技术分享图片

现在我们可以用 ls 命令查看磁盘目录,嵌入了 Payload 的文件( carrier 文件)和原始的字节码文件两者大小是完全相同的:

技术分享图片

注:如果没有使用 -s 参数,那么原始的字节码文件将会被覆盖。

我们可以通过向 Stegosaurus 传递 -x 参数来提取出 Payload

python3 -m stegosaurus __pycache__/encode.cpython-36-stegosaurus.pyc -x

技术分享图片

我们构造的 Payload 不一定要是一个 ASCII 字符串, shellcode 也是可以的:

技术分享图片

我们重新编写一个 example.py 模块,代码如下:

import sys
import os
import math
def add(a,b):
    return int(a)+int(b)
def sum1(result):
    return int(result)*3

def sum2(result):
    return int(result)/3

def sum3(result):
    return int(result)-3

def main():
    a = 1
    b = 2
    result = add(a,b)
    print(sum1(result))
    print(sum2(result))
    print(sum3(result))

if __name__ == "__main__":
    main()

我们让它携带 Payloadflag_is_here

技术分享图片

我们可以查看嵌入 Payload 之前和之后的 Python 代码运行情况:

技术分享图片

通过 strings 查看 Stegosaurus 嵌入了 Payload 之后的文件输出情况( payload 并没有显示出来):

技术分享图片

接下来使用 Pythondis 模块来查看 Stegosaurus 嵌入 Payload 之前和之后的文件字节码变化情况:

嵌入payload之前:

#( 11/29/[email protected] 5:14下午 )( [email protected] ):~/桌面
   python3 -m dis example.py 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  2           8 LOAD_CONST               0 (0)
             10 LOAD_CONST               1 (None)
             12 IMPORT_NAME              1 (os)
             14 STORE_NAME               1 (os)

  3          16 LOAD_CONST               0 (0)
             18 LOAD_CONST               1 (None)
             20 IMPORT_NAME              2 (math)
             22 STORE_NAME               2 (math)

  4          24 LOAD_CONST               2 (<code object add at 0x7f90479778a0, file "example.py", line 4>)
             26 LOAD_CONST               3 ('add')
             28 MAKE_FUNCTION            0
             30 STORE_NAME               3 (add)

  6          32 LOAD_CONST               4 (<code object sum1 at 0x7f9047977810, file "example.py", line 6>)
             34 LOAD_CONST               5 ('sum1')
             36 MAKE_FUNCTION            0
             38 STORE_NAME               4 (sum1)

  9          40 LOAD_CONST               6 (<code object sum2 at 0x7f9047977ae0, file "example.py", line 9>)
             42 LOAD_CONST               7 ('sum2')
             44 MAKE_FUNCTION            0
             46 STORE_NAME               5 (sum2)

 12          48 LOAD_CONST               8 (<code object sum3 at 0x7f9047977f60, file "example.py", line 12>)
             50 LOAD_CONST               9 ('sum3')
             52 MAKE_FUNCTION            0
             54 STORE_NAME               6 (sum3)

 15          56 LOAD_CONST              10 (<code object main at 0x7f904798c300, file "example.py", line 15>)
             58 LOAD_CONST              11 ('main')
             60 MAKE_FUNCTION            0
             62 STORE_NAME               7 (main)

 23          64 LOAD_NAME                8 (__name__)
             66 LOAD_CONST              12 ('__main__')
             68 COMPARE_OP               2 (==)
             70 POP_JUMP_IF_FALSE       78

 24          72 LOAD_NAME                7 (main)
             74 CALL_FUNCTION            0
             76 POP_TOP
        >>   78 LOAD_CONST               1 (None)
             80 RETURN_VALUE

嵌入 payload 之后:

#( 11/29/[email protected] 5:31下午 )( [email protected] ):~/桌面
   python3 -m dis example.py                                 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  2           8 LOAD_CONST               0 (0)
             10 LOAD_CONST               1 (None)
             12 IMPORT_NAME              1 (os)
             14 STORE_NAME               1 (os)

  3          16 LOAD_CONST               0 (0)
             18 LOAD_CONST               1 (None)
             20 IMPORT_NAME              2 (math)
             22 STORE_NAME               2 (math)

  4          24 LOAD_CONST               2 (<code object add at 0x7f146e7038a0, file "example.py", line 4>)
             26 LOAD_CONST               3 ('add')
             28 MAKE_FUNCTION            0
             30 STORE_NAME               3 (add)

  6          32 LOAD_CONST               4 (<code object sum1 at 0x7f146e703810, file "example.py", line 6>)
             34 LOAD_CONST               5 ('sum1')
             36 MAKE_FUNCTION            0
             38 STORE_NAME               4 (sum1)

  9          40 LOAD_CONST               6 (<code object sum2 at 0x7f146e703ae0, file "example.py", line 9>)
             42 LOAD_CONST               7 ('sum2')
             44 MAKE_FUNCTION            0
             46 STORE_NAME               5 (sum2)

 12          48 LOAD_CONST               8 (<code object sum3 at 0x7f146e703f60, file "example.py", line 12>)
             50 LOAD_CONST               9 ('sum3')
             52 MAKE_FUNCTION            0
             54 STORE_NAME               6 (sum3)

 15          56 LOAD_CONST              10 (<code object main at 0x7f146e718300, file "example.py", line 15>)
             58 LOAD_CONST              11 ('main')
             60 MAKE_FUNCTION            0
             62 STORE_NAME               7 (main)

 23          64 LOAD_NAME                8 (__name__)
             66 LOAD_CONST              12 ('__main__')
             68 COMPARE_OP               2 (==)
             70 POP_JUMP_IF_FALSE       78

 24          72 LOAD_NAME                7 (main)
             74 CALL_FUNCTION            0
             76 POP_TOP
        >>   78 LOAD_CONST               1 (None)
             80 RETURN_VALUE

注: Payload 的发送和接受方法完全取决于用户个人喜好, Stegosaurus 只提供了一种向 Python 字节码文件嵌入或提取 Payload 的方法。但是为了保证嵌入之后的代码文件大小不会发生变化,因此 Stegosaurus 所支持嵌入的 Payload 字节长度十分有限。因此 ,如果你需要嵌入一个很大的 Payload ,那么你可能要将其分散存储于多个字节码文件中了。

为了在不改变源文件大小的情况下向其嵌入 Payload ,我们需要识别出字节码中的无效空间( Dead Zone )。这里所谓的无效空间指的是那些即使被修改也不会改变原 Python 脚本正常行为的那些字节数据。

需要注意的是,我们可以轻而易举地找出 Python3.6 代码中的无效空间。 Python 的引用解释器 CPython 有两种类型的操作码:即无参数的和有参数的。在版本号低于 3.5Python 版本中,根据操作码是否带参,字节码中的操作指令将需要占用 1 个字节或 3 个字节。在 Python3.6 中就不一样了, Python3.6 中所有的指令都占用 2 个字节,并会将无参数指令的第二个字节设置为 0 ,这个字节在其运行过程中将会被解释器忽略。这也就意味着,对于字节码中每一个不带参数的操作指令, Stegosaurus 都可以安全地嵌入长度为 1 个字节的 Payload 代码。

我们可以通过 Stegosaurus-vv 选项来查看 Payload 是如何嵌入到这些无效空间之中的:

#( 11/29/[email protected]:35下午 )( [email protected] ):~/桌面
   python3 -m stegosaurus example.py -s -p "ABCDE" -vv          
2018-11-29 22:36:26,795 - stegosaurus - DEBUG - Validated args
2018-11-29 22:36:26,797 - stegosaurus - INFO - Compiled example.py as __pycache__/example.cpython-36.pyc for use as carrier
2018-11-29 22:36:26,797 - stegosaurus - DEBUG - Read header and bytecode from carrier
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_SUBTRACT (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - INFO - Found 14 bytes available for payload
Payload embedded in carrier
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (65) ----A
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (66) ----B
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (67) ----C
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (68) ----D
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_SUBTRACT (69) ----E
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - Creating new carrier file name for side-by-side install
2018-11-29 22:36:26,799 - stegosaurus - INFO - Wrote carrier file as __pycache__/example.cpython-36-stegosaurus.pyc

参考文献

  • https://bitbucket.org/jherron/stegosaurus/src
  • https://github.com/AngelKitty/stegosaurus
  • https://www.freebuf.com/sectool/129357.html

以上是关于一文弄懂什么是RPC的主要内容,如果未能解决你的问题,请参考以下文章

还没弄懂分布式场景下数据一致性问题?一文教你轻松解决!

一文弄懂分布式存储

一文弄懂“分布式锁”

解读一文弄懂“分布式锁”

一文弄懂 ZooKeeper

三分钟彻底弄懂什么是分布式和微服务架构