2024 DubheCTF pwn wp
o4最赢的一集!*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。
ggbond
一个go写的grpc,8.3的IDA yyds
但有个屑没想到搜文件提proto的工具准备嗯逆,重复一个打开IDA开逆——放弃——再开逆——再放弃的过程
grpc
可以简单理解为远程调用函数
一个grpc server示例(部分)
greeter_server/main.go
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
46package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "hellogrpc/hellogrpc"
)
var (
port = flag.Int("port", 50051, "The server port")
)
// server嵌入了pb.UnimplementedGreeterServer,表示server将实现pb.GreeterServer接口的所有方法
type server struct {
pb.UnimplementedGreeterServer
}
// server结构体的方法,实现了pb.GreeterServer中的SayHello方法
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
// 同上,实现了SayHelloAgain方法
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}
func main() {
flag.Parse() // 解析命令行参数
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) // 建立tcp监听器
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer() // 创建grpc服务器实例
pb.RegisterGreeterServer(s, &server{}) // 注册实例
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil { // 开始监听并处理客户端的请求
log.Fatalf("failed to serve: %v", err)
}
}hellogrpc/hellogrpc.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17syntax = "proto3";
option go_package = "hellogrpc/hellogrpc";
package hellogrpc;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}hellogrpc/hellogrpc_grpc.pb.go(部分),只放一下上面提到的部分
1
2
3
4
5
6
7
8
9
10
11// UnimplementedGreeterServer must be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHelloAgain not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}1
2
3func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
s.RegisterService(&Greeter_ServiceDesc, srv)
}hellogrpc/hellogrpc.pb.go
1
2
3
4
5
6
7
8// The response message containing the greetings
type HelloReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
proto提取
有一种神奇的工具叫 pbtk 可以从文件里提取proto :)
题外话,我刚开始打CTF的时候见过这个工具,但显然我不可能记得这件事QVQ,神奇的命运的轮回(雾)
提出来的proto
1 |
|
python grpc
真不错,有个屑也不知道grpc还有python库:)
grpc工具包可以利用proto文件生成grpc服务类
1 |
|
会生成ggbond_pb2.py和ggbond_pb2_grpc.py两个文件,_pb2中定义了数据结构,_pd2_grpc中定义了方法
交互部分代码,根据ggbond_pb2和ggbond_pb2_grpc中的函数和proto文件猜着写就行
1 |
|
go,狗都不逆,汪
既然会交互了那就随便试试
1 |
|
输出
1 |
|
然后IDA搜”Role No Change.”字符串搜到了处理函数main.(*server).Handler
role_change处理部分,role=3的时候有向栈上复制数据的操作,数据来源是将输入base64解码
那就可以愉快的溢出了 ^v^
exp
由于交互是通过端口进行的,不是将输入输出重定向到远程,所以弹不了shell,直接打orw
- 爆破确认现在端口的fd,这里是7
- 有syscall有gadget没开pie,可以直接用(甚至连flag字符串都有),真方便~
1 |
|
tcpdump抓包,因为是本地所以抓lo
1 |
|
简简又单单,但有笨b没做出来^v^
cvm
要打TLS但有个屑为了调试直接把start_thread patch了^v^
c++,狗都不逆,汪
比赛的时候其实已经把这玩意逆完了
vm的控制数据结构:
1 |
|
支持的指令:
1 |
|
program.bin如下:
rip | 字节码 | 命令 | 栈内容 |
---|---|---|---|
0 | 11 57 65 6c 63 6f 6d 65 20 | push b’Welcome ‘ | b’Welcome ‘ |
9 | 11 74 6f 20 54 53 43 54 46 | push b’to TSCTF’ | b’Welcome to TSCTF’ |
12 | 11 21 0a 0a 53 74 61 72 74 | push b’!\n\nStart’ | b’Welcome to TSCTF!\n\nStart’ |
1b | 11 20 79 6f 75 72 20 65 78 | push b’ your ex’ | b’Welcome to TSCTF!\n\nStart your ex’ |
24 | 11 70 6c 6f 69 74 20 66 72 | push b’ploit fr’ | b’Welcome to TSCTF!\n\nStart your exploit fr’ |
2d | 11 6f 6d 20 68 65 72 65 21 | push b’om here!’ | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
36 | 11 30 00 00 00 00 00 00 00 | push p64(0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30) | |||
3f | 11 00 00 00 00 00 00 00 00 | push p64(0) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30)+p64(0) | |||
48 | 0d | pop p64(0) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
push p64(stack) | +p64(0x30)+p64(stack) | ||
49 | 11 fc d2 98 14 10 2c 24 14 | push p64(output) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30)+p64(stack)+p64(output) | |||
52 | 12 | output(stack,0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(cnt) | |||
53 | 11 30 00 00 00 00 00 00 00 | push p64(0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(cnt)+p64(0x30) | |||
5c | 03 | pop p64(0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
pop p64(0x30) | +p64(0x30-cnt) | ||
push p64(0x30-cnt) | |||
5d | 16 | dup | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30-cnt)+p64(0x30-cnt) | |||
5e | 11 9a 00 00 00 00 00 00 00 | push p64(0x9a) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30-cnt)+p64(0x30-cnt)+p64(0x9a) | |||
67 | 15 | jg | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30-cnt) | |||
68 | 11 9a 00 00 00 00 00 00 00 | push p64(0x9a) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x30-cnt)+p64(0x9a) | |||
71 | 14 | jl | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
72 | 11 9b 00 00 00 00 00 00 00 | push p64(0x9b) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b) | |||
7b | 11 00 10 00 00 00 00 00 00 | push p64(0x1000) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b)+p64(0x1000) | |||
84 | 11 00 00 00 00 00 00 00 00 | push p64(0) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b)+p64(0x1000)+p64(0) | |||
8d | 0d | pop p64(0) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
push p64(stack) | +p64(0x9b)+p64(0x1000)+p64(stack) | ||
8e | 11 1c 85 c6 e3 59 76 6d f0 | push p64(input) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b)+p64(0x1000)+p64(stack) | |||
+p64(input) | |||
97 | 12 | input(stack,0x1000) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b)+p64(cnt) | |||
98 | 01 | pop | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+p64(0x9b) | |||
99 | 0c | ret | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
9a | 17 | hlt | |
9b | 11 48 61 76 65 20 66 75 6e | push b’Have fun’ | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+b’Have fun’ | |||
a4 | 11 08 00 00 00 00 00 00 00 | push p64(8) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+b’Have fun’+p64(8) | |||
ad | 11 30 00 00 00 00 00 00 00 | push p64(0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+b’Have fun’+p64(8)+p64(0x30) | |||
b6 | 0d | pop p64(0x30) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
push p64(stack+0x30) | +b’Have fun’+p64(8)+p64(0x30)+p64(stack) | ||
b7 | 11 fc d2 98 14 10 2c 24 14 | push p64(output) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+b’Have fun’+p64(8)+p64(0x30)+p64(stack) | |||
+p64(output) | |||
c0 | 12 | output(stack+0x30,8) | b’Welcome to TSCTF!\n\nStart your exploit from here!’ |
+b’Have fun’+p64(cnt) | |||
c1 | 17 | hlt |
漏洞点,判断stack overflow的时候用的==,但push有1248四种,可以溢出
且memory out of range还取了页对齐,所以可以溢出0xfff字节
比赛的时候注意到这个了,但当时脑子抽了以为stack在code下面,虽然就算没搞错我把多线程patch掉了且不会打TLS也没戏.jpg
其实也不算太难逆,主要是各种检查和io都是由exception实现的,看起来很丑,嗯,都是c++的锅
利用链
先贴一个线程结构体(部分)
1 |
|
read
read和write函数中会根据__libc_single_threaded全局变量判断是否需要调用__pthread_enable_asynccancel开启异步取消
源码不如汇编系列,珍爱生命原理宏定义
多线程时__libc_single_threaded=0否则__libc_single_threaded=1
__pthread_enable_asynccancel
__pthread_enable_asynccancel大致流程:
- 获取当前线程的cancelhandling,进入循环
- 判断是否已经设置了异步取消CANCELTYPE_BIT,是则跳出循环,否则继续执行
- 设置CANCELTYPE_BIT
- 判断当前线程的cancelhandling是否设置了CANCELED_BITMASK需要被取消,否则跳出循环,是则
- 设置当前线程result为PTHREAD_CANCELED
- 调用__do_cancel取消线程
__do_cancel
- 设置当前线程cancelhandling的EXITING_BITMASK(确保不会再被其他线程取消)
- 调用__pthread_unwind,参数为当前线程的cleanup_jmp_buf
__pthread_unwind
当一个函数拥有多个互斥锁却不幸被cancel时这个函数用于完成他的遗嘱doge
- 初始化线程的exc的一些field
- 调用_Unwind_ForcedUnwind,参数是exc,unwind_stop函数和cleanup_jmp_buf
_Unwind_ForcedUnwind
调用__libc_unwind_link_get获取全局struct unwind_link global,里面的函数都是加密过的
获取当前线程的pointer_guard,解密ptr__Unwind_ForcedUnwind
调用ptr__Unwind_ForcedUnwind指向的函数,这个是libgcc_s.so.1中的_Unwind_ForcedUnwind函数
unwind_stop
在libgcc中执行一些函数后又会回到glibc执行unwind_stop,参数stop_parameter为当前线程的cleanup_jmp_buf
- 在start_thread创建新线程的时候会调用setjmp新建cleanup_jmp_buf,如果这个线程被取消,则调用longjmp返回start_thread进行收尾工作
- unwind_stop会判断最后保存的上下文是不是正在展开的栈帧,是则调用longjmp,需要利用这一过程
longjmp
调用流程如下,这里的函数指针当然也是加密过的:
1 |
|
加密是循环左移0x11不是右移11^v^
exp
用循环,只用dup发的数据太多read可能提前结束
1 |
|
ToySMM
\呆神/ \呆神/ \呆神/
先康康附件
尝试改变一下思维方式,不能什么都追溯到宇宙洪荒,效率低学习成本高容易劝退,说不定走一步看一步会有奇效:)
run.sh
1 |
|
- CFI:一种用于模拟闪存设备的接口,CFI模拟驱动允许将虚拟机中的一块区域映射为可供虚拟机访问的闪存设备
- -global:设置全局设备
- driver:设置了全局的driver pflash的配置,这里是secure on
- 之后如果-driver选项通过if设置的接口类型为pflash则使用这个全局的配置
- pflash01:01表示设备的索引或编号
- driver:设置了全局的driver pflash的配置,这里是secure on
- OVMF_CODE.fd:
- if=pflash:使用pflash接口
- format=raw:驱动器为原始格式
- unit=0:驱动器单元号为0
- file=OVMF_CODE.fd:指定驱动映像文件
- readonly=on:只读
- OVMF_VARS_copy.fd同上
kvmvapic.bin
qemu使用的BIOS ROM,因为不是本地的qemu所以需要这个文件
OVMF
部分内容来自2023HWS的ppt
- UEFI:一种标准,用来定义操作系统与系统固件之间的软件界面,作为BIOS的替代方案
- EKD2:第二代UEFI的官方开发库,UEFI的一份实现代码
- OVMF:一个固件,可以在虚拟机上运行的edk2包
rootfs
rootfs作为硬盘驱动被载入
- startupn.nsh:UEFI shell的shell脚本
- ToyApp.efi:UEFI模块
我是谁我在哪我要干什么
先捋一下题目干了什么我们要干什么
题目主要有两个部分
- ToySMM:一个SMM模块
- ToyApp:一个efi模块
ToySMM
这是一个SMM模块(工具:UEFITool)
模块入口函数ToySMM_entry_5DB9425E(工具:efiXplorer)
大概分为三部分:
利用EFI_SMM_BASE2_PROTOCOL的GetSmstLocation服务获取EFI_SMM_SYSTEM_TABLE2
这个结构体主要提供一些服务(结构体部分成员如下):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct _EFI_SMM_SYSTEM_TABLE2 {
/// 内存服务,申请或释放SMRAM
EFI_ALLOCATE_POOL SmmAllocatePool;
EFI_FREE_POOL SmmFreePool;
EFI_ALLOCATE_PAGES SmmAllocatePages;
EFI_FREE_PAGES SmmFreePages;
/// Protocol服务
EFI_INSTALL_PROTOCOL_INTERFACE SmmInstallProtocolInterface;
EFI_UNINSTALL_PROTOCOL_INTERFACE SmmUninstallProtocolInterface;
EFI_HANDLE_PROTOCOL SmmHandleProtocol;
EFI_SMM_REGISTER_PROTOCOL_NOTIFY SmmRegisterProtocolNotify;
EFI_LOCATE_HANDLE SmmLocateHandle;
EFI_LOCATE_PROTOCOL SmmLocateProtocol;
/// SMI处理函数
EFI_SMM_INTERRUPT_MANAGE SmiManage;
EFI_SMM_INTERRUPT_REGISTER SmiHandlerRegister;
EFI_SMM_INTERRUPT_UNREGISTER SmiHandlerUnRegister;
};利用EFI_SMM_ACCESS2_PROTOCOL的GetCapabilities服务获取EFI_SMRAM_DESCRIPTOR
- 先令GetCapabilities的SmramMap参数为0获取size
- 再申请内存
- 再通过GetCapabilities获取EFI_SMRAM_DESCRIPTOR
- 再通过size>>5获取EFI_SMRAM_DESCRIPTOR的个数(EFI_SMRAM_DESCRIPTOR32字节)
1
2
3
4
5
6
7
8typedef struct {
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_PHYSICAL_ADDRESS CpuStart;
UINT64 PhysicalSize;
UINT64 RegionState;
} EFI_MMRAM_DESCRIPTOR;
typedef EFI_MMRAM_DESCRIPTOR EFI_SMRAM_DESCRIPTOR;然后是一些Hob操作,看不懂(
然后又是一些看不懂的操作…
- 之后输出ToySMM模块的加载基址
- 利用SmiHandlerRegister注册SMM handler
ToyApp
从输出来判断一下这个app的功能:
- 输入shellcode
- 输入Done执行shellcode
- 输入QUIT退出
ToyApp.efi入口函数_ModuleEntryPoint执行的就是这个功能(反汇编完依托答辩)
动态加载地址可以搜索字符串确定(注意UTF-16LE)
SMM
- SMM功能:
- 提供一块隔离内存SMRAM,包含代码和数据
- 只有SMM(ring -2)可以访问
- 进入SMM:
- 发出SMI,向0xB2端口写入数据就可以触发SMI
SmiHandlerRegister
看一下SmiHandlerRegister的源码
1 |
|
SMI_HANDLER结构体:
1 |
|
这里的EFI_SMM_HANDLER_ENTRY_POINT2是个函数
1 |
|
ToySMM中注册的是ToyMain函数
gcSmiHandlerTemplate
通过SMI进入SMM后:
- 会将当前状态存在SMBASE + 0x8000 + 0x7c00,比如各个寄存器的值
- 执行SMBASE + 0x8000处的代码
SMBASE + 0x8000会被初始化为gcSmiHandlerTemplate
函数调用链:
1 |
|
SmiManage中最后会执行之前注册的Handler,ToySMM中是ToyMain
1 |
|
SmmEntryPoint将gSmmCorePrivate->CommunicationBuffer的数据传递给了SmiManage,gSmmCorePrivate是个全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17……
CommunicationBuffer = gSmmCorePrivate->CommunicationBuffer;
BufferSize = gSmmCorePrivate->BufferSize;
……
} else {
CommunicateHeader = (EFI_SMM_COMMUNICATE_HEADER *)CommunicationBuffer;
// BufferSize was updated by the SafeUintnSub() call above.
Status = SmiManage (
&CommunicateHeader->HeaderGuid,
NULL,
CommunicateHeader->Data,
&BufferSize
);
……SmiManage调用SmmCoreFindSmiEntry通过HandlerType(GUID)查找之前注册的SmiHandler,调用Handler
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
29EFI_STATUS
EFIAPI
SmiManage (
IN CONST EFI_GUID *HandlerType,
IN CONST VOID *Context OPTIONAL,
IN OUT VOID *CommBuffer OPTIONAL,
IN OUT UINTN *CommBufferSize OPTIONAL
)
{
……
} else {
//
// Non-root SMI handler
//
SmiEntry = SmmCoreFindSmiEntry ((EFI_GUID *)HandlerType, FALSE);
……
Status = SmiHandler->Handler (
(EFI_HANDLE)SmiHandler,
Context,
CommBuffer,
CommBufferSize
);
……
}
gSmmCorePrivate全局变量定义在PiSmmIpl模块,被初始化为mSmmCorePrivateData
1 |
|
思路
ToyApp处于ring 0,读取真的flag需要ring -2
ToyApp已经给了执行shellcode的功能,可以直接写shellcode发出SMI进入ring -2
SMI执行的handler由mSmmCorePrivateData全局变量决定,可以通过shellcode更改mSmmCorePrivateData
ToySMM的handler中调用了gBS->LocateProtocol,可以更改这个函数指针为PrintFlag来绕过&aaaa == (int *)0x23330000 && cmpString((_BYTE *)0x23330000, &aaaa, 3i64)
这题第一次上的时候附件还有问题,&aaaa != (int *)0x23330000 || !StringCmp(0x23330000i64, &aaaa, 3i64),乐
exp
来自呆神
1 |
|