某借款软件逆向实战实现自动加解密+重新签名

admin 2025年4月29日10:44:57评论4 views字数 12894阅读42分58秒阅读模式

某借款软件逆向实战实现自动加解密+重新签名

如图,下面的APP,在发送请求的时候会对请求进行加密,这就很不方便我们进行测试,所以需要对其进行逆向,找出它的加密算法,对其进行解密,来方便我们对其进行测试。

某借款软件逆向实战实现自动加解密+重新签名

首先我们对其进行反编译,获取到它的前端源码,这里我已看到是有一个 360壳子的特征的,而且也确实没有获取到全部的前端代码,所以需要对其进行砸壳

某借款软件逆向实战实现自动加解密+重新签名

安装砸壳工具 pip3 install frida-dexdump

使用 frida-dexdump -U -f com.app.app 砸壳,它会在当前路径下新建一个 com.app.app 的路径,然后将脱壳后的dex文件保存在这里

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

之后使用这位大佬提供的合并工具,对其进行合并 https://www.52pojie.cn/thread-1947354-1-1.html

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

之后我们对这个脱壳的apk进行反编译看看(合并完发现丢东西了,代码不完整,我直接找研发让他重新打个不加壳的包出来,这就是社会工程学,当然也是我太菜了,不会脱壳)

某借款软件逆向实战实现自动加解密+重新签名

然后根据我们之前抓到的发送验证码的接口作为突破口,找到他的加密方法

某借款软件逆向实战实现自动加解密+重新签名

然后我们再找谁调用了这个变量

某借款软件逆向实战实现自动加解密+重新签名

然后我们再看这个接口请求方法被谁调用了,这里我们需要注意的是 传给它的第二个参数,也就是 body 部分,这里我们找到三个调用,根据类名判断是第二个登录使用的获取验证码,是我们要找的代码

某借款软件逆向实战实现自动加解密+重新签名

然后我们看他的第二个参数名为 AESEncipherMap 推测已经完成了 AES 加密,我们看一下他的代码,发现是通过 gson的 tojson方法对 processMap(map)返回的java对象进行序列化,但是我们还是看一下为好

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

使用 frida hook 这个方法,看看它的入参是什么,又返回了什么东西,脚本如下

javascript                  // Hook aes 加密方法                  function aeshook(){                  // 指定类                  var HttpUtils = Java.use('com.a.a.network.HttpUtils');                  // 重写 AESEncipherMapBody 方法                  // 保留原始的 AESEncipherMapBody 方法                  var originalAESEncipherMapBody = HttpUtils.AESEncipherMapBody;                  HttpUtils.AESEncipherMapBody.implementation = function (map) {                  // 打印传入的参数                  console.log("发送验证码加密入参:");                  var mapEntries = Java.cast(map.entrySet(), Java.use('java.util.Set'));                  var iterator = mapEntries.iterator();                  while (iterator.hasNext()) {                  var entry = Java.cast(iterator.next(),Java.use('java.util.Map$Entry'));                  console.log(entry.getKey() + ": " + entry.getValue());                  }                  // 调用原始方法获取返回值                  var result = originalAESEncipherMapBody.call(this, map);                  try {                  var buffer = Java.use('okio.Buffer');                  var bufferInstance = buffer.$new();                  result.writeTo(bufferInstance);                  var content = bufferInstance.readUtf8();                  console.log("原始方法的返回值", content);                  } catch (e) {                  console.log("读取 RequestBody 内容时出错:", e)                  }                  return result;                  }                  }                  Java.perform(function(){                  // 启动入口                  aeshook()                  })                  

可见,这里的入参就已经完成了加密,那么我们就应该,顺着这个参数继续往上找,看看是谁把这个 AESEncipherMap 传过来的

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

跟进去之后我们找到了这个方法,那么我们就要看一下 AESOuterEncrypt() 这个方法了,我们可以看到,主要是要了两个参数,一个是当前循环的键值对的值,和一个当前的时间戳,

某借款软件逆向实战实现自动加解密+重新签名

我们分析一下下面的加密代码,当传入的时间戳不为 null 时,取时间戳的后八位,如果为 null 就取 "",传给AESCBCUtil.encrypt,而其中的 str 就是要加密的值,加密的key由 so层里面获取的值加上时间戳后八位组成,加密iv同样从so层获取,那么我们下面就应该想办法获取到这个so层里面的key和iv了

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

同样使用hook获取其中的参数值,执行 frida -UF -l .hookAesKey.js,这里可以多试几次,看看是不是固定的,可见key的前8位和iv均为固定值,

javascript                  function hookAesKey() {                  // 指定类                  var AESCBCUtil = Java.use('com.a.a.network.aes.AESCBCUtil')                  // 重写 encrypt 方法                  // 保留原有 encrypt 方法                  var originalencrypt = AESCBCUtil.encrypt;                  AESCBCUtil.encrypt.implementation = function (str1,str2,str3) {                  //打印入参                  console.log("待加密值:", str1)                  console.log("加密key: ", str2)                  console.log("加密iv: ", str3)                  var result = originalencrypt.call(this, str1, str2, str3)                  return result                  }                  }                  Java.perform(function(){                  // 启动入口                  hookAesKey()                  })

某借款软件逆向实战实现自动加解密+重新签名

那么后面就很简单了,我们要做的就是让burp自动解密,在修改参数之后自动加密即可

下面我们先写一个python的加解密脚本

python                  # -*- conding: utf-8 -*-                  from Crypto.Cipher import AES                  from Crypto.Util.Padding import pad, unpad                  import base64                  def encrypt(plaintext, key, iv):                  try:                  # 创建 AES 加密器,使用 CBC 模式                  cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))                  # 对明文进行填充,使其长度为 AES 块大小的整数倍                  padded_plaintext = pad(plaintext.encode('utf-8'), AES.block_size)                  # 进行加密操作                  ciphertext = cipher.encrypt(padded_plaintext)                  # 对密文进行 Base64 编码                  return base64.b64encode(ciphertext).decode('utf-8')                  except Exception as e:                  print(f"加密时出现异常: {e}")                  return None                  def decrypt(ciphertext, key, iv):                  try:                  # 对 Base64 编码的密文进行解码                  ciphertext = base64.b64decode(ciphertext)                  # 创建 AES 解密器,使用 CBC 模式                  cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))                  # 进行解密操作                  decrypted_data = cipher.decrypt(ciphertext)                  # 去除填充                  unpadded_data = unpad(decrypted_data, AES.block_size)                  return unpadded_data.decode('utf-8')                  except Exception as e:                  print(f"解密时出现异常: {e}")                  return None                  if __name__ == '__main__':                  text = "13333333333"                  key = "8111111188201096"                  iv = "01111111111111"                  print(encrypt(text, key, iv))

某借款软件逆向实战实现自动加解密+重新签名

可见执行结果和前面的是一致的,下面就是解决联动问题了,这里使用 burp的 Galaxy 插件,注意使用此插件 burp 版本需要大于 v2023.10.3.7 

以下为针对此app的自动加解密及加签脚本

python                  import json                  import base64                  import hashlib                  from fastapi import FastAPI                  from Crypto.Cipher import AES                  from Crypto.Util.Padding import pad, unpad                  from _base_classes import *                  IV = "0111111111111111"                  JSON_KEY = "data"                  app = FastAPI()                  @app.post("/hookRequestToBurp", response_model=RequestModel)                  async def hook_request_to_burp(request: RequestModel):                  """HTTP请求从数据客户端到达Burp时被调用。在此处完成请求解密的代码就可以在Burp中看到明文的请求报文。"""print("--------------------------------")                  request_dict = json.loads(request.model_dump_json())                  print("原client向burp请求体")                  print(request_dict)                  content_base64 = request_dict.get("contentBase64")                  timestamp = request_dict["headers"]["X-Timestamp"][0]                  aes_key = f"8222222f{timestamp[-8:]}"                  print("AES Key: " + aes_key)                  if content_base64:                  # 解码 base64                  content_plaintext = base64.b64decode(content_base64)                  # 解码二进制为json字符串                  content_json = content_plaintext.decode("utf-8")                  # json 转 python对象                  content_dict = json.loads(content_json)                  # 获取待解密的值                  need_decrypt = content_dict.get("content")                  # 判断是否有需要解密的数据                  if need_decrypt is not None:                  # 调用解密函数,解密出要传给 burp 的明文                  plaintext = decrypt(need_decrypt, aes_key, IV)                  # 将解密后的值进行base64,准备替换原本 request_dict 中的 contentBase64 的值                  plaintext_base64 = base64.b64encode(plaintext.encode("utf-8"))                  request_dict["contentBase64"] = plaintext_base64.decode("utf-8")                  # 创建新的 RequestModel 实例,将其传回给原来的请求                  new_request = RequestModel(**request_dict)                  return new_request                  else:                  return request                  else:                  return request                  @app.post("/hookRequestToServer", response_model=RequestModel)                  async def hook_request_to_server(request: RequestModel):                  """HTTP请求从Burp将要发送到Server时被调用。在此处完成请求加密的代码就可以将加密后的请求报文发送到Server。"""print("--------------------------------")                  request_dict = json.loads(request.model_dump_json())                  print("原burp向server请求体")                  print(request_dict)                  content_base64 = request_dict.get("contentBase64")                  timestamp = request_dict["headers"]["X-Timestamp"][0]                  aes_key = f"8222222f{timestamp[-8:]}"                  print("AES Key: " + aes_key)                  if content_base64:                  # 解码 base64                  content_plaintext = base64.b64decode(content_base64)                  # 解码二进制为json字符串                  need_encrypt = content_plaintext.decode("utf-8")                  print("原需要加密传输的请求体为:" + need_encrypt)                  # 对请求体进行二次加签,防止修改请求后签名不过                  need_encrypt_dict = json.loads(need_encrypt)                  print("旧签名为: " + need_encrypt_dict["sign"])                  need_encrypt_dict["sign"] = get_sign(need_encrypt_dict)                  print("新签名为: " + need_encrypt_dict["sign"])                  need_encrypt_dict["timestamp"] = timestamp                  need_encrypt = json.dumps(need_encrypt_dict)                  print("新需要加密传输的请求体为:" + need_encrypt)                  # 判断是否有需要加密的值                  if need_encrypt is not None:                  # 调用加密函数,加密出要传给 server 的密文                  ciphertext = encrypt(need_encrypt, aes_key, IV)                  print("Ciphertext: " + ciphertext)                  # 构造原client请求                  origin_ciphertext = json.dumps({"content": ciphertext})                  # 将加密后的值进行base64,准备替换原本 request_dict 中的 contentBase64 的值                  plaintext_base64 = base64.b64encode(origin_ciphertext.encode("utf-8"))                  request_dict["contentBase64"] = plaintext_base64.decode("utf-8")                  # 创建新的 RequestModel 实例,将其传回给原来的请求                  new_request = RequestModel(**request_dict)                  return new_request                  else:                  return request                  else:                  return request                  @app.post("/hookResponseToBurp", response_model=ResponseModel)                  async def hook_response_to_burp(response: ResponseModel):                  """HTTP响应从Server到达Burp时被调用。在此处完成响应解密的代码就可以在Burp中看到明文的响应报文。"""print("--------------------------------")                  # 获取server向burp的响应体                  response_dict = json.loads(response.model_dump_json())                  print("原server向burp的响应体")                  print(response_dict)                  if response_dict.get("contentBase64"):                  content_base64 = response_dict.get("contentBase64")                  # base64解码响应体,以便获取时间戳                  content_b64decode_json = base64.b64decode(content_base64).decode("utf-8")                  content_b64decode_dict = json.loads(content_b64decode_json)                  timestamp = content_b64decode_dict["timestamp"]                  aes_key = f"8222222f{timestamp[-8:]}"                  print("AES Key: " + aes_key)                  print(content_b64decode_dict)                  need_decrypt = content_b64decode_dict["data"]                  # 判断是否有需要解密的值                  if need_decrypt is not None:                  # 对其中的 data 进行解密                  plaintext = decrypt(need_decrypt, aes_key, IV)                  # 拼接解密后的数据到原来的响应体中                  content_b64decode_dict["data"] = plaintext                  # 将新的响应体base64后,准备发送给burp                  plaintext_base64 = base64.b64encode(json.dumps(content_b64decode_dict, ensure_ascii=False).encode("utf-8"))                  response_dict["contentBase64"] = plaintext_base64                  new_response = ResponseModel(**response_dict)                  print("新响应体")                  print(new_response)                  return new_response                  else:                  return response                  else:                  return response                  @app.post("/hookResponseToClient", response_model=ResponseModel)                  async def hook_response_to_client(response: ResponseModel):                  """HTTP响应从Burp将要发送到Client时被调用。在此处完成响应加密的代码就可以将加密后的响应报文返回给Client。"""print("--------------------------------")                  # 获取 burp 向 client 的响应体                  response_dict = json.loads(response.model_dump_json())                  print("原burp向client响应体")                  print(response_dict)                  if response_dict.get("contentBase64"):                  content_base64 = response_dict.get("contentBase64")                  # base64 解码响应体                  content_b64decode_json = base64.b64decode(content_base64).decode("utf-8")                  content_b64decode_dict = json.loads(content_b64decode_json)                  timestamp = content_b64decode_dict["timestamp"]                  aes_key = f"8222222f{timestamp[-8:]}"                  print('AES Key: ' + aes_key)                  # 获取待加密值,并调用加密函数                  need_decrypt = content_b64decode_dict["data"]                  # 判断是否有需要加密的值                  if need_decrypt is not None:                  ciphertext = encrypt(need_decrypt, aes_key, IV)                  print("Ciphertext: " + ciphertext)                  # 将加密值放回到原有的字典中,并生成json字符串                  content_b64decode_dict["data"] = ciphertext                  ciphertext_base64 = base64.b64encode(json.dumps(content_b64decode_dict, ensure_ascii=False).encode("utf-8"))                  response_dict["contentBase64"] = ciphertext_base64                  new_response = ResponseModel(**response_dict)                  print("新响应体")                  print(new_response)                  return new_response                  else:                  return response                  else:                  return response                  def decrypt(ciphertext, key, iv):                  try:                  # 对 Base64 编码的密文进行解码                  ciphertext = base64.b64decode(ciphertext)                  # 创建 AES 解密器,使用 CBC 模式                  cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))                  # 进行解密操作                  decrypted_data = cipher.decrypt(ciphertext)                  print(decrypted_data)                  # 去除填充                  unpadded_data = unpad(decrypted_data, AES.block_size)                  return unpadded_data.decode('utf-8')                  except Exception as e:                  print(f"解密时出现异常: {e}")                  return None                  def encrypt(plaintext, key, iv):                  try:                  # 创建 AES 加密器,使用 CBC 模式                  cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))                  # 对明文进行填充,使其长度为 AES 块大小的整数倍                  padded_plaintext = pad(plaintext.encode('utf-8'), AES.block_size)                  # 进行加密操作                  ciphertext = cipher.encrypt(padded_plaintext)                  print(ciphertext)                  # 对密文进行 Base64 编码                  return base64.b64encode(ciphertext).decode("utf-8")                  except Exception as e:                  print(f"加密时出现异常: {e}")                  return None                  def string_no_null(obj):                  return obj is not None and obj != "" and obj != "null" and obj != " "                  def encrypt_md5(text):                  md5 = hashlib.md5()                  md5.update(text.encode('utf-8'))                  return md5.hexdigest()                  def get_sign(request_data):                  request_data.pop('sign', None)                  if request_data["timestamp"]:                  request_data["soltSignKey"] = encrypt_md5(request_data["timestamp"])                  tree_map = {}                  for key, value in request_data.items():                  if not isinstance(value, (dict, list)) and string_no_null(value):                  tree_map[key] = str(value)                  sorted_tree_map = dict(sorted(tree_map.items()))                  input_str = ""                  for v in sorted_tree_map.values():                  input_str += v + "&"                  if input_str:                  input_str = input_str[:-1]                  return encrypt_md5(input_str)                  if __name__ == "__main__":                  # 多进程启动                  # uvicorn aes_cbc_query:app --host 0.0.0.0 --port 5000 --workers 4                  import uvicorn                  uvicorn.run(app, host="0.0.0.0", port=5000)

在下面填写对应的要抓包的 host 值,以及自己上面的加解密脚本的本地地址,然后点击开始即可

某借款软件逆向实战实现自动加解密+重新签名

某借款软件逆向实战实现自动加解密+重新签名

原文始发于微信公众号(隐雾安全):某借款软件逆向实战实现自动加解密+重新签名

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月29日10:44:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   某借款软件逆向实战实现自动加解密+重新签名https://cn-sec.com/archives/4014259.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息