├── .gitignore ├── Des.py ├── OutputStream.py ├── StuffStructStream.py ├── StuffTableStruct.py ├── constants.py ├── dataHelper.py ├── document └── 挂机赚钱之可转债打新-同花顺逆向分析笔记.md ├── ftj.py ├── header.py ├── hexin.py ├── hookths.py ├── readme.md ├── rsa_utils.py ├── stuffInteractData.py ├── stuffTextData.py ├── utils.py └── 券商参数.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /__pycache__ 3 | /venv 4 | passport.dat 5 | persondata.txt 6 | -------------------------------------------------------------------------------- /Des.py: -------------------------------------------------------------------------------- 1 | # 根据反编译的JAVA撸的3DES的代码 2 | 3 | bitTable = [0x80, 0x40, 0x20, 16, 8, 4, 2, 1] 4 | bigbyte = [0x800000, 0x400000, 0x200000, 0x100000, 0x80000, 0x40000, 0x20000, 0x10000, 0x8000, 0x4000, 0x2000, 0x1000, 5 | 0x800, 0x400, 0x200, 0x100, 0x80, 0x40, 0x20, 16, 8, 4, 2, 1] 6 | key_table = [56, 0x30, 40, 0x20, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 7 | 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 8 | 11, 3] 9 | left_shift_table = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28] 10 | after_shift_table = [13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 11 | 36, 46, 54, 29, 39, 50, 44, 0x20, 0x2F, 43, 0x30, 38, 55, 33, 52, 45, 41, 49, 35, 28, 0x1F] 12 | DES_SPBOX1 = [0x1010400, 0, 0x10000, 0x1010404, 0x1010004, 0x10404, 4, 0x10000, 0x400, 0x1010400, 0x1010404, 0x400, 13 | 0x1000404, 0x1010004, 0x1000000, 4, 0x404, 0x1000400, 0x1000400, 0x10400, 0x10400, 0x1010000, 0x1010000, 14 | 0x1000404, 0x10004, 0x1000004, 0x1000004, 0x10004, 0, 0x404, 0x10404, 0x1000000, 0x10000, 0x1010404, 4, 15 | 0x1010000, 0x1010400, 0x1000000, 0x1000000, 0x400, 0x1010004, 0x10000, 0x10400, 0x1000004, 0x400, 4, 16 | 0x1000404, 0x10404, 0x1010404, 0x10004, 0x1010000, 0x1000404, 0x1000004, 0x404, 0x10404, 0x1010400, 0x404, 17 | 0x1000400, 0x1000400, 0, 0x10004, 0x10400, 0, 0x1010004] 18 | DES_SPBOX2 = [0x80108020, 0x80008000, 0x8000, 0x108020, 0x100000, 0x20, 0x80100020, 0x80008020, 0x80000020, 0x80108020, 19 | 0x80108000, 0x80000000, 0x80008000, 0x100000, 0x20, 0x80100020, 0x108000, 0x100020, 0x80008020, 0, 20 | 0x80000000, 0x8000, 0x108020, 0x80100000, 0x100020, 0x80000020, 0, 0x108000, 0x8020, 0x80108000, 21 | 0x80100000, 0x8020, 0, 0x108020, 0x80100020, 0x100000, 0x80008020, 0x80100000, 0x80108000, 0x8000, 22 | 0x80100000, 0x80008000, 0x20, 0x80108020, 0x108020, 0x20, 0x8000, 0x80000000, 0x8020, 0x80108000, 23 | 0x100000, 0x80000020, 0x100020, 0x80008020, 0x80000020, 0x100020, 0x108000, 0, 0x80008000, 0x8020, 24 | 0x80000000, 0x80100020, 0x80108020, 0x108000] 25 | DES_SPBOX3 = [520, 0x8020200, 0, 0x8020008, 0x8000200, 0, 0x20208, 0x8000200, 0x20008, 0x8000008, 0x8000008, 0x20000, 26 | 0x8020208, 0x20008, 0x8020000, 520, 0x8000000, 8, 0x8020200, 0x200, 0x20200, 0x8020000, 0x8020008, 27 | 0x20208, 0x8000208, 0x20200, 0x20000, 0x8000208, 8, 0x8020208, 0x200, 0x8000000, 0x8020200, 0x8000000, 28 | 0x20008, 520, 0x20000, 0x8020200, 0x8000200, 0, 0x200, 0x20008, 0x8020208, 0x8000200, 0x8000008, 0x200, 0, 29 | 0x8020008, 0x8000208, 0x20000, 0x8000000, 0x8020208, 8, 0x20208, 0x20200, 0x8000008, 0x8020000, 0x8000208, 30 | 520, 0x8020000, 0x20208, 8, 0x8020008, 0x20200] 31 | DES_SPBOX4 = [0x802001, 0x2081, 0x2081, 0x80, 0x802080, 0x800081, 0x800001, 0x2001, 0, 0x802000, 0x802000, 0x802081, 32 | 0x81, 0, 0x800080, 0x800001, 1, 0x2000, 0x800000, 0x802001, 0x80, 0x800000, 0x2001, 0x2080, 0x800081, 1, 33 | 0x2080, 0x800080, 0x2000, 0x802080, 0x802081, 0x81, 0x800080, 0x800001, 0x802000, 0x802081, 0x81, 0, 0, 34 | 0x802000, 0x2080, 0x800080, 0x800081, 1, 0x802001, 0x2081, 0x2081, 0x80, 0x802081, 0x81, 1, 0x2000, 35 | 0x800001, 0x2001, 0x802080, 0x800081, 0x2001, 0x2080, 0x800000, 0x802001, 0x80, 0x800000, 0x2000, 36 | 0x802080] 37 | DES_SPBOX5 = [0x100, 0x2080100, 0x2080000, 0x42000100, 0x80000, 0x100, 0x40000000, 0x2080000, 0x40080100, 0x80000, 38 | 0x2000100, 0x40080100, 0x42000100, 0x42080000, 0x80100, 0x40000000, 0x2000000, 0x40080000, 0x40080000, 0, 39 | 0x40000100, 0x42080100, 0x42080100, 0x2000100, 0x42080000, 0x40000100, 0, 0x42000000, 0x2080100, 40 | 0x2000000, 0x42000000, 0x80100, 0x80000, 0x42000100, 0x100, 0x2000000, 0x40000000, 0x2080000, 0x42000100, 41 | 0x40080100, 0x2000100, 0x40000000, 0x42080000, 0x2080100, 0x40080100, 0x100, 0x2000000, 0x42080000, 42 | 0x42080100, 0x80100, 0x42000000, 0x42080100, 0x2080000, 0, 0x40080000, 0x42000000, 0x80100, 0x2000100, 43 | 0x40000100, 0x80000, 0, 0x40080000, 0x2080100, 0x40000100] 44 | DES_SPBOX6 = [0x20000010, 0x20400000, 0x4000, 0x20404010, 0x20400000, 16, 0x20404010, 0x400000, 0x20004000, 0x404010, 45 | 0x400000, 0x20000010, 0x400010, 0x20004000, 0x20000000, 0x4010, 0, 0x400010, 0x20004010, 0x4000, 0x404000, 46 | 0x20004010, 16, 0x20400010, 0x20400010, 0, 0x404010, 0x20404000, 0x4010, 0x404000, 0x20404000, 0x20000000, 47 | 0x20004000, 16, 0x20400010, 0x404000, 0x20404010, 0x400000, 0x4010, 0x20000010, 0x400000, 0x20004000, 48 | 0x20000000, 0x4010, 0x20000010, 0x20404010, 0x404000, 0x20400000, 0x404010, 0x20404000, 0, 0x20400010, 16, 49 | 0x4000, 0x20400000, 0x404010, 0x4000, 0x400010, 0x20004010, 0, 0x20404000, 0x20000000, 0x400010, 50 | 0x20004010] 51 | DES_SPBOX7 = [0x200000, 0x4200002, 0x4000802, 0, 0x800, 0x4000802, 0x200802, 0x4200800, 0x4200802, 0x200000, 0, 52 | 0x4000002, 2, 0x4000000, 0x4200002, 2050, 0x4000800, 0x200802, 0x200002, 0x4000800, 0x4000002, 0x4200000, 53 | 0x4200800, 0x200002, 0x4200000, 0x800, 2050, 0x4200802, 0x200800, 2, 0x4000000, 0x200800, 0x4000000, 54 | 0x200800, 0x200000, 0x4000802, 0x4000802, 0x4200002, 0x4200002, 2, 0x200002, 0x4000000, 0x4000800, 55 | 0x200000, 0x4200800, 2050, 0x200802, 0x4200800, 2050, 0x4000002, 0x4200802, 0x4200000, 0x200800, 0, 2, 56 | 0x4200802, 0, 0x200802, 0x4200000, 0x800, 0x4000002, 0x4000800, 0x800, 0x200002] 57 | DES_SPBOX8 = [0x10001040, 0x1000, 0x40000, 0x10041040, 0x10000000, 0x10001040, 0x40, 0x10000000, 0x40040, 0x10040000, 58 | 0x10041040, 0x41000, 0x10041000, 0x41040, 0x1000, 0x40, 0x10040000, 0x10000040, 0x10001000, 0x1040, 59 | 0x41000, 0x40040, 0x10040040, 0x10041000, 0x1040, 0, 0, 0x10040040, 0x10000040, 0x10001000, 0x41040, 60 | 0x40000, 0x41040, 0x40000, 0x10041000, 0x1000, 0x40, 0x10040040, 0x1000, 0x41040, 0x10001000, 0x40, 61 | 0x10000040, 0x10040000, 0x10040040, 0x10000000, 0x40000, 0x10001040, 0, 0x10041040, 0x40040, 0x10000040, 62 | 0x10040000, 0x10001000, 0x10001040, 0, 0x10041040, 0x41000, 0x41000, 0x1040, 0x1040, 0x40040, 0x10000000, 63 | 0x10041000] 64 | 65 | 66 | class DES: 67 | n = False 68 | o = [0] * 64 69 | p = [0] * 64 70 | r = [0] * 32 71 | s = [0] * 32 72 | t = [0] * 32 73 | 74 | def SHR(self, a, b): 75 | return (a & 0xFFFFFFFF) >> (b & 255) 76 | 77 | def des_a(self, plain_1, len_plain): 78 | v0 = [0] * 2 79 | self.des_b(plain_1, len_plain, v0) 80 | self.des_c(v0, self.r) 81 | self.des_c(v0, self.s) 82 | self.des_c(v0, self.t) 83 | return self.des_e(v0) 84 | 85 | def des_b(self, arg5, arg6, arg7): 86 | arg7[0] = (arg5[arg6] & 0xFF) << 24 87 | arg7[0] |= (arg5[arg6 + 1] & 0xFF) << 16 88 | arg7[0] |= (arg5[arg6 + 2] & 0xFF) << 8 89 | arg7[0] |= arg5[arg6 + 3] & 0xFF 90 | arg7[1] = (arg5[arg6 + 4] & 0xFF) << 24 91 | arg7[1] |= (arg5[arg6 + 5] & 0xFF) << 16 92 | arg7[1] |= (arg5[arg6 + 6] & 0xFF) << 8 93 | arg7[1] |= arg5[arg6 + 7] & 0xFF 94 | 95 | def des_c(self, arg14, arg15): 96 | v12 = 0xFF00FF 97 | v11 = 0xFFFF 98 | v9 = 0xAAAAAAAA 99 | v0 = arg14[0] 100 | v3 = (self.SHR(v0, 4) ^ arg14[1]) & 0xF0F0F0F 101 | v2 = arg14[1] ^ v3 102 | v0 ^= v3 << 4 103 | v3 = (self.SHR(v0, 16) ^ v2) & v11 104 | v2 ^= v3 105 | v0 ^= v3 << 16 106 | v3 = (self.SHR(v2, 2) ^ v0) & 0x33333333 107 | v0 ^= v3 108 | v2 ^= v3 << 2 109 | v3 = (self.SHR(v2, 8) ^ v0) & v12 110 | v0 ^= v3 111 | v2 ^= v3 << 8 112 | v2 = (self.SHR(v2, 0x1F) & 1 | v2 << 1) & -1 113 | v3 = (v0 ^ v2) & v9 114 | v4 = v0 ^ v3 115 | v3 ^= v2 116 | v4 = (v4 << 1 | self.SHR(v4, 0x1F) & 1) & -1 117 | v0 = 0 118 | v2 = 0 119 | while v0 < 8: 120 | v5 = (v3 << 28 | self.SHR(v3, 4)) ^ arg15[v2] 121 | v2 = v2 + 1 122 | v5 = DES_SPBOX1[self.SHR(v5, 24) & 0x3F] | ( 123 | DES_SPBOX7[v5 & 0x3F] | DES_SPBOX5[self.SHR(v5, 8) & 0x3F] | DES_SPBOX3[ 124 | self.SHR(v5, 16) & 0x3F]) 125 | v6 = arg15[v2] ^ v3 126 | v2 = v2 + 1 127 | v4 ^= v5 | DES_SPBOX8[v6 & 0x3F] | DES_SPBOX6[self.SHR(v6, 8) & 0x3F] | DES_SPBOX4[ 128 | self.SHR(v6, 16) & 0x3F] | DES_SPBOX2[self.SHR(v6, 24) & 0x3F] 129 | v5 = (v4 << 28 | self.SHR(v4, 4)) ^ arg15[v2] 130 | v2 = v2 + 1 131 | v5 = DES_SPBOX1[self.SHR(v5, 24) & 0x3F] | ( 132 | DES_SPBOX7[v5 & 0x3F] | DES_SPBOX5[self.SHR(v5, 8) & 0x3F] | DES_SPBOX3[ 133 | self.SHR(v5, 16) & 0x3F]) 134 | v6 = arg15[v2] ^ v4 135 | v2 = v2 + 1 136 | v3 ^= v5 | DES_SPBOX8[v6 & 0x3F] | DES_SPBOX6[self.SHR(v6, 8) & 0x3F] | DES_SPBOX4[ 137 | self.SHR(v6, 16) & 0x3F] | DES_SPBOX2[self.SHR(v6, 24) & 0x3F] 138 | v0 = v0 + 1 139 | v0 = v3 << 0x1F | self.SHR(v3, 1) 140 | v2 = (v4 ^ v0) & v9 141 | v3 = v4 ^ v2 142 | v0 ^= v2 143 | v2 = v3 << 0x1F | self.SHR(v3, 1) 144 | v3 = (self.SHR(v2, 8) ^ v0) & v12 145 | v0 ^= v3 146 | v2 ^= v3 << 8 147 | v3 = (self.SHR(v2, 2) ^ v0) & 0x33333333 148 | v0 ^= v3 149 | v2 ^= v3 << 2 150 | v3 = (self.SHR(v0, 16) ^ v2) & v11 151 | v2 ^= v3 152 | v0 ^= v3 << 16 153 | v3 = (self.SHR(v0, 4) ^ v2) & 0xF0F0F0F 154 | arg14[0] = (v0 ^ v3 << 4) & 0xffffffff 155 | arg14[1] = (v2 ^ v3) & 0xffffffff 156 | 157 | def des_e(self, arg5): 158 | arg6 = b'' 159 | arg6 += int.to_bytes(self.SHR(arg5[0], 24) & 0xFF, 1, byteorder='little', signed=False) 160 | arg6 += int.to_bytes(self.SHR(arg5[0], 16) & 0xFF, 1, byteorder='little', signed=False) 161 | arg6 += int.to_bytes(self.SHR(arg5[0], 8) & 0xFF, 1, byteorder='little', signed=False) 162 | arg6 += int.to_bytes(arg5[0] & 0xFF, 1, byteorder='little', signed=False) 163 | arg6 += int.to_bytes(self.SHR(arg5[1], 24) & 0xFF, 1, byteorder='little', signed=False) 164 | arg6 += int.to_bytes(self.SHR(arg5[1], 16) & 0xFF, 1, byteorder='little', signed=False) 165 | arg6 += int.to_bytes(self.SHR(arg5[1], 8) & 0xFF, 1, byteorder='little', signed=False) 166 | arg6 += int.to_bytes(arg5[1] & 0xFF, 1, byteorder='little', signed=False) 167 | return arg6 168 | 169 | def des_d(self, param): 170 | param[0:32] = self.r[0:] 171 | 172 | def des_f(self, arg2, arg3): 173 | if self.n: 174 | return self.des_a(arg2, arg3) 175 | 176 | def des_g(self, param): 177 | self.r[0:] = param[0:32] 178 | self.n = False 179 | 180 | def des_h(self, param): 181 | self.des_d(param) 182 | param[32:] = self.s[0:] 183 | 184 | def des_i(self, param): 185 | self.des_g(param) 186 | self.s[0:] = param[32:] 187 | self.des_d(self.t) 188 | self.n = True 189 | 190 | def des_set_key_8(self, arg10, arg11): 191 | v3 = [True] * 56 192 | v4 = [True] * 56 193 | 194 | for v1 in range(56): 195 | v0 = True if (arg10[self.SHR(key_table[v1], 3)] & bitTable[key_table[v1] & 7]) != 0 else False 196 | v3[v1] = v0 197 | 198 | for v1 in range(16): 199 | v0_1 = 15 - v1 << 1 if not arg11 else v1 << 1 200 | v5 = v0_1 + 1 201 | self.r[v5] = 0 202 | self.r[v0_1] = 0 203 | 204 | for v2 in range(28): 205 | v6 = left_shift_table[v1] + v2 206 | v4[v2] = v3[v6] if v6 < 28 else v3[v6 - 28] 207 | 208 | for v2 in range(28, 56): 209 | v6 = left_shift_table[v1] + v2 210 | v4[v2] = v3[v6] if v6 < 56 else v3[v6 - 28] 211 | 212 | for v2 in range(24): 213 | if v4[after_shift_table[v2]]: 214 | self.r[v0_1] |= bigbyte[v2] 215 | if v4[after_shift_table[v2 + 24]]: 216 | self.r[v5] |= bigbyte[v2] 217 | 218 | for v0_1 in range(0, 32, 2): 219 | v1 = self.r[v0_1] 220 | v2 = self.r[v0_1 + 1] 221 | self.r[v0_1] = (0xFC0000 & v1) << 6 | (v1 & 0xFC0) << 10 | (0xFC0000 & v2) >> 10 | (v2 & 0xFC0) >> 6 222 | self.r[v0_1 + 1] = (v1 & 0x3F) << 16 | (0x3F000 & v1) << 12 | (0x3F000 & v2) >> 4 | v2 & 0x3F 223 | 224 | self.n = False 225 | 226 | def des_set_key_16(self, key, opType): 227 | self.des_set_key_8(key[8:], not opType) 228 | self.des_d(self.s) 229 | self.des_set_key_8(key[0:8], opType) 230 | self.des_d(self.t) 231 | self.n = True 232 | 233 | def setKey(self, key, opType): 234 | keyLength = len(key) 235 | if keyLength != 16 and keyLength != 8: 236 | return False 237 | if keyLength == 16: 238 | self.des_set_key_16(key, opType) 239 | else: 240 | self.des_set_key_8(key, opType) 241 | return True 242 | 243 | def operate(self, plain, opType): 244 | cipher_text = b'' 245 | if len(plain) == 0: 246 | return 247 | 248 | if opType: 249 | self.des_i(self.o) 250 | else: 251 | self.des_i(self.p) 252 | 253 | v1 = len(plain) // 8 254 | for v0 in range(v1): 255 | v2 = v0 * 8 256 | cipher_text += self.des_f(plain, v2) 257 | return cipher_text 258 | 259 | def __init__(self, key): 260 | self.setKey(key, True) 261 | self.des_h(self.o) 262 | self.setKey(key, False) 263 | self.des_h(self.p) 264 | pass 265 | 266 | 267 | def des(data, des_key, opType): 268 | des_op = DES(des_key) 269 | return des_op.operate(data, opType) 270 | 271 | -------------------------------------------------------------------------------- /OutputStream.py: -------------------------------------------------------------------------------- 1 | class OutputStream: 2 | data = b'' 3 | index = 0 4 | 5 | def __init__(self, InputStream): 6 | self.data = InputStream 7 | self.index = 0 8 | 9 | def available(self): 10 | return len(self.data) - self.index 11 | 12 | def skipBytes(self, number): 13 | self.index += number 14 | 15 | # 返回指定数量unicode2Utf-8的文本 16 | def readUnicode2UTF8(self, number): 17 | temp = '' 18 | for i in range(number): 19 | temp += chr(self.readChar()) 20 | return temp 21 | 22 | def readBoolean(self): 23 | ret = True if int.from_bytes(self.data[self.index: self.index + 1], byteorder='little') == 1 else False 24 | self.index += 1 25 | return ret 26 | 27 | def readByte(self): 28 | ret = self.data[self.index: self.index + 1] 29 | ret = int.from_bytes(ret, byteorder='little') 30 | self.index += 1 31 | return ret 32 | 33 | # python没有char这玩意儿, 直接返回一个int 34 | def readChar(self): 35 | ret = int.from_bytes(self.data[self.index: self.index + 2], byteorder='little') 36 | self.index += 2 37 | return ret 38 | 39 | def readShort(self): 40 | ret = int.from_bytes(self.data[self.index: self.index + 2], byteorder='little') 41 | self.index += 2 42 | return ret 43 | 44 | def readInt(self): 45 | ret = int.from_bytes(self.data[self.index: self.index + 4], byteorder='little') 46 | self.index += 4 47 | return ret 48 | 49 | def readLong(self): 50 | hex_str = '0x' + self.data[self.index: self.index + 4].hex() 51 | ret = eval(hex_str) 52 | self.index += 4 53 | return ret 54 | 55 | def readUTF(self): 56 | return self.readUnicode2UTF8(self.readUnsignedShort()) 57 | 58 | def readUnsignedShort(self): 59 | ret = int.from_bytes(self.data[self.index: self.index + 2], byteorder='little') 60 | self.index += 2 61 | return ret 62 | 63 | # 读取指定数量的bytes 64 | def readBytes(self, number): 65 | ret = self.data[self.index: self.index + number] 66 | self.index += number 67 | return ret 68 | 69 | # 读取指定数量的bytes, 但是不向前滑动 70 | def readBytesOnly(self, number): 71 | ret = self.data[self.index: self.index + number] 72 | return ret 73 | -------------------------------------------------------------------------------- /StuffStructStream.py: -------------------------------------------------------------------------------- 1 | from OutputStream import OutputStream 2 | import ftj 3 | 4 | 5 | # 这部分代码不要问我命名风格为什么奇怪, 不要问我代码到底啥意思, 我也不知道... 6 | # 代码根据反编译JAVA代码来的, 就硬抄, 硬翻译 7 | 8 | class a: 9 | a = 0 10 | b = 0 11 | c = 0 12 | 13 | def __init__(self, decode_Data): 14 | self.a = decode_Data.readUnsignedShort() 15 | self.b = decode_Data.readByte() 16 | self.c = decode_Data.readByte() 17 | 18 | 19 | class StuffStructStream: 20 | a = b'' 21 | b = 0 22 | c = 0 23 | d = 0 24 | e = 0 25 | f = 0 26 | g = 0 27 | h = [] 28 | i = 0 29 | 30 | def a_out(self, decode_Data): 31 | v3 = decode_Data.readBytes(4) 32 | v1 = 0 33 | v2 = 0 34 | while v1 < 4: 35 | v0 = v2 << 8 36 | if v3[v1] < 0: 37 | v0 += 0x100 38 | v2 = v3[v1] + v0 39 | v1 += 1 40 | return v2 41 | 42 | def a_i_i(self, arg3, arg4): 43 | if arg3 > 9 or arg3 < 1 or arg4 > 9 or arg4 < -1: 44 | return '' 45 | elif arg4 <= -1: 46 | return "cv" + arg3 + "." 47 | else: 48 | return "cv" + arg3 + "." + arg4 49 | 50 | def b_out(self, decode_Data): 51 | if not self.c_i_i(3, -1): 52 | return decode_Data 53 | v7 = self.i - decode_Data.available() 54 | if v7 >= self.e: 55 | return decode_Data 56 | v0 = self.e - v7 57 | v1 = decode_Data.readBytes(v0) 58 | v3 = self.f * self.b 59 | v4 = decode_Data.readInt() 60 | v5 = self.a_out(decode_Data) 61 | v6 = v5 + 1 62 | v6_1 = decode_Data.readBytes(decode_Data.available()) 63 | v8 = b'' 64 | for index in range(v6): 65 | v8 += b'\x00' 66 | ret, v6_1, v8 = ftj.ftj_b(v6_1, 0, v4, v8, v5) 67 | if ret != v3: 68 | return decode_Data 69 | v9 = b'' 70 | for index in range(v3 + v0 + 4): 71 | v9 += b'\x00' 72 | v9 = bytearray(v9) 73 | v9[:len(v1)] = bytearray(v1) 74 | v1_1 = 0 75 | v3 = 0 76 | while v3 < self.f: 77 | v4 = 0 78 | v5 = v0 79 | while v4 < self.b: 80 | v9[v5: v5 + 1] = v8[v1_1: v1_1 + 1] 81 | v5 += self.f 82 | v4 += 1 83 | v1_1 += 1 84 | v3 += 1 85 | v0 += 1 86 | self.i = len(v9) + v7 87 | decode_Data = OutputStream(bytes(v9)) 88 | return decode_Data 89 | 90 | def b_i_i(self, arg3, arg4): 91 | if arg3 > 9 or arg3 < 1 or arg4 > 9 or arg4 < -1: 92 | return '' 93 | elif arg4 <= -1: 94 | return "tb" + str(arg3) + "." 95 | else: 96 | return "tb" + str(arg3) + "." + str(arg4) 97 | 98 | def c(self, decode_Data): 99 | self.a = decode_Data.readBytes(6) 100 | self.b = decode_Data.readInt() 101 | self.c = decode_Data.readLong() 102 | self.d = decode_Data.readInt() 103 | self.e = decode_Data.readUnsignedShort() 104 | self.f = decode_Data.readUnsignedShort() 105 | self.g = decode_Data.readUnsignedShort() 106 | 107 | for temp in range(self.g): 108 | temp = a(decode_Data) 109 | self.h.append(temp) 110 | 111 | def c_i_i(self, arg5, arg6): 112 | if 1 <= arg5 <= 9 and -1 <= arg6 <= 9 and len(self.a) != 0: 113 | v1 = '' 114 | v2_1 = str(self.a, encoding='gbk') 115 | if v2_1[0:2] == 'tb': 116 | v1 = self.b_i_i(arg5, arg6) 117 | elif v2_1[0:2] == 'cv': 118 | v1 = self.a_i_i(arg5, arg6) 119 | if v1 == '': 120 | return False 121 | if arg6 >= 0: 122 | return v2_1 == v1 123 | return v2_1[0:4] == v1[0:4] 124 | 125 | def d(self, decode_Data): 126 | if decode_Data.available() < 28: 127 | return decode_Data 128 | self.i = decode_Data.available() 129 | self.c(decode_Data) 130 | decode_Data = self.b_out(decode_Data) 131 | return decode_Data 132 | -------------------------------------------------------------------------------- /StuffTableStruct.py: -------------------------------------------------------------------------------- 1 | class StuffTableStruct: 2 | u = False 3 | tableHead = [] 4 | n = [] 5 | caption = "" 6 | r = {} 7 | q = {} 8 | s = 0 9 | t = 0 10 | o = 0 11 | p = 0 12 | 13 | def __init__(self, boolean): 14 | self.u = boolean 15 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | # 券商参数详见券商参数.txt 2 | 3 | # 同花顺服务器地址和端口 4 | CONNECT_HOST = 'mobi2.hexin.cn' 5 | CONNECT_PORT = 9528 6 | 7 | # 获取新债数据地址, 该接口可能不是很稳定 8 | CATE_URL = 'http://data.hexin.cn/ipo/bond/cate/info/' 9 | 10 | # 数据包的头部标志, 严格的说fd fd fd fd才是标志, 30 30 30 30是长度一部分, 不过这儿直接把它也算进来吧 11 | DATA_MARK = b'\xfd\xfd\xfd\xfd\x30\x30\x30\x30' 12 | 13 | # socket接收缓冲区大小 14 | BUFFER_CACHE = 16 * 1024 15 | 16 | # 加密_加密密钥rsa参数 17 | RSA_E = 65537 18 | RSA_N = 0xD78558CB2D5E06464BEFC2DDDD11AF1D7D7E2EDB0EE8CB6AE28901B21156970BDCD608C21273F394821199BD7C2D9ADA7BED2E3937235C5926B36094A2C67D97 19 | RSA_KEY_HEADER = b'\x00\x02\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a' \ 20 | b'\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x0a\x00' 21 | 22 | # 同花顺登录rsa密钥 23 | RSA_KEY = '-----BEGIN PUBLIC KEY-----\n'\ 24 | 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLmaOkiR/+zt2U9FXFxIa5NtCj' \ 25 | 'ckfXUNKZ1mpxH198HvjBfq/S4VUggd/9H3iWZZPYGkmbgCsYsNdu8ddPIX4/2Y6O' \ 26 | 'BakGJFvt2BBVffuPZTEY5ZKToIweUd3PoswTJRpb4wGwgKDJOlh8txuu0Yrvnx4n' \ 27 | '2mh3r+1rxWSdsS3QIQIDAQAB' \ 28 | '\n-----END PUBLIC KEY-----' 29 | 30 | # app信息, 不同版本的app以下信息不同, 正常情况下不需要改动, app更新时以下数据需要更新 31 | # 应用版本号 32 | APPLET_VERSION = 'G037.08.431.1.32' 33 | # SVN版本号, 同花顺app大概采用SVN进行版本控制 34 | SVN_VER = '38695f7fc663b7916de88c9b2ebcdda44e86a3de' 35 | # 测试的版本号 36 | TEST_VERSION = '57' 37 | # 当前分支名称 38 | BRANCH_NAME = 'K线分支' 39 | # 渠道ID, 标志着app从哪儿下载 40 | SOURCE_ID = '743' 41 | # 表示此客户端是那个运营商的 42 | SP_CODE = '9588000' 43 | # #设备类型,发送给主站,默认为m, iptv, car, pda 44 | DEV = 'm' 45 | # 还有一些数据, 不是很重要, 直接固定在源码中, 没必要在这儿写出来了 46 | 47 | # 多种设备信息 48 | # 正式上线的时候TYPE/UDID/IMEI/IMSI/MAC地址需要根据自己的实际情况修改或者生成 49 | # 如果需要生成, 可以调用utils模块中的函数进行生成 50 | # 不可多个帐号共享设备信息 51 | # TYPE代表机型 52 | TYPE = '' 53 | UDID = '' 54 | IMEI = '' 55 | IMSI = '' 56 | MAC = '' 57 | HD_INFO = 'MAC:' + MAC.replace(':', '-') + ',IMEI:' + IMEI + ',IMSI:' + IMSI 58 | 59 | # debug为0,关闭调试功能,该模式下正常操作 60 | # debug为1,开启调试功能,该模式下不进行网络连接 61 | debug = 0 62 | 63 | # log为1,开启详细日志功能,输出收发数据包内容 64 | # log为0,关闭详细日志功能,不输出数据包详情,只输出关键的节点数据 65 | log = 1 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /dataHelper.py: -------------------------------------------------------------------------------- 1 | from StuffStructStream import StuffStructStream 2 | from StuffTableStruct import StuffTableStruct 3 | import json 4 | 5 | 6 | class fth: 7 | a = 0x80000000 8 | b = 0xFFFFFFFF 9 | c = 0 10 | d = 0 11 | e = 0 12 | f = 0 13 | g = 0 14 | 15 | def d_m(self): 16 | self.c = 0 17 | self.d = 0 18 | self.e = 0 19 | self.f = 0 20 | self.g = 0 21 | 22 | def c_m(self): 23 | if self.g == -2147483648 or self.g == -1 or self.g == self.a or self.g == self.b: 24 | return True 25 | return False 26 | 27 | def a_m(self): 28 | return 0 if self.f == 0 else self.e 29 | 30 | def a_long(self, arg4): 31 | if arg4 == 0: 32 | self.d_m() 33 | self.g = arg4 34 | self.c = int(0x7FFFFFF & arg4) 35 | self.d = int((0x8000000 & arg4) >> 27) 36 | self.e = int((0x70000000 & arg4) >> 28) 37 | self.f = int((-2147483648 & arg4) >> 0x1F) 38 | 39 | def e_m(self): 40 | return False if self.c != 0 or self.d != 0 or self.e != 0 or self.f != 0 else True 41 | 42 | def b_m(self): 43 | if self.c_m(): 44 | v0 = -2147483648 45 | elif self.e_m(): 46 | v0 = 0 47 | else: 48 | v0 = pow(10, self.e) 49 | if self.f == 1: 50 | v0 = self.c / v0 51 | else: 52 | v0 *= self.c 53 | if self.d != 0: 54 | v0 = -v0 55 | return v0 56 | 57 | 58 | def handle_case_0(data_header, decode_Data): 59 | if decode_Data.available() < 24: 60 | return '' 61 | 62 | v2_1 = True if data_header.id != -1 else False 63 | type_str = str(decode_Data.readBytesOnly(6), encoding='gbk') 64 | 65 | if type_str[0:2] == '8,': 66 | data = decode_Data.readBytes(decode_Data.available()) 67 | return str(data, encoding='gbk') 68 | 69 | if type_str[0:2] != 'tb' and type_str[0:2] != 'cv': 70 | return '' 71 | 72 | sss = StuffStructStream() 73 | decode_Data = sss.d(decode_Data) 74 | if type_str[0:2] == 'tb': 75 | sts = StuffTableStruct(v2_1) 76 | title = decode_Data.readUTF() 77 | sts.caption = title 78 | v4 = readTableExtData(sss, decode_Data, sts) 79 | readTableData(sss, decode_Data, sts, v4) 80 | json_data = json.dumps(v4) 81 | return json_data 82 | 83 | # cv的数据暂未处理 84 | return 'this is a stuffCurveData, no decoding' 85 | 86 | 87 | def readTableExtData(sss, decode_Data, sts): 88 | v3 = sss.i 89 | v4 = [] 90 | # 读取扩展数据 91 | if sss.e > v3 - decode_Data.available(): 92 | v0 = 0 93 | # 此处v2和v5似乎没什么用, 先屏蔽掉再说 94 | # v2 = 0 95 | # v5 = [] 96 | while v0 < sss.g: 97 | v6 = decode_Data.readUTF() 98 | if v6 != "": 99 | # v2 = 1 100 | v4.append(v6) 101 | # v5.append(sss.h[v0].a & 0x8FFF) 102 | v0 += 1 103 | ''' 104 | if v2 != 0: 105 | sts.tableHead = v4 106 | sts.n = v5 107 | ''' 108 | v4 = {'TableHeader': v4} 109 | if sss.e > v3 - decode_Data.available(): 110 | v4_1 = decode_Data.readShort() 111 | if 0 <= v4_1 <= 100: 112 | v5_1 = {} 113 | v6_1 = sts.r 114 | for v2 in range(v4_1): 115 | v7 = decode_Data.readUnsignedShort() 116 | v8 = v7 & 0x8FFF 117 | mark = v7 & 0x7000 118 | if mark == 0: 119 | v0_1 = decode_Data.readUTF() 120 | if v0_1 != "": 121 | v5_1[v8] = v0_1 122 | else: 123 | break 124 | elif mark == 0x2000: 125 | v5_1[v8] = decode_Data.readInt() 126 | elif mark == 0x6000: 127 | if v8 == 0x8008: 128 | v9 = decode_Data.readShort() 129 | if v9 > 0: 130 | v10 = [] 131 | for v0 in range(v9): 132 | v10.append(decode_Data.readInt()) 133 | v5_1[v8] = v10 134 | v6_1[v8] = v7 135 | sts.q = v5_1 136 | v0 = v3 - decode_Data.available() 137 | if sss.e > v0: 138 | decode_Data.skipBytes(sss.e - v0) 139 | else: 140 | v0 = v3 - decode_Data.available() 141 | if sss.e > v0: 142 | decode_Data.skipBytes(sss.e - v0) 143 | 144 | if not v4: 145 | v4 = {'TableHeader': []} 146 | return v4 147 | 148 | 149 | def readTableData(sss, decode_Data, sts, json_data): 150 | v2 = 0 151 | v11 = 0x8008 in sts.r if sts.r != {} else False 152 | if v11: 153 | v2 = False if sts.q == {} else sts.q[0x8008] 154 | if v2: 155 | v8 = v2 156 | v9 = sss.c 157 | v10 = sss.g 158 | else: 159 | v8 = v2 160 | v9 = sss.g 161 | v10 = sss.b 162 | if v10 > 0 and v9 > 0: 163 | # v12 = {} 164 | # v13 = {} 165 | # v14 = sts.r 166 | v15 = fth() 167 | # row 168 | TableDatas = [] 169 | for v7 in range(v10): 170 | # column 171 | TableData = [] 172 | for v6 in range(v9): 173 | v16 = '' 174 | if v11: 175 | v3 = v8[v6] 176 | v4 = sss.h[v7].a | v3 177 | else: 178 | v4 = sss.h[v6].a 179 | v3 = 0x8FFF & v4 180 | 181 | ''' 182 | v5 = v3 183 | if v7 == 0: 184 | v2_2 = [] 185 | v3_1 = [] 186 | v14[v5] = v4 187 | v12[v5] = v2_2 188 | v13[v5] = v3_1 189 | TableData = v2_2 190 | else: 191 | v2 = v12[v5] 192 | v3_2 = v13[v5] 193 | v5_2 = v2 194 | ''' 195 | 196 | if v4 & 0x7000 == 0: 197 | v2_1 = int(sss.h[v7].b / 2 if v11 else sss.h[v6].b / 2) 198 | v4_1 = decode_Data.readUnicode2UTF8(v2_1) 199 | v2_1 = decode_Data.readByte() 200 | if v4_1 != '': 201 | v16 += v4_1 202 | v16 = eus_a_i_s(v2_1, v16) 203 | elif v4 & 0x7000 == 4096: 204 | arg4 = decode_Data.readLong() 205 | v15.a_long(arg4) 206 | v2_1 = decode_Data.readByte() 207 | v16 = eus_a_fth_i_s(v15, v2_1, v16) 208 | elif v4 & 0x7000 == 8192: 209 | v4 = str(decode_Data.readInt()) 210 | v2_1 = decode_Data.readByte() 211 | v16 += v4 212 | v16 = eus_a_i_s(v2_1, v16) 213 | elif v4 & 0x7000 == 1288: 214 | v4 = str(decode_Data.readShort()) 215 | v2_1 = decode_Data.readByte() 216 | v16 += v4 217 | v16 = eus_a_i_s(v2_1, v16) 218 | TableData.append(v16.strip()) 219 | # fse_a = [0xFF000000, 0xFF00A8A8, 0xFF00FF00, 0xFF00FFFF, 0xFF5454FF, 0xFF54FF54, 0xFF54FFFF, 0xFFA80000, 220 | # 0xFFA8A8A8, 0xFFC8C8C8, 0xFFFF0000, 0xFFFF5454, 0xFFFF54FF, 0xFFFFFF00, 0xFFFFFF54, -1] 221 | # v3_1.append(0 if (v2_1 & 15) < 0 or (v2_1 & 15) >= len(fse_a) else fse_a[v2_1 & 15]) 222 | TableDatas.append(TableData) 223 | json_data['TableData'] = TableDatas 224 | sts.s = v10 225 | sts.t = v9 226 | # sts.o = v12 227 | # sts.p = v13 228 | 229 | 230 | def fse_a_i(arg1): 231 | if arg1 == 16: 232 | return '万手' 233 | elif arg1 == 0x20: 234 | return '%' 235 | elif arg1 == 0x30: 236 | return '亿' 237 | elif arg1 == 0x40: 238 | return '万' 239 | elif arg1 == 0: 240 | return '' 241 | 242 | 243 | def eus_a_i_s(arg1, arg2): 244 | v0 = fse_a_i(arg1 & 0xF0) 245 | if v0 != '': 246 | return arg2 + v0 247 | return arg2 248 | 249 | 250 | def eus_a_fth_i_s(arg4, arg5, arg6): 251 | if arg4.c_m(): 252 | return arg6 + '--' 253 | else: 254 | arg6 = fti_a(arg4.b_m(), arg4.a_m(), True) 255 | arg6 = eus_a_i_s(arg5, arg6) 256 | return arg6 257 | 258 | 259 | def fti_a(arg10, arg12, arg13): 260 | fti_a = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000] 261 | v9 = '.' 262 | v8 = '0' 263 | v0 = 0.5 264 | v2 = 0 265 | v4 = 0 266 | arg14 = '' 267 | if 0 <= arg12 < len(fti_a): 268 | if arg10 != v2: 269 | if arg12 == 0: 270 | if not arg13: 271 | v0 = arg10 272 | elif arg10 > v2: 273 | v0 += arg10 274 | else: 275 | v0 = arg10 - v0 276 | arg14 += str(v0) 277 | else: 278 | if arg12 <= 0: 279 | return arg14 280 | if arg10 < v2: 281 | v5 = 1 282 | arg10 = -arg10 283 | else: 284 | v5 = 0 285 | if not arg13: 286 | v0 = v2 287 | v0_1 = v0 + fti_a[arg12] * arg10 288 | arg14 += str(v0_1) 289 | v0 = v0_1 / fti_a[arg12] 290 | if v0 < 1: 291 | if v0 < 0.1: 292 | v1 = arg12 - len(arg14) 293 | for _ in range(v1): 294 | arg14 = v8 + arg14 295 | arg14 = '0.' + arg14 296 | else: 297 | if len(arg14) - arg12 >= 0: 298 | arg14 = arg14[0:len(arg14) - arg12] + str(v9) + arg14[len(arg14) - arg12:] 299 | if v5 == 0: 300 | return arg14 301 | arg14 = '-' + arg14 302 | return arg14 303 | arg14 += v8 304 | if arg12 <= 0: 305 | return arg14 306 | arg14 += v9 307 | while v4 < arg12: 308 | arg14 += v8 309 | v4 += 1 310 | return arg14 311 | return arg14 312 | 313 | -------------------------------------------------------------------------------- /document/挂机赚钱之可转债打新-同花顺逆向分析笔记.md: -------------------------------------------------------------------------------- 1 | # 挂机赚钱之可转债打新-同花顺逆向分析笔记 2 | 3 | ## 前言 4 | 三月份的时候,接触了一下可转债打新,中了三次。感觉操作简便,收益率可观,于是便想着搞一个自动化打新工具。但是苦于没有合适的API接口,自己还需要上课,想着手动申购一下也不麻烦。五月份可转债数量很少,自己漏掉几个,很难受,于是决定逆向一下同花顺app,搞一个自动化申购的接口出来,于是便有了这篇文章。目前只有自动化申购的接口,卖出,撤单等各类股票交易接口暂时未完成,别问,问就是敏捷开发。测试了几天,都没有什么问题。这个话题不知道能不能聊,先发出来试试。/狗头 5 | ## 用到的工具 6 | JEB、GDA、xposed、frida、fiddler、wireshark、DDMS、pycharm、雷电模拟器、同花顺appV10.02.12 7 | ## 寻找核心dex 8 | 通过直接解包逆向app,发现其dex并不是核心dex,而是类似于一个加载器,利用so接口获取odex并完成加载。 通过启动时发送的MonitorInfoReceive数据包,可以发现软件odex以及dex在本地目录有保存。 9 | ![](https://gitee.com/ysybh/image_bed/raw/master/img/20200615140930.png) 10 | 在相应目录下果然找到了相关文件: 11 | ![](https://gitee.com/ysybh/image_bed/raw/master/img/20200615141241.png) 12 | 将文件复制到电脑后,发现dex文件异常,而odex正常,直接将odex中dex.035(dex文件的开头标识符)字符前面的字段删除,得到dex文件,正常打开。此处我没有深入研究app加载的流程以及dex文件显示异常的原因,对比太麻烦了==实际工作中,大部分情况下这样拿到dex文件已经够用了,但是部分函数反编译的时候会有问题,对比采用frida hook拿到的dex文件不存在问题。因此最后采用的是通过frida拿到的dex文件。 13 | 该app一共有7个dex,分开研究肯定很麻烦,这么多dex在混淆的情况下需要合并分析才行。这儿安利JEB,对multidex支持很好,希望GDA也能够增加这个功能。但是JEB对各种跳转显示的不好,各种goto,翻译一部分代码的时候,N多goto差点儿让我猝死。 14 | 至此,已经拿到了核心dex,并且通过JEB能够合并分析。 15 | ## 寻找日志函数 16 | 我逆向分析的时候喜欢先搞定日志函数,然后通过hook日志函数来寻找突破点。在这个案例中,直接看JEB字符串部分,找到疑似日志的部分,跳转过去,定位就行。这部分没什么可谈的,日志部分是最简单的。最后定位于frr类以及frq类,采用frida进行hook。 17 | ```javascript 18 | var frr = Java.use('frr'); 19 | frr.a.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 20 | send(arg1 + " : " + arg2); 21 | }; 22 | frr.b.implementation = function (arg1, arg2) { 23 | send(arg1 + " bbb: " + arg2); 24 | }; 25 | frr.c.implementation = function (arg1, arg2) { 26 | send(arg1 + " : " + arg2); 27 | }; 28 | frr.d.implementation = function (arg1, arg2) { 29 | send(arg1 + " : " + arg2); 30 | }; 31 | frr.e.implementation = function (arg1, arg2) { 32 | send(arg1 + " : " + arg2); 33 | }; 34 | 35 | var frq = Java.use('frq'); 36 | frq.a.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 37 | this.a(arg1, arg2); 38 | send(arg1 + " : " + arg2); 39 | }; 40 | frq.a.overload('java.lang.String', 'java.lang.String', 'boolean').implementation = function (arg1, arg2, arg3) { 41 | this.a(arg1, arg2, arg3); 42 | send(arg1 + " : " + arg2); 43 | }; 44 | frq.b.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 45 | this.b(arg1, arg2); 46 | send(arg1 + " : " + arg2); 47 | }; 48 | frq.c.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 49 | this.c(arg1, arg2); 50 | send(arg1 + " : " + arg2); 51 | }; 52 | frq.d.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 53 | this.d(arg1, arg2); 54 | send(arg1 + " : " + arg2); 55 | }; 56 | frq.e.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 57 | this.e(arg1, arg2); 58 | send(arg1 + " : " + arg2); 59 | }; 60 | ``` 61 | 通过日志没有看到多少有用的东西,但是可以了解到同花顺app采用TCP通信,并且可以拿到IP地址和服务器端口,方便下一步的抓包。 62 | ## 连接认证 63 | 通常我分析app会fiddler和wireshark全部开启,方便抓包分析。 64 | 同花顺app整体协议采用TCP通信协议,很多不重要的数据包并没有加密,直接采用的明文方式。这个给出了可乘之机。 65 | 首次启动app会发送一条注册设备的数据包,服务器返回passport.dat数据包作为日后连接登录的凭证。以后的连接数据包全部采用passport.dat。 66 | ![](https://gitee.com/ysybh/image_bed/raw/master/img/20200615143741.png) 67 | ![](https://gitee.com/ysybh/image_bed/raw/master/img/20200615144102.png) 68 | 值得一提的是,此处发送的时候,采用的宽字符。python实现部分: 69 | ```python 70 | str_info = 'ScreenWidth=720' 71 | str_info += '\r\nScreenHeight=1280' 72 | str_info += '\r\nsmallestWidth=0dp' 73 | str_info += '\r\ndensity=1.0' 74 | str_info += '\r\nrealdata=true' 75 | str_info += '\r\ntime2012=1' 76 | str_info += '\r\nAppletVersion=' + constants.APPLET_VERSION 77 | str_info += '\r\nsvnver=' + constants.SVN_VER 78 | str_info += '\r\nTestVersion=' + constants.TEST_VERSION 79 | str_info += '\r\nBranchName=' + constants.BRANCH_NAME 80 | str_info += '\r\nFunClientSupport=0111111111100011111111' 81 | str_info += '\r\napp=android' 82 | str_info += '\r\nfor=ths_am_gphone_login' 83 | str_info += '\r\nprogid=500' 84 | str_info += '\r\nnet=1' 85 | str_info += '\r\nqsid=800' 86 | str_info += '\r\nsourceid=' + constants.SOURCE_ID 87 | str_info += '\r\nspcode=' + constants.SP_CODE 88 | str_info += '\r\nchannelid=' + constants.SOURCE_ID 89 | str_info += '\r\ntype=' + constants.TYPE 90 | str_info += '\r\nudid=' + constants.UDID 91 | str_info += '\r\nimei=' + constants.IMEI 92 | str_info += '\r\nsim=' + constants.UDID 93 | str_info += '\r\nimsi=' + constants.IMSI 94 | str_info += '\r\nmacA=' + constants.MAC 95 | str_info += '\r\nsdk=22' 96 | str_info += '\r\nsdkn=5.1.1' 97 | str_info += '\r\nCA=4' 98 | str_info += '\r\ndev=' + constants.DEV 99 | str_info += '\r\n' 100 | data = b'' 101 | for i in range(len(str_info)): 102 | data += int.to_bytes(ord(str_info[i]), 2, byteorder='little', signed=False) 103 | 104 | data = int.to_bytes(len(str_info), 2, byteorder='little', signed=False) + data 105 | 106 | # data长度不够8的倍数则用00补齐 107 | data += b"\x00\x00\x00\x00\x00\x00\x00" 108 | data = data[0:int(len(data) / 8) * 8] 109 | ``` 110 | 数据包组包完毕,直接在前面加上头部分就行,后续统一讲头部分。 111 | ## 手机号绑定(验证码发送及验证) 112 | 其实这部分没有什么好讲的,都是明文,只有一个RSA加密,直接搜索字符串,找到RSA的公钥就ok,单纯的体力活。直接给出这两部分的python实现部分: 113 | ```python 114 | # 获取验证码 115 | reqpage = str(random.randint(10000, 99999)) 116 | enc_account = base64.b64encode(rsa_utils.rsa_encrypt(account.encode('utf-8'))) 117 | enc_account = parse.quote(enc_account) 118 | url = 'verify?reqtype=wlh_thsreg_modify&mobile_login=1&qsid=800®flag&udid=' + constants.UDID + '&encoding=GBK&mobile=' + enc_account + '&rsa_version=default_4&foreign=1&foreign_country=86' 119 | str_info = '[frame]' 120 | str_info += '\r\nid=4222' 121 | str_info += '\r\npageList=' + reqpage 122 | str_info += '\r\nreqPage=' + reqpage 123 | str_info += '\r\nreqPageCount=1' 124 | str_info += '\r\n[' + reqpage + ']' 125 | str_info += '\r\nid=1101' 126 | str_info += '\r\nhost=auth' 127 | str_info += '\r\nurl=' + url 128 | str_info += '\r\n' 129 | ``` 130 | ```python 131 | # 验证验证码,采用密码登录也是一样的,不过几个参数变化 132 | reqpage = str(random.randint(10000, 99999)) 133 | 134 | enc_account = base64.b64encode(rsa_utils.rsa_encrypt(account.encode('utf-8'))) 135 | enc_account = str(enc_account, 'utf-8') 136 | enc_password = base64.b64encode(rsa_utils.rsa_encrypt(password.encode('utf-8'))) 137 | enc_password = str(enc_password, 'utf-8') 138 | 139 | str_info = '[frame]' 140 | str_info += '\r\nid=2054' 141 | str_info += '\r\npageList=' + reqpage 142 | str_info += '\r\nreqPage=' + reqpage 143 | str_info += '\r\nreqPageCount=1' 144 | str_info += '\r\n[' + reqpage + ']' 145 | str_info += '\r\nid=1001' 146 | str_info += '\r\ncrypt=2' 147 | str_info += '\r\nctrlcount=2' 148 | str_info += '\r\nctrlid_0=34338' 149 | str_info += '\r\nctrlvalue_0=' + enc_account 150 | str_info += '\r\nctrlid_1=34339' 151 | str_info += '\r\nctrlvalue_1=' + enc_password 152 | str_info += '\r\nreqctrl=4304' 153 | str_info += '\r\nloginmode=1' 154 | if not isSMS: 155 | str_info += '\r\nloginType=3\r\n' 156 | else: 157 | str_info += '\r\nforeign=1' 158 | str_info += '\r\nforeign_country=86' 159 | str_info += '\r\nloginType=7\r\n' 160 | ``` 161 | ## 券商登录 162 | 和券商相关的协议部分全部采用DES加密,DES密钥由客户端生成,512位RSA密钥加密过后在券商登录阶段发送至服务器。这部分不再是简单的上述的文本,而是类似于TLV的结构体。其中T为一个字节,L为双字节,V根据L的值来确定。 163 | 这部分定位比较困难,因此需要采用DDMS来找到关键位置。寻找过程比较枯燥无味,这部分正如之前网友讲的,没什么营养。个人也没怎么记录,因此直接讲解一下该部分组包方法。 164 | ```python 165 | qssj = wtid + "#" + qsid + "#" + dtkltype + "#1#" 166 | reqpage = str(random.randint(10000, 99999)) 167 | 168 | data = b'\x13\x02' + b'\x00\x01\x00\x30\x01\x01\x00\x30' 169 | 170 | data += int.to_bytes(2, 1, byteorder='little', signed=False) 171 | data += int.to_bytes(len(account), 2, byteorder='little', signed=False) 172 | data += str.encode(account) 173 | 174 | data += int.to_bytes(3, 1, byteorder='little', signed=False) 175 | data += int.to_bytes(len(password), 2, byteorder='little', signed=False) 176 | data += str.encode(password) 177 | 178 | data += int.to_bytes(4, 1, byteorder='little', signed=False) 179 | data += int.to_bytes(len(txmm), 2, byteorder='little', signed=False) 180 | data += str.encode(txmm) 181 | 182 | data += int.to_bytes(5, 1, byteorder='little', signed=False) 183 | data += int.to_bytes(0, 2, byteorder='little', signed=False) 184 | 185 | data += int.to_bytes(6, 1, byteorder='little', signed=False) 186 | data += int.to_bytes(len(qssj), 2, byteorder='little', signed=False) 187 | data += str.encode(qssj) 188 | 189 | data += int.to_bytes(7, 1, byteorder='little', signed=False) 190 | data += int.to_bytes(len(reqpage), 2, byteorder='little', signed=False) 191 | data += str.encode(reqpage) 192 | 193 | data += int.to_bytes(8, 1, byteorder='little', signed=False) 194 | data += int.to_bytes(1, 2, byteorder='little', signed=False) 195 | data += int.to_bytes(49, 1, byteorder='little', signed=False) 196 | 197 | data += int.to_bytes(9, 1, byteorder='little', signed=False) 198 | HD_INFO = 'HDInfo=' + constants.HD_INFO 199 | data += int.to_bytes(len(HD_INFO), 2, byteorder='little', signed=False) 200 | data += str.encode(HD_INFO) 201 | 202 | # 这部分数据直接固定 203 | data += b'\x0a\x00\x00\x0b\x00\x00\x0c\x00\x00\x0d\x01\x00\x30\x0e\x00\x00\x0f\x00\x00\x10\x00\x00\x11\x00\x00' \ 204 | b'\x12\x00\x00' 205 | ``` 206 | 需要发送的数据构造完毕后,在其前面添加包序号以及两字节的包头等内容后补齐至8的倍数。补齐后,数据采用随机生成的16字节密钥完成DES加密,密钥通过RSA(该处RSA加密与上述不同)加密后放在之前加密好的密文之前。 207 | ```python 208 | enc_data = Des.des(data, globals()['server_key'], True) 209 | enc_key = constants.RSA_KEY_HEADER 210 | enc_key += globals()['qs_login_header'] 211 | enc_key += globals()['server_key'] 212 | enc_key = rsa_utils.rsa_encrypt_key(enc_key) 213 | enc_key_length = len(enc_key) 214 | data = enc_key 215 | data += enc_data 216 | ``` 217 | ## 申购可转债 218 | 登录完成后,来到了申购可转债环节。这部分可转债数据构造完成后,就可以采用上述生成的16字节密钥进行加密了。 219 | ```python 220 | reqpage = random.randint(10000, 99999).__str__() 221 | str_info = '[frame]' 222 | str_info += '\r\nid=2682' 223 | str_info += '\r\npageList=' + reqpage 224 | str_info += '\r\nreqPage=' + reqpage 225 | str_info += '\r\nreqPageCount=1' 226 | str_info += '\r\nqsid=' + globals()['qsid'] 227 | str_info += '\r\nwtaccount=' + globals()['wtaccount'] 228 | str_info += '\r\nwttype=' + globals()['dtkltype'] 229 | str_info += '\r\n[' + reqpage + ']' 230 | str_info += '\r\nid=1820' 231 | str_info += '\r\nreqctrl=2001' 232 | str_info += '\nctrlid_0=36641' 233 | str_info += '\nctrlvalue_0=1' 234 | str_info += '\nctrlid_1=36615' 235 | str_info += '\nctrlvalue_1=' + quantity 236 | str_info += '\nctrlid_2=2102' 237 | str_info += '\nctrlvalue_2=' + code 238 | str_info += '\nctrlid_3=2127' 239 | str_info += '\nctrlvalue_3=' + price 240 | str_info += '\nctrlcount=4' 241 | str_info += '\r\nHDInfo=' + constants.HD_INFO 242 | ``` 243 | ## 包头部分 244 | 包头部分主要包含了当前数据包的长度以及类型之类的信息。 245 | ```python 246 | full_data = b'' 247 | full_data += int.to_bytes(data_header.headLength, 2, byteorder='little', signed=False) 248 | full_data += int.to_bytes(data_header.id, 4, byteorder='little', signed=False) 249 | full_data += int.to_bytes(data_header.type, 4, byteorder='little', signed=False) 250 | full_data += int.to_bytes(data_header.pageId, 2, byteorder='little', signed=False) 251 | full_data += int.to_bytes(data_header.dataLength, 4, byteorder='little', signed=False) 252 | full_data += int.to_bytes(data_header.frameId, 4, byteorder='little', signed=False) 253 | full_data += int.to_bytes(data_header.textLength, 4, byteorder='little', signed=False) 254 | full_data += int.to_bytes(data_header.sessionType, 4, byteorder='little', signed=False) 255 | full_data += data 256 | ``` 257 | ## 后记 258 | 讲到现在,对app通讯部分就了解差不多了。其实该APP逆向过程中,主要就是体力活==只是单纯的分享源码,不写点儿啥的话对不起自己这么多天。所以随便写点儿东西,该源码我也没有完善,所以希望通过我的一点儿笔记给有心思研究该app的同志一点儿小小的启发。 259 | 该app在逆向的过程中,最麻烦的莫过于服务器返回数据的解析了,其他还好说,关键是主要StuffCurveStruct以及StuffTableStruct两种格式数据的处理。StuffCurveStruct应该包含的是股票的详细信息, 这部分暂未完成解析, 感觉用处不大。StuffTableStruct包含了除股票信息之类, 比如个人持仓之类的信息,这部分花费了比较大的精力去搞定。 260 | 目前大家如果还想开发新的接口的话,直接通过frida hook发送的数据康康就行,返回数据我应该已经解析的差不多了。 261 | -------------------------------------------------------------------------------- /ftj.py: -------------------------------------------------------------------------------- 1 | # 不要问这部分干嘛的 2 | # 我也看不懂, 差点儿就疯了 3 | # 这部分代码debug了两整天... 4 | 5 | 6 | def from_bytes(p, v): 7 | return int.from_bytes(bytes(p[v: v + 1]), byteorder='little') 8 | 9 | 10 | def to_bytes(p, v, index): 11 | p[index: index + 1] = bytearray(int.to_bytes(v, 1, byteorder='little', signed=False)) 12 | 13 | 14 | def ftj_b(p0_bytes, p1, p2, p3_bytes, p4): 15 | p0 = bytearray(p0_bytes) 16 | p3 = bytearray(p3_bytes) 17 | v4 = 8 18 | v5 = 0 19 | v2 = p1 + 1 20 | v1 = from_bytes(p0, p1) 21 | v0 = 1 22 | v3 = v2 + 1 23 | v2 = from_bytes(p0, v2) 24 | to_bytes(p3, v2, v5) 25 | v7 = v4 26 | v6 = v3 27 | v3 = v0 28 | v0 = v2 29 | while v3 < p4: 30 | v8 = v1 & 0x80 31 | v1 += v1 32 | v2 = v1 33 | v1 = v7 - 1 34 | if v1 != 0: 35 | v5 = v6 36 | else: 37 | v5 = v6 + 1 38 | v1 = from_bytes(p0, v6) 39 | v2 = v1 40 | v1 = v4 41 | if v8 != 0: 42 | v7 = v2 & 0x80 43 | v2 += v2 44 | v1 -= 1 45 | if v1 != 0: 46 | v6 = v2 47 | v9 = v1 48 | v1 = v5 49 | v5 = v9 50 | else: 51 | v2 = v5 + 1 52 | v1 = from_bytes(p0, v5) 53 | v5 = v4 54 | v6 = v1 55 | v1 = v2 56 | if v7 != 0: 57 | v2 = v0 58 | v0 = v1 59 | v1 = v3 60 | else: 61 | v2 = v3 + 1 62 | v0 = v1 + 1 63 | v1 = from_bytes(p0, v1) 64 | to_bytes(p3, v1, v3) 65 | v9 = v2 66 | v2 = v1 67 | v1 = v9 68 | v7 = v1 + 1 69 | to_bytes(p3, v2, v1) 70 | v8 = v6 & 0x80 71 | v1 = v6 + v6 72 | v3 = v1 73 | v1 = v5 - 1 74 | if v1 != 0: 75 | v5 = v0 76 | v0 = v1 77 | v1 = v3 78 | else: 79 | v5 = v0 + 1 80 | v0 = from_bytes(p0, v0) 81 | v1 = v0 82 | v0 = v4 83 | if v8 == 0: 84 | v6 = v5 85 | v3 = v7 86 | v7 = v0 87 | v0 = v2 88 | continue 89 | v6 = v7 + 1 90 | to_bytes(p3, v2, v7) 91 | v7 = v1 & 0x80 92 | v1 += v1 93 | v0 -= 1 94 | if v0 != 0: 95 | v3 = v5 96 | else: 97 | v3 = v5 + 1 98 | v0 = from_bytes(p0, v5) 99 | v1 = v0 100 | v0 = v4 101 | if v7 == 0: 102 | v5 = v1 & 0x80 103 | v1 += v1 104 | v0 -= 1 105 | if v0 == 0: 106 | v1 = v3 + 1 107 | v0 = from_bytes(p0, v3) 108 | v3 = v1 109 | v1 = v0 110 | v0 = v4 111 | if v5 == 0: 112 | v7 = v0 113 | v0 = v2 114 | v9 = v6 115 | v6 = v3 116 | v3 = v9 117 | continue 118 | v5 = v6 + 1 119 | to_bytes(p3, v2, v6) 120 | v7 = v0 121 | v6 = v3 122 | v3 = v5 123 | v0 = v2 124 | continue 125 | v7 = v6 + 1 126 | to_bytes(p3, v2, v6) 127 | v5 = v7 + 1 128 | to_bytes(p3, v2, v7) 129 | v6 = v1 & 0x80 130 | v1 += v1 131 | v0 -= 1 132 | if v0 == 0: 133 | v1 = v3 + 1 134 | v0 = from_bytes(p0, v3) 135 | v3 = v1 136 | v1 = v0 137 | v0 = v4 138 | if v6 == 0: 139 | v7 = v0 140 | v6 = v3 141 | v3 = v5 142 | v0 = v2 143 | continue 144 | v6 = v5 + 1 145 | to_bytes(p3, v2, v5) 146 | v7 = v1 & 0x80 147 | v1 += v1 148 | v0 -= 1 149 | if v0 != 0: 150 | v5 = v3 151 | else: 152 | v5 = v3 + 1 153 | v0 = from_bytes(p0, v3) 154 | v1 = v0 155 | v0 = v4 156 | if v7 == 0: 157 | v7 = v1 & 0x80 158 | v1 += v1 159 | v0 -= 1 160 | if v0 != 0: 161 | v3 = v5 162 | else: 163 | v3 = v5 + 1 164 | v0 = from_bytes(p0, v5) 165 | v1 = v0 166 | v0 = v4 167 | if v7 == 0: 168 | v5 = v1 & 0x80 169 | v1 += v1 170 | v0 -= 1 171 | if v0 == 0: 172 | v1 = v3 + 1 173 | v0 = from_bytes(p0, v3) 174 | v3 = v1 175 | v1 = v0 176 | v0 = v4 177 | if v5 == 0: 178 | v7 = v0 179 | v0 = v2 180 | v9 = v6 181 | v6 = v3 182 | v3 = v9 183 | continue 184 | v5 = v6 + 1 185 | to_bytes(p3, v2, v6) 186 | v7 = v0 187 | v6 = v3 188 | v3 = v5 189 | v0 = v2 190 | continue 191 | v5 = v1 & 0x80 192 | v1 += v1 193 | v0 -= 1 194 | if v0 == 0: 195 | v1 = v3 + 1 196 | v0 = from_bytes(p0, v3) 197 | v3 = v1 198 | v1 = v0 199 | v0 = v4 200 | if v5 == 0: 201 | v7 = v6 + 1 202 | to_bytes(p3, v2, v6) 203 | v5 = v7 + 1 204 | to_bytes(p3, v2, v7) 205 | v7 = v0 206 | v6 = v3 207 | v3 = v5 208 | v0 = v2 209 | continue 210 | v5 = v6 + 1 211 | to_bytes(p3, v2, v6) 212 | v6 = v5 + 1 213 | to_bytes(p3, v2, v5) 214 | v5 = v6 + 1 215 | to_bytes(p3, v2, v6) 216 | v7 = v0 217 | v6 = v3 218 | v3 = v5 219 | v0 = v2 220 | continue 221 | v3 = v6 + 1 222 | to_bytes(p3, v2, v6) 223 | v6 = v3 + 1 224 | to_bytes(p3, v2, v3) 225 | v3 = v6 + 1 226 | to_bytes(p3, v2, v6) 227 | v6 = v3 + 1 228 | to_bytes(p3, v2, v3) 229 | v7 = v1 & 0x80 230 | v1 += v1 231 | v0 -= 1 232 | if v0 != 0: 233 | v3 = v5 234 | else: 235 | v3 = v5 + 1 236 | v0 = from_bytes(p0, v5) 237 | v1 = v0 238 | v0 = v4 239 | if v7 == 0: 240 | v5 = v1 & 0x80 241 | v1 += v1 242 | v0 -= 1 243 | if v0 == 0: 244 | v1 = v3 + 1 245 | v0 = from_bytes(p0, v3) 246 | v3 = v1 247 | v1 = v0 248 | v0 = v4 249 | if v5 == 0: 250 | v7 = v0 251 | v0 = v2 252 | v9 = v6 253 | v6 = v3 254 | v3 = v9 255 | continue 256 | v5 = v6 + 1 257 | to_bytes(p3, v2, v6) 258 | v7 = v0 259 | v6 = v3 260 | v3 = v5 261 | v0 = v2 262 | continue 263 | v5 = v1 & 0x80 264 | v1 += v1 265 | v0 -= 1 266 | if v0 != 0: 267 | v7 = v0 268 | v8 = v1 269 | v0 = v3 270 | else: 271 | v1 = v3 + 1 272 | v0 = from_bytes(p0, v3) 273 | v7 = v4 274 | v8 = v0 275 | v0 = v1 276 | if v5 == 0: 277 | v1 = v6 + 1 278 | to_bytes(p3, v2, v6) 279 | v3 = v1 + 1 280 | to_bytes(p3, v2, v1) 281 | v1 = v8 282 | v6 = v0 283 | v0 = v2 284 | continue 285 | v1 = v6 + 1 286 | to_bytes(p3, v2, v6) 287 | v3 = v1 + 1 288 | to_bytes(p3, v2, v1) 289 | v1 = v3 + 1 290 | to_bytes(p3, v2, v3) 291 | v5 = v0 + 1 292 | v0 = from_bytes(p0, v0) 293 | if v0 >= 0: 294 | v3 = v0 295 | else: 296 | v3 = 0x100 297 | v0 += v3 298 | v3 = v0 299 | v6 = 0x7f 300 | if v3 <= v6: 301 | v6 = v3 302 | v3 = v0 303 | v0 = v5 304 | else: 305 | v0 = v3 - 0x80 306 | v0 = v0 << 8 307 | v3 = v5 + 1 308 | v5 = from_bytes(p0, v5) 309 | if v5 < 0: 310 | v0 += 0x100 311 | v0 += v5 312 | v6 = v0 313 | v9 = v3 314 | v3 = v0 315 | v0 = v9 316 | while True: 317 | v5 = v3 - 1 318 | if v3 == 0: 319 | v3 = 0x7fff 320 | if v6 == v3: 321 | v5 = v0 + 1 322 | v0 = from_bytes(p0, v0) 323 | if v0 >= 0: 324 | v3 = v0 325 | else: 326 | v3 = 0x100 327 | v0 += v3 328 | v3 = v0 329 | v6 = 0x7f 330 | if v3 <= v6: 331 | v6 = v3 332 | v3 = v0 333 | v0 = v5 334 | continue 335 | v0 = v3 - 0x80 336 | v0 = v0 << 8 337 | v3 = v5 + 1 338 | v5 = from_bytes(p0, v5) 339 | if v5 < 0: 340 | v0 += 0x100 341 | v0 += v5 342 | v6 = v0 343 | v9 = v3 344 | v3 = v0 345 | v0 = v9 346 | continue 347 | v6 = v0 348 | v3 = v1 349 | v1 = v8 350 | v0 = v2 351 | continue 352 | v3 = v1 + 1 353 | to_bytes(p3, v2, v1) 354 | v1 = v3 355 | v3 = v5 356 | else: 357 | v6 = v3 + 1 358 | v7 = v5 + 1 359 | v0 = from_bytes(p0, v5) 360 | to_bytes(p3, v0, v3) 361 | v0 = v6 + 1 362 | v3 = v7 + 1 363 | v5 = from_bytes(p0, v7) 364 | to_bytes(p3, v5, v6) 365 | v7 = v1 366 | v6 = v3 367 | v1 = v2 368 | v3 = v0 369 | v0 = v5 370 | continue 371 | return p4, bytes(p0), bytes(p3) 372 | 373 | 374 | -------------------------------------------------------------------------------- /header.py: -------------------------------------------------------------------------------- 1 | from OutputStream import OutputStream 2 | 3 | 4 | class MiniDataHead: 5 | headLength = 0 6 | id = 0 7 | type = 0 8 | pageId = 0 9 | dataLength = 0 10 | frameId = 0 11 | textLength = 0 12 | sessionType = 0 13 | 14 | def __init__(self, *InputStream): 15 | if len(InputStream) <= 0: 16 | return 17 | decode_Data = OutputStream(InputStream[0]) 18 | self.headLength = decode_Data.readUnsignedShort() 19 | self.id = decode_Data.readInt() 20 | self.type = decode_Data.readInt() 21 | self.frameId = decode_Data.readUnsignedShort() 22 | self.pageId = decode_Data.readInt() 23 | self.dataLength = decode_Data.readInt() 24 | self.textLength = decode_Data.readInt() 25 | -------------------------------------------------------------------------------- /hexin.py: -------------------------------------------------------------------------------- 1 | # 同花顺安卓端交易协议 2 | import json 3 | import os 4 | import random 5 | import sys 6 | from socket import * 7 | import base64 8 | from urllib import parse 9 | 10 | import snappy 11 | 12 | import requests 13 | 14 | import Des 15 | import constants 16 | import rsa_utils 17 | import dataHelper 18 | from header import MiniDataHead 19 | from OutputStream import OutputStream 20 | from stuffTextData import stuffTextData 21 | from stuffInteractData import stuffInteractData 22 | 23 | 24 | # 处理未定义情况的数据包, 比如接收到不是发送包id的数据包时, 由该部分完成处理 25 | # 相当于同步情况下的异步吧, 想要速度的话, 这部分可以扔在线程里面, 或者收到包就先解析id, 不符合条件的扔到线程中 26 | def default_handle_data(ret_data): 27 | if constants.log == 1: 28 | print(ret_data) 29 | 30 | 31 | # 处理接收到的数据,负责完成数据解压缩/数据解密/数据解析/数据分发等工作 32 | def handle_data(data): 33 | 34 | if len(data) < 24: 35 | return 36 | 37 | decode_Data = OutputStream(data) 38 | 39 | data_header = MiniDataHead(decode_Data.readBytes(24)) 40 | 41 | if data_header.id == 8888: 42 | data_header.type |= 0x0A 43 | 44 | # 加密种类 45 | enc_type = data_header.type & 0xF0000000 46 | 47 | # 数据包被DES加密 48 | if enc_type == 0x10000000: 49 | plain_data = Des.des(decode_Data.readBytes(decode_Data.available()), globals()['server_key'], False) 50 | decode_Data.__init__(plain_data) 51 | 52 | # 数据包被RSA_短密钥加密,这种情况暂时未遇到,不能上线正式环境,如果出现,尝试解密后抛出异常以便分析 53 | if enc_type == 0x20000000: 54 | if constants.debug == 1: 55 | assert False, 'RSA!!!' 56 | 57 | # 数据包被AES加密,这种情况暂时未遇到,不能上线正式环境,如果出现,抛出异常以便分析 58 | if enc_type == 0x70000000: 59 | if constants.debug == 1: 60 | assert False, 'AES!!!' 61 | 62 | # 数据包被压缩 63 | if data_header.type & 0xF000 == 0x1000: 64 | uncompress_data = snappy.uncompress(decode_Data.readBytes(data_header.pageId)) 65 | data = uncompress_data + decode_Data.readBytes(decode_Data.available()) 66 | decode_Data.__init__(data) 67 | 68 | # 进程数据各类情况,暂时未遇到,先直接无视,以后有时间再完善 69 | if data_header.id == -3: 70 | if constants.debug == 1: 71 | if data_header.type & 15 == 11: 72 | assert False, 'data_header.id == -3' 73 | 74 | mark = data_header.type & 15 # 采用字典代替switch 75 | 76 | if constants.log == 1: 77 | print('data_mark={}'.format(mark)) 78 | 79 | ret_data = '' 80 | 81 | # 主要包含StuffCurveStruct以及StuffTableStruct两种格式数据的处理 82 | # StuffCurveStruct应该包含的是股票的详细信息, 这部分暂未完成解析, 感觉用处不大 83 | # StuffTableStruct包含了除股票信息之类, 比如个人持仓之类的信息 84 | def case_0(): 85 | nonlocal ret_data 86 | ret_data = dataHelper.handle_case_0(data_header, decode_Data) 87 | 88 | # 系统消息 89 | def case_1(): 90 | nonlocal ret_data 91 | stuffTextData_data = stuffTextData(data_header.type, decode_Data.readBytes(decode_Data.available())) 92 | ret_data = stuffTextData_data.title + ':' + stuffTextData_data.content 93 | 94 | def case_3(): 95 | nonlocal ret_data 96 | number = decode_Data.readByte() 97 | decode_Data.skipBytes(1) 98 | for _ in range(number): 99 | dataId = decode_Data.readUnsignedShort() 100 | ctrlType = decode_Data.readInt() 101 | colorIndex = decode_Data.readByte() 102 | content = decode_Data.readUTF() 103 | ret_data += 'stuffCtrlData:' + 'dataId=' + str(dataId) + ',ctrlType=' + str(ctrlType) + ',colorIndex=' + str( 104 | colorIndex) + ',content=' + content + '\n' 105 | 106 | # 券商登录成功返回 107 | def case_4(): 108 | nonlocal ret_data 109 | if data_header.type == 0x10000004: 110 | ret_data = '证券登录成功' 111 | return 112 | if data_header.type == 0x30000004: 113 | ret_data = '连接中断' 114 | return 115 | ret_data = '证券登录失败' 116 | 117 | # 同花顺连接认证以及登录返回 118 | def case_6(): 119 | nonlocal ret_data 120 | content_header = decode_Data.readByte() 121 | if content_header == 128: 122 | globals()['qs_login_header'] = decode_Data.readBytes(2) 123 | data_length = decode_Data.readShort() 124 | # 这部分数据用于二次登录的使用, 手机上的登录保持就是由于该数据. 125 | if data_length > 0: 126 | passport_dat = decode_Data.readBytes(data_length) 127 | # 写出passport.dat内容, 下一次直接加载登录就可以 128 | passport_dat = b'\x00\x00' + passport_dat[0:2] + passport_dat 129 | with open("passport.dat", mode='wb') as f: 130 | f.write(passport_dat) 131 | # 以下数据是服务器返回的帐号登录信息 132 | data_length = decode_Data.readShort() 133 | if data_length > 0: 134 | data_bytes = decode_Data.readBytes(data_length) 135 | data_str = str(data_bytes, encoding='gbk') 136 | # 判断一下手机号是否已经登录 137 | if data_str[-4:] != 'sid=': 138 | nonlocal ret_data 139 | ret_data = '登录成功' 140 | return 141 | 142 | ret_data = decode_Data.readUTF() 143 | if ret_data == "": 144 | ret_data = '需要手机号登录' 145 | 146 | 147 | # 这部分数据没必要解析, 主要包含sessid以及服务器地址和端口等 148 | ''' 149 | decode_Data.skipBytes(5) 150 | data_length = decode_Data.readShort() 151 | if data_length > 0: 152 | data_bytes = decode_Data.readBytes(data_length) 153 | data_str = str(data_bytes, encoding='gbk') 154 | print('server data: ' + data_str) 155 | 156 | decode_Data.skipBytes(2) 157 | data_length = decode_Data.readShort() 158 | if data_length > 0: 159 | data_bytes = decode_Data.readBytes(data_length) 160 | data_str = str(data_bytes, encoding='gbk') 161 | print('server data: ' + data_str) 162 | 163 | data_length = decode_Data.readShort() 164 | if data_length > 0: 165 | data_bytes = decode_Data.readBytes(data_length) 166 | data_str = str(data_bytes, encoding='gbk') 167 | print('server data: ' + data_str) 168 | ''' 169 | 170 | def case_7(): 171 | nonlocal ret_data 172 | server_data = decode_Data.readBytes(decode_Data.available()) 173 | ret_data = str(server_data, encoding='gbk') 174 | if constants.debug == 1: 175 | assert False, 'case_7' 176 | 177 | def case_9(): 178 | nonlocal ret_data 179 | server_data = decode_Data.readBytes(decode_Data.available()) 180 | ret_data = str(server_data, encoding='gbk') 181 | 182 | # 同花顺或证券登录成功后返回的个人数据或自选 183 | def case_11(): 184 | nonlocal ret_data 185 | server_data = decode_Data.readBytes(data_header.pageId) 186 | ret_data = str(server_data, encoding='gbk') 187 | 188 | def case_13(): 189 | nonlocal ret_data 190 | stuffInteractData_data = stuffInteractData(decode_Data.readBytes(decode_Data.available())) 191 | ret_data = stuffInteractData_data.title + ':' + stuffInteractData_data.content 192 | 193 | def case_15(): 194 | nonlocal ret_data 195 | stuffTextData_data = stuffTextData(data_header.type, decode_Data.readBytes(decode_Data.available())) 196 | ret_data = stuffTextData_data.title + ':' + stuffTextData_data.content 197 | 198 | def default(): 199 | nonlocal ret_data 200 | ret_data = "出错" 201 | if constants.log == 1: 202 | print('No such case:' + str(mark)) 203 | 204 | # python没有switch, 通过这样的方式曲线救国 205 | switch = {0: case_0, 206 | 1: case_1, 207 | 3: case_3, 208 | 4: case_4, 209 | 6: case_6, 210 | 7: case_7, 211 | 9: case_9, 212 | 11: case_11, 213 | 13: case_13, 214 | 14: case_15, 215 | 15: case_15, 216 | } 217 | switch.get(mark, default)() 218 | return ret_data, data_header.id 219 | 220 | 221 | # 接收数据工厂,负责完成数据接收/粘包处理工作 222 | # 由于缓存为16*1024,因此暂时不考虑复杂粘包情况,出现的时候再修改吧 223 | def recv_factory(id, *datas): 224 | for _ in range(30): 225 | if constants.debug != 1: 226 | recv_data = tcp_client_socket.recv(constants.BUFFER_CACHE) 227 | else: 228 | recv_data = datas[0] 229 | i = 31 230 | if constants.log == 1: 231 | print('recv:' + recv_data.hex(), globals()['server_key'].hex()) 232 | 233 | if len(recv_data) <= 12: 234 | return 235 | 236 | index = recv_data.find(constants.DATA_MARK) 237 | while index > -1: 238 | index += 8 # +8是为了跳过data_mark 239 | length = int(recv_data[index:index + 4], 16) # 加4为获取一个四字节的int 240 | if length <= 32: 241 | break 242 | index += 5 # 索引跳过一个int和一个00位 243 | if index + length > len(recv_data): 244 | break 245 | data = recv_data[index:index + length] 246 | ret_data, ret_id = handle_data(data) 247 | if ret_id == id: 248 | return ret_data 249 | default_handle_data(ret_data) 250 | recv_data = recv_data[index + length:] 251 | index = recv_data.find(constants.DATA_MARK) 252 | return "" 253 | 254 | 255 | # 获取包ID,全局唯一,不可重复,否则掉线,采用自增算法 256 | def get_pack_id(): 257 | global pack_id 258 | res = pack_id 259 | pack_id = pack_id + 1 260 | return res 261 | 262 | 263 | # 数据包添加一些标志的标头 264 | def add_header(data, data_header, is_encrypt): 265 | if is_encrypt: 266 | data = int.to_bytes(data_header.id, 2, byteorder='little', signed=False) + globals()[ 267 | 'qs_login_header'] + b'\x00\x00' + data 268 | # 长度需要为8的倍数 269 | data = data + b"\x00\x00\x00\x00\x00\x00\x00" 270 | data = data[0:int(len(data) / 8) * 8] 271 | data = Des.des(data, server_key, True) 272 | 273 | data_length = len(data) - data_header.textLength 274 | data_header.dataLength = data_length 275 | if data_header.type == 0: 276 | data_header.type = 0x11010000 277 | if data_header.dataLength == 0: 278 | data_header.dataLength = len(data) 279 | else: 280 | if data_header.type == 0: 281 | data_header.type = 0x1010000 282 | 283 | if data_header.pageId == 0: 284 | data_header.pageId = 0xFF00 285 | 286 | full_data = b'' 287 | full_data += int.to_bytes(data_header.headLength, 2, byteorder='little', signed=False) 288 | full_data += int.to_bytes(data_header.id, 4, byteorder='little', signed=False) 289 | full_data += int.to_bytes(data_header.type, 4, byteorder='little', signed=False) 290 | full_data += int.to_bytes(data_header.pageId, 2, byteorder='little', signed=False) 291 | full_data += int.to_bytes(data_header.dataLength, 4, byteorder='little', signed=False) 292 | full_data += int.to_bytes(data_header.frameId, 4, byteorder='little', signed=False) 293 | full_data += int.to_bytes(data_header.textLength, 4, byteorder='little', signed=False) 294 | full_data += int.to_bytes(data_header.sessionType, 4, byteorder='little', signed=False) 295 | full_data += data 296 | 297 | data_length = int.to_bytes(len(full_data), 4, byteorder='big').hex().encode() 298 | data = b'\xfd\xfd\xfd\xfd' + data_length + b'\x00' + full_data 299 | return data 300 | 301 | 302 | # 获取券商登录数据包 303 | # 帐号、密码、通信密码(可空)、其他三个券商相关参数 304 | def get_qs_login_data(account, password, txmm, qsid, wtid, dtkltype): 305 | qssj = wtid + "#" + qsid + "#" + dtkltype + "#1#" 306 | reqpage = str(random.randint(10000, 99999)) 307 | 308 | data = b'\x13\x02\x00\x01\x00\x30\x01\x01\x00\x30' 309 | 310 | data += int.to_bytes(2, 1, byteorder='little', signed=False) 311 | data += int.to_bytes(len(account), 2, byteorder='little', signed=False) 312 | data += str.encode(account) 313 | 314 | data += int.to_bytes(3, 1, byteorder='little', signed=False) 315 | data += int.to_bytes(len(password), 2, byteorder='little', signed=False) 316 | data += str.encode(password) 317 | 318 | data += int.to_bytes(4, 1, byteorder='little', signed=False) 319 | data += int.to_bytes(len(txmm), 2, byteorder='little', signed=False) 320 | data += str.encode(txmm) 321 | 322 | data += int.to_bytes(5, 1, byteorder='little', signed=False) 323 | data += int.to_bytes(0, 2, byteorder='little', signed=False) 324 | 325 | data += int.to_bytes(6, 1, byteorder='little', signed=False) 326 | data += int.to_bytes(len(qssj), 2, byteorder='little', signed=False) 327 | data += str.encode(qssj) 328 | 329 | data += int.to_bytes(7, 1, byteorder='little', signed=False) 330 | data += int.to_bytes(len(reqpage), 2, byteorder='little', signed=False) 331 | data += str.encode(reqpage) 332 | 333 | data += int.to_bytes(8, 1, byteorder='little', signed=False) 334 | data += int.to_bytes(1, 2, byteorder='little', signed=False) 335 | data += int.to_bytes(49, 1, byteorder='little', signed=False) 336 | 337 | data += int.to_bytes(9, 1, byteorder='little', signed=False) 338 | HD_INFO = 'HDInfo=' + constants.HD_INFO 339 | data += int.to_bytes(len(HD_INFO), 2, byteorder='little', signed=False) 340 | data += str.encode(HD_INFO) 341 | 342 | # 这部分数据直接固定 343 | data += b'\x0a\x00\x00\x0b\x00\x00\x0c\x00\x00\x0d\x01\x00\x30\x0e\x00\x00\x0f\x00\x00\x10\x00\x00\x11\x00\x00' \ 344 | b'\x12\x00\x00' 345 | 346 | # 主要数据构造完毕,接下来构造包头 347 | pack_id_temp = get_pack_id() 348 | 349 | data_header = b'' 350 | data_header += int.to_bytes(pack_id_temp, 2, byteorder='little', signed=False) 351 | data_header += globals()['qs_login_header'] 352 | data_header += int.to_bytes(70024 & 65535, 2, byteorder='little', signed=False) 353 | 354 | # data长度不够8的倍数则用00补齐 355 | data = data_header + data + b"\x00\x00\x00\x00\x00\x00\x00" 356 | 357 | data = data[0:int(len(data) / 8) * 8] 358 | 359 | data_length = len(data) 360 | enc_data = Des.des(data, globals()['server_key'], True) 361 | enc_key = constants.RSA_KEY_HEADER 362 | enc_key += globals()['qs_login_header'] 363 | enc_key += globals()['server_key'] 364 | enc_key = rsa_utils.rsa_encrypt_key(enc_key) 365 | enc_key_length = len(enc_key) 366 | 367 | data_header = MiniDataHead() 368 | data_header.headLength = 28 369 | data_header.id = pack_id_temp 370 | data_header.type = 0x11188 | 0x50000000 371 | data_header.dataLength = data_length # 加密前以及添加包头前数据的长度 372 | data_header.frameId = 0xA2A 373 | data_header.textLength = enc_key_length 374 | 375 | data = b'' 376 | data += enc_key 377 | data += enc_data 378 | 379 | data = add_header(data, data_header, False) 380 | return data, data_header.id 381 | 382 | 383 | # 获取同花顺连接数据包 384 | def get_hexin_connect_data(passport_dat): 385 | if passport_dat != b'': 386 | data_header = MiniDataHead() 387 | data_header.headLength = 28 388 | data_header.id = get_pack_id() 389 | data_header.type = 0x70000 390 | data_header.dataLength = len(passport_dat) 391 | data = add_header(passport_dat, data_header, False) 392 | return data, data_header.id 393 | 394 | str_info = 'ScreenWidth=720' 395 | str_info += '\r\nScreenHeight=1280' 396 | str_info += '\r\nsmallestWidth=0dp' 397 | str_info += '\r\ndensity=1.0' 398 | str_info += '\r\nrealdata=true' 399 | str_info += '\r\ntime2012=1' 400 | str_info += '\r\nAppletVersion=' + constants.APPLET_VERSION 401 | str_info += '\r\nsvnver=' + constants.SVN_VER 402 | str_info += '\r\nTestVersion=' + constants.TEST_VERSION 403 | str_info += '\r\nBranchName=' + constants.BRANCH_NAME 404 | str_info += '\r\nFunClientSupport=0111111111100011111111' 405 | str_info += '\r\napp=android' 406 | str_info += '\r\nfor=ths_am_gphone_login' 407 | str_info += '\r\nprogid=500' 408 | str_info += '\r\nnet=1' 409 | str_info += '\r\nqsid=800' 410 | str_info += '\r\nsourceid=' + constants.SOURCE_ID 411 | str_info += '\r\nspcode=' + constants.SP_CODE 412 | str_info += '\r\nchannelid=' + constants.SOURCE_ID 413 | str_info += '\r\ntype=' + constants.TYPE 414 | str_info += '\r\nudid=' + constants.UDID 415 | str_info += '\r\nimei=' + constants.IMEI 416 | str_info += '\r\nsim=' + constants.UDID 417 | str_info += '\r\nimsi=' + constants.IMSI 418 | str_info += '\r\nmacA=' + constants.MAC 419 | str_info += '\r\nsdk=22' 420 | str_info += '\r\nsdkn=5.1.1' 421 | str_info += '\r\nCA=4' 422 | str_info += '\r\ndev=' + constants.DEV 423 | str_info += '\r\n' 424 | 425 | data = b'' 426 | for i in range(len(str_info)): 427 | data += int.to_bytes(ord(str_info[i]), 2, byteorder='little', signed=False) 428 | 429 | data = int.to_bytes(len(str_info), 2, byteorder='little', signed=False) + data 430 | 431 | # data长度不够8的倍数则用00补齐 432 | data += b"\x00\x00\x00\x00\x00\x00\x00" 433 | data = data[0:int(len(data) / 8) * 8] 434 | 435 | data_header = MiniDataHead() 436 | data_header.headLength = 28 437 | data_header.id = get_pack_id() 438 | data_header.type = 0x70000 439 | data_header.dataLength = len(data) 440 | data = add_header(data, data_header, False) 441 | 442 | return data, data_header.id 443 | 444 | 445 | # 获取同花顺登录手机验证码数据包 446 | def get_hexin_login_sms_data(account): 447 | reqpage = random.randint(10000, 99999).__str__() 448 | enc_account = base64.b64encode(rsa_utils.rsa_encrypt(account.encode('utf-8'))) 449 | enc_account = parse.quote(enc_account) 450 | url = 'verify?reqtype=wlh_thsreg_modify&mobile_login=1&qsid=800®flag&udid=' + constants.UDID + '&encoding=GBK&mobile=' + enc_account + '&rsa_version=default_4&foreign=1&foreign_country=86' 451 | str_info = '[frame]' 452 | str_info += '\r\nid=4222' 453 | str_info += '\r\npageList=' + reqpage 454 | str_info += '\r\nreqPage=' + reqpage 455 | str_info += '\r\nreqPageCount=1' 456 | str_info += '\r\n[' + reqpage + ']' 457 | str_info += '\r\nid=1101' 458 | str_info += '\r\nhost=auth' 459 | str_info += '\r\nurl=' + url 460 | str_info += '\r\n' 461 | 462 | data_header = MiniDataHead() 463 | data_header.headLength = 28 464 | data_header.id = get_pack_id() 465 | data_header.frameId = 0x107E 466 | data_header.textLength = len(str_info) 467 | data = add_header(str_info.encode(), data_header, False) 468 | return data, data_header.id 469 | 470 | 471 | # 获取同花顺登录数据包, 当isSMS为True时, 意味着为验证码登录, password填写验证码就行 472 | def get_hexin_login_data(account, password, isSMS): 473 | reqpage = random.randint(10000, 99999).__str__() 474 | 475 | enc_account = base64.b64encode(rsa_utils.rsa_encrypt(account.encode('utf-8'))) 476 | enc_account = str(enc_account, 'utf-8') 477 | enc_password = base64.b64encode(rsa_utils.rsa_encrypt(password.encode('utf-8'))) 478 | enc_password = str(enc_password, 'utf-8') 479 | 480 | str_info = '[frame]' 481 | str_info += '\r\nid=2054' 482 | str_info += '\r\npageList=' + reqpage 483 | str_info += '\r\nreqPage=' + reqpage 484 | str_info += '\r\nreqPageCount=1' 485 | str_info += '\r\n[' + reqpage + ']' 486 | str_info += '\r\nid=1001' 487 | str_info += '\r\ncrypt=2' 488 | str_info += '\r\nctrlcount=2' 489 | str_info += '\r\nctrlid_0=34338' 490 | str_info += '\r\nctrlvalue_0=' + enc_account 491 | str_info += '\r\nctrlid_1=34339' 492 | str_info += '\r\nctrlvalue_1=' + enc_password 493 | str_info += '\r\nreqctrl=4304' 494 | str_info += '\r\nloginmode=1' 495 | if not isSMS: 496 | str_info += '\r\nloginType=3\r\n' 497 | else: 498 | str_info += '\r\nforeign=1' 499 | str_info += '\r\nforeign_country=86' 500 | str_info += '\r\nloginType=7\r\n' 501 | 502 | data_header = MiniDataHead() 503 | data_header.headLength = 28 504 | data_header.id = get_pack_id() 505 | data_header.frameId = 0x806 506 | data_header.textLength = len(str_info) 507 | data = add_header(str_info.encode(), data_header, False) 508 | 509 | return data, data_header.id 510 | 511 | 512 | # 获取可转债/股票行情数据包 513 | def get_prepurchase_data(code, price, quantity): 514 | reqpage = random.randint(10000, 99999).__str__() 515 | str_info = '[frame]' 516 | str_info += '\r\nid=2682' 517 | str_info += '\r\npageList=' + reqpage 518 | str_info += '\r\nreqPage=' + reqpage 519 | str_info += '\r\nreqPageCount=1' 520 | str_info += '\r\nqsid=' + globals()['qsid'] 521 | str_info += '\r\nwtaccount=' + globals()['wtaccount'] 522 | str_info += '\r\nwttype=' + globals()['dtkltype'] 523 | str_info += '\r\n[' + reqpage + ']' 524 | str_info += '\r\nid=1804' 525 | str_info += '\nctrlid_0=2102' 526 | str_info += '\nctrlvalue_0=' + code 527 | str_info += '\nctrlid_1=2127' 528 | str_info += '\nctrlvalue_1=' + price 529 | str_info += '\nreqctrl=4507' 530 | str_info += '\nctrlid_2=36615' 531 | str_info += '\nctrlvalue_2=' + quantity 532 | str_info += '\nctrlcount=3' 533 | str_info += '\r\nHDInfo=' + constants.HD_INFO 534 | data_header = MiniDataHead() 535 | data_header.headLength = 28 536 | data_header.id = get_pack_id() 537 | data_header.frameId = 0xA7A 538 | data_header.textLength = len(str_info) 539 | data = add_header(str_info.encode(), data_header, True) 540 | return data, data_header.id 541 | 542 | 543 | # 获取可转债/股票行情数据包 544 | def get_price_data_4491(code): 545 | reqpage = random.randint(10000, 99999).__str__() 546 | str_info = '[frame]' 547 | str_info += '\r\nid=2682' 548 | str_info += '\r\npageList=' + reqpage 549 | str_info += '\r\nreqPage=' + reqpage 550 | str_info += '\r\nreqPageCount=1' 551 | str_info += '\r\nqsid=' + globals()['qsid'] 552 | str_info += '\r\nwtaccount=' + globals()['wtaccount'] 553 | str_info += '\r\nwttype=' + globals()['dtkltype'] 554 | str_info += '\r\n[' + reqpage + ']' 555 | str_info += '\r\nid=1804' 556 | str_info += "\r\nreqtype=262144" 557 | str_info += '\nctrlid_0=2102' 558 | str_info += '\nctrlvalue_0=' + code 559 | str_info += '\nctrlid_1=2218' 560 | str_info += '\nctrlvalue_1=1' 561 | str_info += '\nctrlid_2=2219' 562 | str_info += '\nctrlvalue_2=1' 563 | str_info += '\nreqctrl=4491' 564 | str_info += '\nctrlcount=3' 565 | str_info += '\r\nHDInfo=' + constants.HD_INFO 566 | data_header = MiniDataHead() 567 | data_header.headLength = 28 568 | data_header.id = get_pack_id() 569 | data_header.frameId = 0xA7A 570 | data_header.textLength = len(str_info) 571 | data = add_header(str_info.encode(), data_header, True) 572 | return data, data_header.id 573 | 574 | 575 | # 获取可转债/股票行情数据包 576 | def get_price_data_4492(code, price): 577 | reqpage = random.randint(10000, 99999).__str__() 578 | str_info = '[frame]' 579 | str_info += '\r\nid=2682' 580 | str_info += '\r\npageList=' + reqpage 581 | str_info += '\r\nreqPage=' + reqpage 582 | str_info += '\r\nreqPageCount=1' 583 | str_info += '\r\nqsid=' + globals()['qsid'] 584 | str_info += '\r\nwtaccount=' + globals()['wtaccount'] 585 | str_info += '\r\nwttype=' + globals()['dtkltype'] 586 | str_info += '\r\n[' + reqpage + ']' 587 | str_info += '\r\nid=1804' 588 | str_info += "\r\nreqtype=262144" 589 | str_info += '\nreqctrl=4492' 590 | str_info += '\nctrlid_0=2127' 591 | str_info += '\nctrlvalue_0=' + price 592 | str_info += '\nctrlid_1=2102' 593 | str_info += '\nctrlvalue_1=' + code 594 | str_info += '\nctrlcount=2' 595 | str_info += '\r\nHDInfo=' + constants.HD_INFO 596 | data_header = MiniDataHead() 597 | data_header.headLength = 28 598 | data_header.id = get_pack_id() 599 | data_header.frameId = 0xA7A 600 | data_header.textLength = len(str_info) 601 | data = add_header(str_info.encode(), data_header, True) 602 | return data, data_header.id 603 | 604 | 605 | # 获取申购可转债数据包 606 | def get_purchase_data(code, price, quantity): 607 | reqpage = random.randint(10000, 99999).__str__() 608 | str_info = '[frame]' 609 | str_info += '\r\nid=2682' 610 | str_info += '\r\npageList=' + reqpage 611 | str_info += '\r\nreqPage=' + reqpage 612 | str_info += '\r\nreqPageCount=1' 613 | str_info += '\r\nqsid=' + globals()['qsid'] 614 | str_info += '\r\nwtaccount=' + globals()['wtaccount'] 615 | str_info += '\r\nwttype=' + globals()['dtkltype'] 616 | str_info += '\r\n[' + reqpage + ']' 617 | str_info += '\r\nid=1820' 618 | str_info += '\r\nreqctrl=2001' 619 | str_info += '\nctrlid_0=36641' 620 | str_info += '\nctrlvalue_0=1' 621 | str_info += '\nctrlid_1=36615' 622 | str_info += '\nctrlvalue_1=' + quantity 623 | str_info += '\nctrlid_2=2102' 624 | str_info += '\nctrlvalue_2=' + code 625 | str_info += '\nctrlid_3=2127' 626 | str_info += '\nctrlvalue_3=' + price 627 | str_info += '\nctrlcount=4' 628 | str_info += '\r\nHDInfo=' + constants.HD_INFO 629 | data_header = MiniDataHead() 630 | data_header.headLength = 28 631 | data_header.id = get_pack_id() 632 | data_header.frameId = 0xA7A 633 | data_header.textLength = len(str_info) 634 | data = add_header(str_info.encode(), data_header, True) 635 | return data, data_header.id 636 | 637 | 638 | # 获取新债数据,返回数据为申购代码列表 639 | def get_cate_info(): 640 | session = requests.session() 641 | res = session.get(constants.CATE_URL, verify=False, allow_redirects=False) 642 | task = [] 643 | json_infos = json.loads(res.text) 644 | for info in json_infos: 645 | if info['sgDate'] == info['today']: 646 | task.append(info['sgCode']) 647 | return task 648 | 649 | 650 | # 调用指定的获取数据包函数, 完成相应的数据包的发送和接收 651 | def sendAndRecv(data_func, *args): 652 | data, id = data_func(*args) 653 | if constants.log == 1: 654 | print(data_func.__name__ + ':' + data.hex()) 655 | tcp_client_socket.send(data) 656 | ret_data = recv_factory(id=id) 657 | return ret_data 658 | 659 | 660 | # 创建一个tcp连接 661 | tcp_client_socket = socket(AF_INET, SOCK_STREAM) 662 | try: 663 | if constants.debug != 1: 664 | tcp_client_socket.connect((constants.CONNECT_HOST, constants.CONNECT_PORT)) 665 | print("connect success!") 666 | except Exception as err: 667 | print(err) 668 | sys.exit(0) 669 | 670 | # 全局数据包ID,该ID不可重复 671 | pack_id = 0 672 | 673 | # 后续生成数据需要用到 674 | qs_login_header = b'' 675 | 676 | # 会话密钥,随机生成,可固定 677 | server_key = b'' 678 | for _ in range(16): 679 | server_key += int.to_bytes(random.randint(0, 255), 1, byteorder='little', signed=False) 680 | 681 | # 券商账户 682 | wtaccount = '' 683 | # 券商密码 684 | wtpassword = '' 685 | # 券商参数 686 | qsid = '' 687 | wtid = '' 688 | dtkltype = '' 689 | 690 | # 测试的时候, 部分参数需要固定 691 | if constants.debug == 1: 692 | pack_id = 43 693 | qs_login_header = b'' 694 | server_key = bytes.fromhex(''.replace(" ", "")) 695 | 696 | if constants.log == 1: 697 | print("server_key:", server_key.hex()) 698 | 699 | # 获取可转债信息 700 | cate_info = get_cate_info() 701 | 702 | if constants.debug != 1 and len(cate_info) > 0: 703 | 704 | passport_dat = b'' 705 | # 尝试读取passport.dat文件 706 | if os.path.exists('passport.dat'): 707 | with open("passport.dat", mode='rb') as f: 708 | passport_dat = f.read() 709 | 710 | # 获取连接数据 711 | ret_data = sendAndRecv(get_hexin_connect_data, passport_dat) 712 | 713 | # 第一次需要手机号登录一下, 后续不再需要 714 | if ret_data == '需要手机号登录': 715 | while True: 716 | account = input('输入同花顺手机号:') 717 | ret_data = sendAndRecv(get_hexin_login_sms_data, account) 718 | if ret_data.find('ret code="0"') == -1: 719 | print('短信验证码发送失败') 720 | print(ret_data) 721 | break 722 | 723 | while True: 724 | data = input('输入验证码:') 725 | ret_data = sendAndRecv(get_hexin_login_data, account, data, True) 726 | if ret_data != '登录成功': 727 | print('验证码校验失败') 728 | print(ret_data) 729 | break 730 | 731 | elif ret_data != '登录成功': 732 | print('未知错误') 733 | print(ret_data) 734 | sys.exit(0) 735 | 736 | # 同花顺登录成功后尝试登录证券 737 | ret_data = sendAndRecv(get_qs_login_data, wtaccount, wtpassword, '', qsid, wtid, dtkltype) 738 | if ret_data != '证券登录成功': 739 | print('证券登录失败') 740 | print(ret_data) 741 | sys.exit(0) 742 | 743 | # 证券登录成功后尝试申购可转债 744 | for code in cate_info: 745 | price = '100.000' 746 | quantity = '10000' 747 | 748 | # 获取可转债信息 749 | ret_data = sendAndRecv(get_price_data_4491, code) 750 | if ret_data.find(code) == -1: 751 | print('申购失败-->' + ret_data) 752 | continue 753 | 754 | # 类似于生成订单, 需要确认提交 755 | ret_data = sendAndRecv(get_prepurchase_data, code, price, quantity) 756 | if ret_data.find('您是否确认以上委托?') == -1: 757 | print('申购失败-->' + ret_data) 758 | continue 759 | print(ret_data) 760 | 761 | # 提交申购请求 762 | ret_data = sendAndRecv(get_purchase_data, code, price, quantity) 763 | if ret_data.find('委托已提交') == -1: 764 | print('申购失败-->' + ret_data) 765 | continue 766 | print(ret_data) 767 | else: 768 | # 这部分主要放测试组包/解包时候的代码 769 | pass 770 | -------------------------------------------------------------------------------- /hookths.py: -------------------------------------------------------------------------------- 1 | import frida 2 | 3 | import frida, sys 4 | 5 | rdev = frida.get_remote_device() 6 | session = rdev.attach("com.hexin.plat.android") 7 | 8 | f = open('D:\\workplace\\frida\\log.txt',mode='wb') 9 | f.write(b'') 10 | f.close() 11 | 12 | f = open('D:\\workplace\\frida\\log.txt',mode='a',encoding='utf-8') 13 | 14 | script = session.create_script(""" 15 | 16 | console.log("[*] Starting script"); 17 | 18 | Java.perform(function(){ 19 | 20 | //bytes2hex 21 | function bytes2hex(bytes) { 22 | for (var hex = [], i = 0; i < bytes.length; i++) { hex.push(((bytes[i] >>> 4) & 0xF).toString(16).toUpperCase()); 23 | hex.push((bytes[i] & 0xF).toString(16).toUpperCase()); 24 | hex.push(" "); 25 | } 26 | return hex.join(""); 27 | } 28 | 29 | 30 | //log 31 | var frr = Java.use('frr'); 32 | frr.a.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 33 | send(arg1 + " : " + arg2); 34 | }; 35 | frr.b.implementation = function (arg1, arg2) { 36 | send(arg1 + " bbb: " + arg2); 37 | }; 38 | frr.c.implementation = function (arg1, arg2) { 39 | send(arg1 + " : " + arg2); 40 | }; 41 | frr.d.implementation = function (arg1, arg2) { 42 | send(arg1 + " : " + arg2); 43 | }; 44 | frr.e.implementation = function (arg1, arg2) { 45 | send(arg1 + " : " + arg2); 46 | }; 47 | 48 | var frq = Java.use('frq'); 49 | frq.a.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 50 | this.a(arg1, arg2); 51 | send(arg1 + " : " + arg2); 52 | }; 53 | frq.a.overload('java.lang.String', 'java.lang.String', 'boolean').implementation = function (arg1, arg2, arg3) { 54 | this.a(arg1, arg2, arg3); 55 | send(arg1 + " : " + arg2); 56 | }; 57 | frq.b.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 58 | this.b(arg1, arg2); 59 | send(arg1 + " : " + arg2); 60 | }; 61 | frq.c.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 62 | this.c(arg1, arg2); 63 | send(arg1 + " : " + arg2); 64 | }; 65 | frq.d.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 66 | this.d(arg1, arg2); 67 | send(arg1 + " : " + arg2); 68 | }; 69 | frq.e.overload('java.lang.String', 'java.lang.String').implementation = function (arg1, arg2) { 70 | this.e(arg1, arg2); 71 | send(arg1 + " : " + arg2); 72 | }; 73 | 74 | //uncompress 75 | var snapcompress = Java.use('org.xerial.snappy.Snappy'); 76 | snapcompress.uncompress.overload('[B', 'int', 'int', '[B', 'int').implementation = function(arg1, arg2, arg3, arg4, arg5){ 77 | send('compress:' + bytes2hex(arg1)); 78 | var ret = this.uncompress.overload('[B', 'int', 'int', '[B', 'int').apply(this, arguments); 79 | send('uncompress:' + bytes2hex(arg4)); 80 | return ret; 81 | } 82 | 83 | //nativeDES 84 | var SecurityModule = Java.use('com.hexin.android.security.SecurityModule'); 85 | SecurityModule.decrypt3DES.overload('[B', '[B').implementation = function(arg1, arg2){ 86 | var ret = this.decrypt3DES(arg1, arg2); 87 | send('nativeDES_dkey:' + bytes2hex(arg1)); 88 | send('nativeDES_dcipher:' + bytes2hex(arg2)); 89 | send('nativeDES_dplain:' + bytes2hex(ret)); 90 | return ret; 91 | } 92 | SecurityModule.encrypt3DES.overload('[B', '[B').implementation = function(arg1, arg2){ 93 | var ret = this.encrypt3DES(arg1, arg2); 94 | send('nativeDES_ekey:' + bytes2hex(arg1)); 95 | send('nativeDES_eplain:' + bytes2hex(arg2)); 96 | send('nativeDES_ecipher:' + bytes2hex(ret)); 97 | return ret; 98 | } 99 | 100 | //javaDES 101 | var fhy = Java.use('fhy'); 102 | fhy.a.overload('[B', 'int', 'int', 'boolean').implementation = function(arg1, arg2, arg3, arg4){ 103 | if(arg4){ 104 | send('javaDES1_plain:' + bytes2hex(arg1)); 105 | this.a(arg1, arg2, arg3, arg4); 106 | send('javaDES1_cipher:' + bytes2hex(arg1)); 107 | }else{ 108 | send('javaDES1_cipher:' + bytes2hex(arg1)); 109 | this.a(arg1, arg2, arg3, arg4); 110 | send('javaDES1_plain:' + bytes2hex(arg1)); 111 | } 112 | } 113 | fhy.a.overload('[B', 'int', 'boolean').implementation = function(arg1, arg2, arg3){ 114 | if(arg3){ 115 | send('javaDES2_plain:' + bytes2hex(arg1)); 116 | this.a(arg1, arg2, arg3); 117 | send('javaDES2_cipher:' + bytes2hex(arg1)); 118 | }else{ 119 | send('javaDES2_cipher:' + bytes2hex(arg1)); 120 | this.a(arg1, arg2, arg3); 121 | send('javaDES2_plain:' + bytes2hex(arg1)); 122 | } 123 | } 124 | /* 125 | //RSA加密部分 126 | var bitInt = Java.use('java.math.BigInteger'); 127 | bitInt.modPow.implementation = function(arg1, arg2){ 128 | send('BigInteger'); 129 | v1 = this.toByteArray(); 130 | v2 = arg1.toByteArray(); 131 | v3 = arg2.toByteArray(); 132 | send('BigInteger_p1:' + bytes2hex(v1)); 133 | send('BigInteger_p2:' + bytes2hex(v2)); 134 | send('BigInteger_p3:' + bytes2hex(v3)); 135 | ret = this.modPow(arg1, arg2); 136 | r1 = ret.toByteArray(); 137 | send('BigInteger_p3:' + bytes2hex(r1)); 138 | return ret; 139 | } 140 | */ 141 | }); 142 | """) 143 | 144 | 145 | def on_message(message, data): 146 | if message['type'] == 'error': 147 | print(message['stack']) 148 | elif message['type'] == 'send': 149 | print(message['payload']) 150 | f.write(message['payload'] + '\n') 151 | f.flush() 152 | else: 153 | print(message) 154 | 155 | 156 | script.on('message', on_message) 157 | script.load() 158 | # rdev.resume(pid); 159 | sys.stdin.read() -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 同花顺API交易协议 QQ群63234102 2 | 代码完善中,可转债打新已完成。 3 | ## 使用方法 4 | 1. 修改constants.py中的设备信息参数: 5 | TYPE = '' 6 | UDID = '' 7 | IMEI = '' 8 | IMSI = '' 9 | MAC = '' 10 | 2. 修改hexin.py的账户信息 11 | //券商账户 12 | wtaccount = '' 13 | //券商密码 14 | wtpassword = '' 15 | //券商参数, 可以从券商参数.txt获取 16 | qsid = '' 17 | wtid = '' 18 | dtkltype = '' 19 | 3. 运行hexin.py 20 | ## 分析笔记 21 | [挂机赚钱之可转债打新-同花顺逆向分析笔记](https://github.com/limitget/10jqka-API/blob/master/document/%E6%8C%82%E6%9C%BA%E8%B5%9A%E9%92%B1%E4%B9%8B%E5%8F%AF%E8%BD%AC%E5%80%BA%E6%89%93%E6%96%B0-%E5%90%8C%E8%8A%B1%E9%A1%BA%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E7%AC%94%E8%AE%B0.md) 22 | 23 | 24 | -------------------------------------------------------------------------------- /rsa_utils.py: -------------------------------------------------------------------------------- 1 | import constants 2 | 3 | from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5 4 | from Crypto.PublicKey import RSA 5 | from rsa import transform, core 6 | 7 | 8 | # 常见的rsa加密 9 | def rsa_encrypt(data): 10 | rsakey = RSA.importKey(constants.RSA_KEY) 11 | cipher = Cipher_pkcs1_v1_5.new(rsakey) 12 | return cipher.encrypt(data) 13 | 14 | 15 | # 上述rsa加密不支持不标准的密钥, 采用下面的方法对数据包加密密钥进行加密/解密 16 | def rsa_encrypt_key(data): 17 | data = transform.bytes2int(data) 18 | encrypted = core.encrypt_int(data, constants.RSA_E, constants.RSA_N) 19 | print(constants.RSA_N) 20 | block = transform.int2bytes(encrypted, 64) 21 | return block -------------------------------------------------------------------------------- /stuffInteractData.py: -------------------------------------------------------------------------------- 1 | from OutputStream import OutputStream 2 | 3 | 4 | class stuffInteractData: 5 | title = '' 6 | content = '' 7 | confirm = '' 8 | cancel = '' 9 | id = 0 10 | type = 0 11 | 12 | def __init__(self, InputStream): 13 | decode_Data = OutputStream(InputStream) 14 | 15 | self.title = decode_Data.readUTF() 16 | self.content = decode_Data.readUTF() 17 | if self.content == '': 18 | self.content == '操作频繁,请稍后再试' 19 | 20 | length = decode_Data.readUnsignedShort() 21 | if length > 0: 22 | self.confirm = decode_Data.readUnicode2UTF8(length) 23 | 24 | length = decode_Data.readUnsignedShort() 25 | if length > 0: 26 | self.cancel = decode_Data.readUnicode2UTF8(length) 27 | 28 | if decode_Data.available() >= 6: 29 | decode_Data.skipBytes(2) 30 | self.type = decode_Data.readInt() 31 | 32 | if decode_Data.available() >= 6: 33 | decode_Data.skipBytes(2) 34 | self.id = decode_Data.readInt() -------------------------------------------------------------------------------- /stuffTextData.py: -------------------------------------------------------------------------------- 1 | from OutputStream import OutputStream 2 | 3 | 4 | class stuffTextData: 5 | title = '' 6 | content = '' 7 | type = 0 8 | id = 0 9 | reCode = 0 10 | 11 | def __init__(self, data_type, InputStream): 12 | decode_Data = OutputStream(InputStream) 13 | 14 | self.title = decode_Data.readUTF() 15 | self.content = decode_Data.readUTF() 16 | if self.content == '': 17 | self.content == '操作频繁,请稍后再试' 18 | 19 | self.type = data_type & 0xF0 20 | if self.type == 0: 21 | self.type == 1 22 | elif self.type == 16: 23 | self.type == 2 24 | elif self.type == 32: 25 | self.type == 3 26 | elif self.type == 15 or self.type == 48: 27 | self.type == 4 28 | elif self.type == 64: 29 | self.type == 5 30 | 31 | if decode_Data.available() >= 6: 32 | decode_Data.skipBytes(2) 33 | self.id = decode_Data.readInt() 34 | 35 | if decode_Data.available() >= 6: 36 | decode_Data.skipBytes(2) 37 | self.reCode = decode_Data.readInt() 38 | 39 | def toList(self): 40 | data = {'title': self.title, 'content': self.content, 'type': self.type, 'id': self.id, 'reCode': self.reCode} 41 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | def luhn_residue(digits): 5 | return sum(sum(divmod(int( d) * (1 + i % 2), 10)) 6 | for i, d in enumerate(digits[::-1])) % 10 7 | 8 | 9 | def getImei(): 10 | part = ''.join(str(random.randrange(0, 9)) for _ in range(14)) 11 | res = luhn_residue('{}{}'.format(part, 0)) 12 | return '{}{}'.format(part, -res % 10) 13 | 14 | 15 | def getMac(): 16 | mac = '' 17 | for _ in range(6): 18 | mac += int.to_bytes(random.randint(0, 255), 1, byteorder='little', signed=False).hex() + ':' 19 | return mac[:-1] 20 | -------------------------------------------------------------------------------- /券商参数.txt: -------------------------------------------------------------------------------- 1 | 文本格式: 证券名称|qsid|wtid|dtkltype 2 | 3 | 爱建证券|134|3791|1#安信证券|83|3980|2#财达证券|124|4837|1#财富证券|11|2397|1#财通证券|12|1664|1#长城证券|13|2433|1#长江证券|15|1060|1#长城国瑞|322|4527|2#川财证券|16|3707|2#东兴证券|55|706|1#东北证券|75|2243|1#东方证券|127|5234|4#东莞证券|340|2401|1#东海证券|22|5047|2#东吴证券|23|4776|4#德邦证券|125|5201|1#大同证券|349|614|1#第一创业|97|3014|1#方正证券|25|9154|4#国都证券|29|9170|1#国联证券|30|2406|1#国泰君安|31|6766|1#国融证券|61|4822|1#国盛证券|313|5771|1#国金证券|331|3917|1#国开证券|344|1000|1#广发武证|72|2133|4#广发证券|27|336|2#光大证券|70|6886|1#光大证券|26|316|2#华融证券|347|2989|2#华宝证券|198|2009|1#华创证券|39|1537|4#华林证券|41|3251|1#华龙证券|136|1232|1#华鑫证券|181|3792|1#华金证券|191|1947|4#华福证券|91|4013|2#华西证券|93|4934|1#红塔证券|110|5043|1#南京证券|113|540|1#华泰证券|114|2947|4#和兴证券|35|3613|1#恒泰证券|36|2475|1#金元证券|49|244|2#江海证券|195|3626|1#九州证券|199|2037|1#开源证券|123|6702|1#联讯证券|51|1082|4#联储证券|185|4804|1#民族证券|53|4821|1#民生证券|120|6711|1#平安证券|59|1411|1#世纪证券|63|5376|1#首创证券|65|2189|1#申港证券|109|4803|1#山西证券|327|4958|1#上海证券|321|2405|1#天风证券|343|2252|1#太平洋证券|96|2445|4#万和证券|186|5958|1#万联证券|99|565|4#网信证券|140|1125|1#五矿证券|193|4281|1#信达证券|33|4179|2#西南证券|73|2446|1#兴业证券|80|3650|4#东方财富|137|9553|3#新时代证券|189|6882|1#湘财证券|196|2272|1#银泰证券|320|6987|1#银河证券|90|6368|2#英大证券|128|9319|1#中邮证券|40|3280|1#中投证券|57|4228|1#中信万通|71|5995|2#中山证券|85|4873|1#中天证券|86|3001|1#中银国际|87|9465|4#中原证券|89|3026|4#中泰证券|100|5373|1#中金公司|131|1018|1#中信证券|158|1225|1#招商证券|165|306|1#中信建投|301|2396|1#中航证券|311|4180|1#浙商证券|339|3024|1# --------------------------------------------------------------------------------