Charles 增加自定义解密功能

  1. 简述
    使用Charles工具,抓取加密请求接口时,在调试时,查看入参、出参明文,过于繁琐。通过反编译 Charles.jar 实现自定义解密扩展功能
  1. Flask服务端演示目录
    1
    2
    3
    ├─AESCipherUtils.py  # 加密、解密类
    ├─app.py # 服务端
    └─app2.py # 解密端
    • app.py 服务端

      定义 127.0.0.1:5000/login接口,逻辑:将密文入参解析为明文赋值给 data,并返回 timestamp。统一加密返回

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      from flask import Flask, request
      from AESCipherUtils import AESCipher
      import time
      app = Flask(__name__)

      @app.route('/login')
      def login():
      AES = AESCipher()
      data = {}
      reqBodyString = request.data.decode()
      try:
      data['data'] = AES.decrypt(reqBodyString)
      data['timestamp'] = int(time.time())
      data = AES.encrypt(str(data))

      except Exception as error:
      data['error'] = str(error)
      return data


      if __name__ == '__main__':
      app.run(
      debug=True
      )

    • app2.py 接口解密

      127.0.0.1:5001/decrypt 接口逻辑,接受json body 格式为{“req”:”请求密文”,”res”:”响应密文”} 处理并返回 {“req”:”请求明文”,”res”:”响应明文”}

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      # time : 2022/2/9
      # __author__ = Ysc
      import json

      from flask import Flask, request
      from AESCipherUtils import AESCipher

      app = Flask(__name__)


      @app.route('/decrypt',methods=['GET','POST'])
      def login():
      AES = AESCipher()
      data = {}
      reqBodyJson = json.loads(request.data.decode())
      reqEncryptStr = reqBodyJson['req']
      resEncryptStr = reqBodyJson['res']
      try:
      data['req'] = AES.decrypt(reqEncryptStr)
      except:
      data['req'] = None

      try:
      data['res'] = AES.decrypt(resEncryptStr)
      except:
      data['res'] = None

      return data


      if __name__ == '__main__':
      app.run(
      debug=True,
      port=5001
      )

    • AESCipherUtils.py AES-CBC工具类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      # time : 2022/1/8
      # __author__ = Ysc

      from Crypto.Cipher import AES
      import base64


      class AESCipher:

      def __init__(self):
      self.key = bytes("1234567812345678", encoding='utf-8')
      self.iv = bytes("1234567812345678", encoding='utf-8')

      def pkcs7padding(self, text):
      """
      明文使用PKCS7填充
      最终调用AES加密方法时,传入的是一个byte数组,要求是16的整数倍,因此需要对明文进行处理
      :param text: 待加密内容(明文)
      :return:
      """
      bs = AES.block_size # 16
      length = len(text)
      bytes_length = len(bytes(text, encoding='utf-8'))
      # tips:utf-8编码时,英文占1个byte,而中文占3个byte
      padding_size = length if (bytes_length == length) else bytes_length
      padding = bs - padding_size % bs
      # tips:chr(padding)看与其它语言的约定,有的会使用'\0'
      padding_text = chr(padding) * padding
      return text + padding_text

      def pkcs7unpadding(self, text):
      """
      处理使用PKCS7填充过的数据
      :param text: 解密后的字符串
      :return:
      """
      try:
      length = len(text)
      unpadding = ord(text[length - 1])
      return text[0:length - unpadding]
      except Exception as e:
      pass

      def encrypt(self, content):
      """
      AES加密
      key,iv使用同一个
      模式cbc
      填充pkcs7
      :param key: 密钥
      :param content: 加密内容
      :return:
      """
      cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
      # 处理明文
      content_padding = self.pkcs7padding(content)
      # 加密
      aes_encode_bytes = cipher.encrypt(bytes(content_padding, encoding='utf-8'))
      # 重新编码
      result = str(base64.b64encode(aes_encode_bytes), encoding='utf-8')
      return result

      def decrypt(self, content):
      """
      AES解密
      key,iv使用同一个
      模式cbc
      去填充pkcs7
      :param key:
      :param content:
      :return:
      """
      try:

      cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
      # base64解码
      aes_encode_bytes = base64.b64decode(content)
      # 解密
      aes_decode_bytes = cipher.decrypt(aes_encode_bytes)
      # 重新编码
      result = str(aes_decode_bytes, encoding='utf-8')
      # 去除填充内容
      result = self.pkcs7unpadding(result)
      except Exception as e:
      pass
      if result == None:
      return ""
      else:
      return result

业务接口调用示例
解密接口调用示例

  1. 实现思路

​ 反编译 charles.jar 注入自定义class,并在右键菜单显示,参考文章 https://www.cnblogs.com/Baylor-Chen/p/14963207.html

charles目录

  1. 准备工具jadx-guijd-guiIDEA
  2. 具体实现

    上面的参考文章写的已经很详细了,这里就不多概述怎么 ‘嗅’ 到要改哪里的

    • idea新建项目 testDecrypt

    • 创建和charles相同的代码路径 以下是例子:testDecrypt/src/com/xk72/charles/gui/transaction/actions/TestDecrypt.java

    • 复制charles安装目录lib目录下 charles.jar ,到testDecrypt libs目录下,并设置依赖

    • 整体结构

      设置依赖

      一路ok下一步

    • 使用jd-gui 打开libs 目录下 charles.jar. 选中com/xk72/charles/gui/transaction/popups/TransactionViewerPopupMenu.class ctrl + s 另存为java文件至项目目录下
      jd-gui
      项目结构

    • 处理下错误信息
      处理下语法判断

    • 可以看到 Base64DecodeAction、CopyToClipboardAction 有错误提示
      错误提示

    • 我们先把这两个文件也保存下来瞅瞅,可以看到有一些反编译之后的小错误
      CopyToClipboardAction
      Base64DecodeAction

    • 改改让代码可以正常编译不报错.

    • 修改前 Base64DecodeAction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      package com.xk72.charles.gui.transaction.actions;

      import com.xk72.charles.CharlesContext;
      import com.xk72.charles.gui.lib.yLDW;
      import com.xk72.charles.gui.transaction.lib.HexAsciiTextPane;
      import java.awt.Component;
      import java.awt.Dimension;
      import java.awt.Point;
      import java.awt.event.ActionEvent;
      import java.util.Base64;
      import javax.swing.AbstractAction;
      import javax.swing.JScrollPane;
      import javax.swing.text.JTextComponent;

      public abstract class Base64DecodeAction extends AbstractAction {
      private final Component source;

      public class Text extends Base64DecodeAction {
      private final String text;

      public Text(String str) {
      super((Component) null);
      this.text = str;
      }

      public Text(String str, Component component) {
      super(component);
      this.text = str;
      }

      /* access modifiers changed from: protected */
      public String getBody() {
      return this.text;
      }
      }

      public class TextComponent extends Base64DecodeAction {
      private final JTextComponent component;

      public TextComponent(JTextComponent jTextComponent) {
      super(jTextComponent);
      this.component = jTextComponent;
      }

      /* access modifiers changed from: protected */
      public String getBody() {
      String selectedText = this.component.getSelectedText();
      return selectedText == null ? this.component.getText() : selectedText;
      }
      }

      protected Base64DecodeAction(Component component) {
      super("Base 64 Decode");
      this.source = component;
      }

      public void actionPerformed(ActionEvent actionEvent) {
      try {
      byte[] decode = Base64.getDecoder().decode(getBody());
      HexAsciiTextPane hexAsciiTextPane = new HexAsciiTextPane();
      hexAsciiTextPane.setEditable(false);
      hexAsciiTextPane.setBytes(decode);
      JScrollPane jScrollPane = new JScrollPane(hexAsciiTextPane);
      jScrollPane.setPreferredSize(new Dimension(700, 200));
      yLDW.OZtq(jScrollPane, this.source, (Point) null);
      } catch (Exception e) {
      CharlesContext.getInstance().error("Failed to decode Base 64. Probably not valid Base 64 input.");
      }
      }

      /* access modifiers changed from: protected */
      public abstract String getBody();
      }

    • 修改后 Base64DecodeAction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      package com.xk72.charles.gui.transaction.actions;

      import com.xk72.charles.CharlesContext;
      import com.xk72.charles.gui.lib.yLDW;
      import com.xk72.charles.gui.transaction.lib.HexAsciiTextPane;
      import java.awt.Component;
      import java.awt.Dimension;
      import java.awt.Point;
      import java.awt.event.ActionEvent;
      import java.util.Base64;
      import javax.swing.AbstractAction;
      import javax.swing.JScrollPane;
      import javax.swing.text.JTextComponent;

      public abstract class Base64DecodeAction extends AbstractAction {
      private final Component source;

      public static class Text extends Base64DecodeAction {
      private final String text;

      public Text(String str) {
      super((Component) null);
      this.text = str;
      }

      public Text(String str, Component component) {
      super(component);
      this.text = str;
      }

      /* access modifiers changed from: protected */
      public String getBody() {
      return this.text;
      }
      }

      public static class TextComponent extends Base64DecodeAction {
      private final JTextComponent component;

      public TextComponent(JTextComponent jTextComponent) {
      super(jTextComponent);
      this.component = jTextComponent;
      }

      /* access modifiers changed from: protected */
      public String getBody() {
      String selectedText = this.component.getSelectedText();
      return selectedText == null ? this.component.getText() : selectedText;
      }
      }

      protected Base64DecodeAction(Component component) {
      super("Base 64 Decode");
      this.source = component;
      }

      public void actionPerformed(ActionEvent actionEvent) {
      try {
      byte[] decode = Base64.getDecoder().decode(getBody());
      HexAsciiTextPane hexAsciiTextPane = new HexAsciiTextPane();
      hexAsciiTextPane.setEditable(false);
      hexAsciiTextPane.setBytes(decode);
      JScrollPane jScrollPane = new JScrollPane(hexAsciiTextPane);
      jScrollPane.setPreferredSize(new Dimension(700, 200));
      yLDW.OZtq(jScrollPane, this.source, (Point) null);
      } catch (Exception e) {
      CharlesContext.getInstance().error("Failed to decode Base 64. Probably not valid Base 64 input.");
      }
      }

      /* access modifiers changed from: protected */
      public abstract String getBody();
      }
    • 修改前 CopyToClipboardAction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      package com.xk72.charles.gui.transaction.actions;

      import com.xk72.charles.gui.lib.ExtendedJOptionPane;
      import com.xk72.charles.gui.transaction.viewers.gen.Hyck;
      import com.xk72.charles.model.Transaction;
      import com.xk72.proxy.Fields;
      import com.xk72.proxy.http2.Http2Fields;
      import java.awt.Component;
      import java.awt.Toolkit;
      import java.awt.datatransfer.ClipboardOwner;
      import java.awt.datatransfer.StringSelection;
      import java.awt.datatransfer.Transferable;
      import java.awt.event.ActionEvent;
      import java.io.IOException;
      import java.util.HashSet;
      import java.util.Set;
      import javax.swing.AbstractAction;
      import javax.swing.text.JTextComponent;

      public abstract class CopyToClipboardAction extends AbstractAction {

      public class CurlCommand extends CopyToClipboardAction {
      private static final Set<String> OZtq = new HashSet();
      private final Transaction transaction;

      public CurlCommand(Transaction transaction2) {
      super("Copy cURL Request");
      OZtq.add("Content-Length".toLowerCase());
      OZtq.add("Transfer-Encoding".toLowerCase());
      OZtq.add("Connection".toLowerCase());
      OZtq.add("Content-Encoding".toLowerCase());
      OZtq.add("Accept-Encoding".toLowerCase());
      this.transaction = transaction2;
      }

      private static void OZtq(StringBuilder sb, String str) {
      if (str.indexOf(34) < 0) {
      sb.append('\"').append(str).append('\"');
      } else if (str.indexOf(39) < 0) {
      sb.append('\'').append(str).append('\'');
      } else {
      sb.append('\"');
      int i = 0;
      int indexOf = str.indexOf(34);
      while (indexOf >= 0) {
      sb.append(str, i, indexOf);
      sb.append('\\').append('\"');
      i = indexOf + 1;
      indexOf = str.indexOf(34, i);
      }
      sb.append(str, i, str.length());
      sb.append('\"');
      }
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      boolean z;
      StringBuilder sb = new StringBuilder();
      sb.append("curl ");
      Fields requestHeader = this.transaction.getRequestHeader();
      if (requestHeader instanceof Http2Fields) {
      sb.append("-H 'Host").append(": ").append(requestHeader.getHost()).append("' ");
      String cookies = requestHeader.getCookies();
      if (cookies != null) {
      sb.append("-H 'Cookie").append(": ").append(cookies).append("' ");
      }
      }
      int i = 0;
      boolean z2 = false;
      boolean z3 = false;
      while (i < requestHeader.getFieldCount()) {
      String fieldName = requestHeader.getFieldName(i);
      if (OZtq.contains(fieldName.toLowerCase())) {
      if (fieldName.equalsIgnoreCase("Accept-Encoding")) {
      String fieldValue = requestHeader.getFieldValue(i);
      if (fieldValue.contains("gzip") || fieldValue.contains("deflate")) {
      z2 = true;
      }
      z = z2;
      }
      z = z2;
      } else {
      if (!(requestHeader instanceof Http2Fields) || (!fieldName.startsWith(":") && !fieldName.equalsIgnoreCase("Host") && !fieldName.equalsIgnoreCase("Cookie"))) {
      if (!this.transaction.hasRequestBody() || !"Content-Type".equals(fieldName) || !"application/x-www-form-urlencoded".equals(requestHeader.getFieldValue(i))) {
      sb.append("-H '").append(fieldName);
      String fieldValue2 = requestHeader.getFieldValue(i);
      if (fieldValue2 == null) {
      sb.append(";");
      } else {
      sb.append(": ").append(fieldValue2);
      }
      sb.append("' ");
      } else {
      z = z2;
      z3 = true;
      }
      }
      z = z2;
      }
      i++;
      z2 = z;
      }
      if (this.transaction.hasRequestBody()) {
      if (z3) {
      sb.append("--data ");
      } else {
      sb.append("--data-binary ");
      }
      OZtq(sb, this.transaction.getDecodedRequestBodyAsString());
      sb.append(' ');
      if (!"POST".equals(this.transaction.getMethod())) {
      sb.append("-X ").append(this.transaction.getMethod()).append(' ');
      }
      } else if (!"GET".equals(this.transaction.getMethod())) {
      sb.append("-X ").append(this.transaction.getMethod()).append(' ');
      }
      if (z2) {
      sb.append("--compressed ");
      }
      String externalForm = this.transaction.toURL().toExternalForm();
      sb.append("'");
      sb.append(externalForm);
      sb.append("'");
      return new StringSelection(sb.toString());
      }
      }

      public class Request extends CopyToClipboardAction {
      private final Transaction transaction;

      public Request(Transaction transaction2) {
      super("Copy Request");
      this.transaction = transaction2;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      String decodedRequestBodyAsString = this.transaction.getDecodedRequestBodyAsString();
      if (decodedRequestBodyAsString != null) {
      return new StringSelection(decodedRequestBodyAsString);
      }
      return null;
      }
      }

      public class Response extends CopyToClipboardAction {
      private final Transaction transaction;

      public Response(Transaction transaction2) {
      super("Copy Response");
      this.transaction = transaction2;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      if (this.transaction.getResponseHeader() != null && this.transaction.getResponseSize() > 0 && Hyck.Bgcz(this.transaction)) {
      return new idWS(this.transaction.getDecodedResponseBody());
      }
      String decodedResponseBodyAsString = this.transaction.getDecodedResponseBodyAsString();
      if (decodedResponseBodyAsString != null) {
      return new StringSelection(decodedResponseBodyAsString);
      }
      return null;
      }
      }

      public class Text extends CopyToClipboardAction {
      private final String text;

      public Text(String str) {
      super("Copy Selection");
      this.text = str;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      return new StringSelection(this.text);
      }
      }

      public class TextComponent extends CopyToClipboardAction {
      private final JTextComponent component;

      public TextComponent(JTextComponent jTextComponent) {
      super("Copy Selection");
      this.component = jTextComponent;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      String selectedText = this.component.getSelectedText();
      if (selectedText == null) {
      selectedText = this.component.getText();
      }
      return new StringSelection(selectedText);
      }
      }

      protected CopyToClipboardAction(String str) {
      super(str);
      }

      public void actionPerformed(ActionEvent actionEvent) {
      try {
      Transferable body = getBody();
      if (body != null) {
      Toolkit.getDefaultToolkit().getSystemClipboard().setContents(body, (ClipboardOwner) null);
      }
      } catch (IOException e) {
      ExtendedJOptionPane.OZtq((Component) actionEvent.getSource(), e, "Copy To Clipboard Error", 0);
      }
      }

      /* access modifiers changed from: protected */
      public abstract Transferable getBody();
      }

    • 修改后 CopyToClipboardAction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      package com.xk72.charles.gui.transaction.actions;

      import com.xk72.charles.gui.lib.ExtendedJOptionPane;
      import com.xk72.charles.gui.transaction.viewers.gen.Hyck;
      import com.xk72.charles.model.Transaction;
      import com.xk72.proxy.Fields;
      import com.xk72.proxy.http2.Http2Fields;
      import java.awt.Component;
      import java.awt.Toolkit;
      import java.awt.datatransfer.ClipboardOwner;
      import java.awt.datatransfer.StringSelection;
      import java.awt.datatransfer.Transferable;
      import java.awt.event.ActionEvent;
      import java.io.IOException;
      import java.util.HashSet;
      import java.util.Set;
      import javax.swing.AbstractAction;
      import javax.swing.text.JTextComponent;

      public abstract class CopyToClipboardAction extends AbstractAction {

      public static class CurlCommand extends CopyToClipboardAction {
      private static final Set<String> OZtq = new HashSet();
      private final Transaction transaction;

      public CurlCommand(Transaction transaction2) {
      super("Copy cURL Request");
      OZtq.add("Content-Length".toLowerCase());
      OZtq.add("Transfer-Encoding".toLowerCase());
      OZtq.add("Connection".toLowerCase());
      OZtq.add("Content-Encoding".toLowerCase());
      OZtq.add("Accept-Encoding".toLowerCase());
      this.transaction = transaction2;
      }

      private static void OZtq(StringBuilder sb, String str) {
      if (str.indexOf(34) < 0) {
      sb.append('\"').append(str).append('\"');
      } else if (str.indexOf(39) < 0) {
      sb.append('\'').append(str).append('\'');
      } else {
      sb.append('\"');
      int i = 0;
      int indexOf = str.indexOf(34);
      while (indexOf >= 0) {
      sb.append(str, i, indexOf);
      sb.append('\\').append('\"');
      i = indexOf + 1;
      indexOf = str.indexOf(34, i);
      }
      sb.append(str, i, str.length());
      sb.append('\"');
      }
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      boolean z;
      StringBuilder sb = new StringBuilder();
      sb.append("curl ");
      Fields requestHeader = this.transaction.getRequestHeader();
      if (requestHeader instanceof Http2Fields) {
      sb.append("-H 'Host").append(": ").append(requestHeader.getHost()).append("' ");
      String cookies = requestHeader.getCookies();
      if (cookies != null) {
      sb.append("-H 'Cookie").append(": ").append(cookies).append("' ");
      }
      }
      int i = 0;
      boolean z2 = false;
      boolean z3 = false;
      while (i < requestHeader.getFieldCount()) {
      String fieldName = requestHeader.getFieldName(i);
      if (OZtq.contains(fieldName.toLowerCase())) {
      if (fieldName.equalsIgnoreCase("Accept-Encoding")) {
      String fieldValue = requestHeader.getFieldValue(i);
      if (fieldValue.contains("gzip") || fieldValue.contains("deflate")) {
      z2 = true;
      }
      z = z2;
      }
      z = z2;
      } else {
      if (!(requestHeader instanceof Http2Fields) || (!fieldName.startsWith(":") && !fieldName.equalsIgnoreCase("Host") && !fieldName.equalsIgnoreCase("Cookie"))) {
      if (!this.transaction.hasRequestBody() || !"Content-Type".equals(fieldName) || !"application/x-www-form-urlencoded".equals(requestHeader.getFieldValue(i))) {
      sb.append("-H '").append(fieldName);
      String fieldValue2 = requestHeader.getFieldValue(i);
      if (fieldValue2 == null) {
      sb.append(";");
      } else {
      sb.append(": ").append(fieldValue2);
      }
      sb.append("' ");
      } else {
      z = z2;
      z3 = true;
      }
      }
      z = z2;
      }
      i++;
      z2 = z;
      }
      if (this.transaction.hasRequestBody()) {
      if (z3) {
      sb.append("--data ");
      } else {
      sb.append("--data-binary ");
      }
      OZtq(sb, this.transaction.getDecodedRequestBodyAsString());
      sb.append(' ');
      if (!"POST".equals(this.transaction.getMethod())) {
      sb.append("-X ").append(this.transaction.getMethod()).append(' ');
      }
      } else if (!"GET".equals(this.transaction.getMethod())) {
      sb.append("-X ").append(this.transaction.getMethod()).append(' ');
      }
      if (z2) {
      sb.append("--compressed ");
      }
      String externalForm = this.transaction.toURL().toExternalForm();
      sb.append("'");
      sb.append(externalForm);
      sb.append("'");
      return new StringSelection(sb.toString());
      }
      }

      public class Request extends CopyToClipboardAction {
      private final Transaction transaction;

      public Request(Transaction transaction2) {
      super("Copy Request");
      this.transaction = transaction2;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      String decodedRequestBodyAsString = this.transaction.getDecodedRequestBodyAsString();
      if (decodedRequestBodyAsString != null) {
      return new StringSelection(decodedRequestBodyAsString);
      }
      return null;
      }
      }

      public class Response extends CopyToClipboardAction {
      private final Transaction transaction;

      public Response(Transaction transaction2) {
      super("Copy Response");
      this.transaction = transaction2;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      if (this.transaction.getResponseHeader() != null && this.transaction.getResponseSize() > 0 && Hyck.Bgcz(this.transaction)) {
      return new idWS(this.transaction.getDecodedResponseBody());
      }
      String decodedResponseBodyAsString = this.transaction.getDecodedResponseBodyAsString();
      if (decodedResponseBodyAsString != null) {
      return new StringSelection(decodedResponseBodyAsString);
      }
      return null;
      }
      }

      public static class Text extends CopyToClipboardAction {
      private final String text;

      public Text(String str) {
      super("Copy Selection");
      this.text = str;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      return new StringSelection(this.text);
      }
      }

      public static class TextComponent extends CopyToClipboardAction {
      private final JTextComponent component;

      public TextComponent(JTextComponent jTextComponent) {
      super("Copy Selection");
      this.component = jTextComponent;
      }

      /* access modifiers changed from: protected */
      public Transferable getBody() {
      String selectedText = this.component.getSelectedText();
      if (selectedText == null) {
      selectedText = this.component.getText();
      }
      return new StringSelection(selectedText);
      }
      }

      protected CopyToClipboardAction(String str) {
      super(str);
      }

      public void actionPerformed(ActionEvent actionEvent) {
      Transferable body = getBody();
      if (body != null) {
      Toolkit.getDefaultToolkit().getSystemClipboard().setContents(body, (ClipboardOwner) null);
      }
      }

      /* access modifiers changed from: protected */
      public abstract Transferable getBody();
      }

  3. 解密方法(重头戏)

根据上面的参考文章,我们已经可以知道只需要接收下 JTextComponent 就可以获取到当前TEXT数据,那我们先试验下
新建一个 TestDecryptOne.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.xk72.charles.gui.transaction.actions;

import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.event.ActionEvent;
import java.util.logging.Logger;

/**
* DEMO1
*
* @author ysc
* @date 2022/02/10 15:22
**/
public class TestDecryptOne extends AbstractAction {
private static final Logger logger = Logger.getLogger("test decrypt one");

private final JTextComponent component;


public TestDecryptOne(JTextComponent jTextComponent) {
// 按钮名
super("test decrypt one");
this.component = jTextComponent;
}

@Override
public void actionPerformed(ActionEvent actionEvent) {
logger.info(this.component.getSelectedText());
}

@Override
public boolean accept(Object sender) {
return false;
}
}


然后修改TransactionViewerPopupMenu.java

1
2
3
4
5
6
add((Action)new CopyToClipboardAction.TextComponent((JTextComponent)component));
add((Action)new Base64DecodeAction.TextComponent((JTextComponent)component));

# 新增下面这一行
add(new TestDecryptOne((JTextComponent)component));

编译生成 out目录

1
2
3
4
5
6
7
8
# 复制一份
copy libs\charles.jar out\production\testDecrypt\

cd out\production\testDecrypt
# 注入编译好的class
jar -uvf charles.jar com\xk72\charles\gui\transaction\actions\TestDecryptOne.class
jar -uvf charles.jar com\xk72\charles\gui\transaction\popups\TransactionViewerPopupMenu.class

然后将注入过class的charles.jar 替换原有安装目录lib目录下charles.jar,可以看到,按钮已经出来了,
upload successful

我们右键点击下. 选中request时 log 正常打印,response 却显示 null.
upload successful

而且由于我们的解密接口需要 入参出参组合作为参数, 由此看来 JTextComponent 无法满足我们.
分析之后我们发现 Transaction 对象包含 getDecodedRequestBodyAsString() 以及 getDecodedResponseBodyAsString() 方法.因此我们改下代码
com.xk72.charles.model.Transaction

新建 TestDecrypt.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.xk72.charles.gui.transaction.actions;

import com.xk72.charles.model.Transaction;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.util.logging.Logger;

/**
* demo
*
* @author ysc
* @date 2022/02/10 10:52actionPerformed
**/
public class TestDecrypt extends AbstractAction {
public final Transaction transaction;
private static final Logger logger = Logger.getLogger("test decrypt");

public TestDecrypt(Transaction transaction) {
super("test decrypt");
this.transaction = transaction;
}

@Override
public void actionPerformed(ActionEvent actionEvent) {

String requestString = transaction.getDecodedRequestBodyAsString();
String responseString = transaction.getDecodedResponseBodyAsString();
logger.warning("__________________________________");
logger.info(requestString);
logger.info(responseString);
logger.warning("__________________________________");
}


@Override
public boolean accept(Object sender) {
return false;
}
}

修改 TransactionViewerPopupMenu.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义 Transaction 
private final Transaction transaction;

public TransactionViewerPopupMenu(Transaction paramTransaction) {
super(paramTransaction, null, null, null);
// 接收
this.transaction = paramTransaction;
}


add((Action)new CopyToClipboardAction.TextComponent((JTextComponent)component));
add((Action)new Base64DecodeAction.TextComponent((JTextComponent)component));

# 新增下面这一行
add(new TestDecryptOne((JTextComponent)component));

# 再新增一行接收 Transaction的
add(new TestDecrypt(this.transaction));

重新编译注入一下

1
jar -uvf charles.jar com\xk72\charles\gui\transaction\actions\TestDecrypt.class

upload successful

upload successful

ok 到这一步,我们已经获取到了 requestBody 和 responseBody. 基本上就已经结束了.

为了省事我就不找charles自己的请求方法了,我们写一个 HttpClient.java 用来请求解密接口
HttpClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package com.xk72.charles.gui.transaction.actions;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
* 请求方法
*
* @author ysc test
* @date 2022/01/24 16:16
**/

public class HttpClient {
public static String doGet(String httpurl) {
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader br = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpurl);
// 通过远程url连接对象打开一个连接,强转成httpURLConnection类
connection = (HttpURLConnection) url.openConnection();
// 设置连接方式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输入流
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
// 封装输入流is,并指定字符集
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// 存放数据
StringBuffer sbf = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}

connection.disconnect();// 关闭远程连接
}

return result;
}

public static String doPost(String httpUrl, String param) {

HttpURLConnection connection = null;
InputStream is = null;
OutputStream os = null;
BufferedReader br = null;
String result = null;
try {
URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);

// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 默认值为:true,当前向远程服务读取数据时,设置为true,该参数可有可无
connection.setDoInput(true);
// 设置传入参数的格式:请求参数应该是 json 的形式。
connection.setRequestProperty("Content-Type", "application/json");
// 通过连接对象获取一个输出流
os = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
os.write(param.getBytes());
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {

is = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));

StringBuffer sbf = new StringBuffer();
String temp = null;
// 循环遍历一行一行读取数据
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != os) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 断开与远程地址url的连接
connection.disconnect();
}
return result;
}

}

然后重新修改下 TestDecrypt.java 注入

1
2
jar -uvf charles.jar com\xk72\charles\gui\transaction\actions\TestDecrypt.class
jar -uvf charles.jar com\xk72\charles\gui\transaction\actions\HttpClient.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.xk72.charles.gui.transaction.actions;

import com.xk72.charles.model.Transaction;
import org.json.JSONObject;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.util.logging.Logger;

/**
* demo
*
* @author ysc
* @date 2022/02/10 10:52actionPerformed
**/
public class TestDecrypt extends AbstractAction {
public final Transaction transaction;
private static final Logger logger = Logger.getLogger("test decrypt");

public TestDecrypt(Transaction transaction) {
super("test decrypt");
this.transaction = transaction;
}

@Override
public void actionPerformed(ActionEvent actionEvent) {
decrypt(this.transaction);

}

public static void decrypt(Transaction transaction){

String requestString = transaction.getDecodedRequestBodyAsString();
String responseString = transaction.getDecodedResponseBodyAsString();

JSONObject jsonObject = new JSONObject();
jsonObject.put("req",requestString);
jsonObject.put("res",responseString);
String decryptJsonString = HttpClient.doPost("http://127.0.0.1:5001/decrypt",jsonObject.toString());

JSONObject decryptJsonObj = new JSONObject(decryptJsonString);

JSONObject res = new JSONObject(decryptJsonObj.get("req").toString());
JSONObject req = new JSONObject(decryptJsonObj.get("res").toString());

WaringDialog("req",req.toString());
WaringDialog("res",res.toString());

}
public static void WaringDialog(String title, String content) {
JFrame JFrame = new JFrame(title);
JFrame.setPreferredSize(new Dimension(800, 500));
JTextArea textArea = new JTextArea();
textArea.setText(content + "\n");
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);

JScrollPane jScrollPane = new JScrollPane(textArea);
jScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

jScrollPane.setAutoscrolls(false);
JFrame.setContentPane(jScrollPane);
JFrame.pack();
JFrame.setVisible(true);
}

@Override
public boolean accept(Object sender) {
return false;
}
}

成功

  1. 总结
    Charles使用Java Gui进行构建以实现跨平台,开发者则可以Java本身机制通过实现自身需要的Class类通过 jar -uvf 注入来实现开发者自身需要的功能。