(译)因循的四象限

因循的四象限 原文:The Four Quadrants of Conformism Author : Paul Gramham July 2020 给人分类最好的标准之一便是其因循程度和积极性。想象一个平面坐标系,横轴从左到右分别是循规蹈矩的人

Setup

Network For hardware stuff, see Wireless SSH Key-Auth ssh-keygen -t rsa host {shortName} Hostname {address} Port 22 User {username} IdentityFile {path/to/key} do not forget to set private key 600 Win10-OpenSSH-Server Install from Settings UI : Optional Features Install from PowerShell : Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 ===Start-Service Start-Service sshd # OPTIONAL but recommended: Set-Service -Name sshd -StartupType 'Automatic' # then back to local machine: ssh username@servername Win32-OpenSSH PS: Set-ExecutionPolicy RemoteSigned or

Server-Hacked

└─# lsof -c uBXOvYBM COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME uBXOvYBM 318196 root cwd DIR 254,1 4096 2 / uBXOvYBM 318196 root rtd DIR 254,1 4096 2 / uBXOvYBM 318196 root txt REG 254,1 819252 1048698 /root/08db56cb75fd057be28be1007c5f4424 (deleted) uBXOvYBM 318196 root DEL REG 0,14 127380184 /anon_hugepage uBXOvYBM 318196 root DEL REG 0,14 17030308 /anon_hugepage uBXOvYBM 318196 root DEL REG 0,14 17030306 /anon_hugepage uBXOvYBM 318196 root

ML-Leaks

NDSS 2019 研究成果概括 ML-Leaks 前言 本文将对 NDSS (Network and Distributed System Security Symposium) 2019 获奖论文 ML-Leaks: Model and Data Independent Membership Inference Attacks and Defenses on Machine Learning Models 进行解读。这篇论文的主要研究内容是针对机器学习模型的成员推

FART

安卓脱壳 FART 速成 环境准备 Pixel 3a XL 一台,代号 bonito,先恢复出厂系统 ,再准备相应源码, android-9.0.0_r47 对应 版本号 PQ3B.190801.002 对应,android-10.0.0_r2 对

Android Hook

Frida

万金油动态调试工具,配合自己收集定制的 hook 模板代码,稍作修改就可以快速查看 Java 层的类及其方法成员信息和 Native 层函数的参数与返回值,便于验证自己的想法,但实际上手可能还会遇到不少坑点令人苦恼:

  • Java/Native 层数据结构映射到 JS 这种动态语言,可能需要 cast 或者自己转换成 JS 中的类型
  • Native 层通过 findExportByName 获取函数不够准确,可能还要通过地址
  • 不应发生的 cannot access address … 问题

https://www.anquanke.com/post/id/195869

https://kevinspider.github.io/fridahookjava/

https://kevinspider.github.io/fridahookso/

https://kevinspider.github.io/zhuabao/

function map2obj(map) {
   var res = {};
   var keyset = map.keySet();
   var it = keyset.iterator();
   while (it.hasNext()) {
      var keystr = it.next().toString();
      var valuestr = map.get(keystr).toString();
      res[keystr] = valuestr
   }
   return res;
}

function dfs(self, depth) {
    if (depth > 6) return {}
    const obj = {}
    const cls = self.getClass()
    const fields = cls.getDeclaredFields()
    // console.log("-".repeat(depth), "dfs", cls, self)
    // console.log("-".repeat(depth), "fields:", fields)
    const immediates = ['short', 'int', 'long', 'float', 'double', 'boolean', 'String']
    fields.forEach(x => {
        x.setAccessible(true)
        const v = x.get(self)
        if (v === null) return
        const s = x.toString()  // public type fullname
        // const type = x.getType()   // class java.lang.String
        // const k = x.getName()   // short name
        // console.warn(x, v, k, type)
        if (immediates.some(type => s.includes(type))) {
            obj[x] = v.toString()
        } else {  // inner class
            obj[x] = dfs(v, depth+1)
        }
    })
    return obj
}

function hookJava() {
   if (Java.available) {
      Java.perform(function () {
         var cls = Java.classFactory.use("com.package.classname");
         cls.methodName.implementation = function (a1, a2, a3, a4) {
            console.log('>'.repeat(10), "hookJava begin")
            let a2str = JSON.stringify(map2obj(a2), null, 4)
            console.log(a1, a3, a4)
            console.warn(a2str)
            var res = this.methodName(a1, a2, a3, a4)
            console.warn('res:', res)
            return res
            console.log("hookJava end", '<'.repeat(10))
         }
      })
   }
}

function hookNative() {
   let m = Process.findModuleByName('lib.so')
   let f = Module.findExportByName('lib.so', 'Functions_xx')
   console.log(m.base, f)
   // f = m.base.add(0xBDB8C)
   Interceptor.attach(f, {
      onEnter: function (args) {
         console.warn("args:", args[1], args[1].readCString())
      },
      onLeave: function (ret) {
         console.warn("ret:", ret, ret.readCString())
         // this.context.r0 = 1
      }
   })
}

setImmediate(hookJava)
// setImmediate(hookNative)

Btrfs 踩坑记录

用上 Btrfs 不到两个月,还没怎么享受透明压缩和增量快照带来的好处,却已为它熬过几个艰难的夜晚

先是 WinBtrfs 的问题,btrfs check --repair 幸运地修回来,果断注册表里改成只读

但之后在 Arch 中作死用 VMWare 从物理磁盘启动自身,却造成了毁灭性后果,整个系统突然变为 ro,重启后果然 transid error 无法进入

老规矩先抢救数据, restore 到 ext4 格式的移动硬盘(exFAT 真没用)

这次虽然 transid 只差了 1,但 check 后发现问题比上次更为严重,check -b, check -s 1 结果都不妙

记下 btrfs-find-root 的结果以备之后 repair

但可惜 repair 也无能为力,可能还让事情更糟了,试了 rescue zero-log 也没救回

神奇的是进 Win 还能正常识别文件,也不知道是 repair 还是 rescue 让 btrfs 分区能直接挂载了

现在问题变成了 EIO,理论上是盘坏了但它肯定没坏,数据都还能读但无法恢复正常

没办法,趁还可以挂载 btrfs 分区,rsync -aviHAXKhP 再备份一遍到移动硬盘(注意 exclude 快照和无用大目录,否则等一晚上)

把 btrfs 分区格了再从移动硬盘拖回去,子卷化,改 fstab,重做 grub 引导,终于进入了熟悉的 Arch

然而用户配置等方面还是有问题,可能第二回的备份不全,把之前备份的配置覆盖回去。pacman 还有数据不一致问题,overwrite 解决

AppRE

App 逆向基础

国产应用大多热衷于构筑自己的 App 围墙,很多功能没有网页版,也就无法利用浏览器一探究竟,不过我们仍然可以通过抓包、静态分析、动态调试的方法解开隐藏在 App 中的秘密。

抓包能让我们快速获得想要的 API,不过其门槛也在不断增高,Android 7.0 之后应用不再相信非系统证书,客户端应用也可能使用 SSL Pinning 等技术防止中间人的干扰,一般需要使用 Xpose 模块 JustTrustMe。

抓包获得关键请求后,分析其字段的意义,并在静态分析工具中全局搜索,定位至相关函数,应用大多会将数据编码、加密或生成摘要,这些逻辑可能放在 native 层实现,增大了逆向的难度。

所幸 frida 等工具的出现大大便利了动态调试,可以方便地 hook 得到 Java 层各个类及其成员、方法,对于 native 层,也可在获得函数的参数和返回值,快速验证逆向分析时的想法。若由于时机等原因难以 hook,还可直接将 so 库封装到自己创建的 app 中,在 build.gradle 里添加 abiFilters 参数以指定 arm 指令集,手动复制关键类并 import,再在 MainActivity 里 loadLibrary,即可直接调用 native 层方法,调试并在断点之间 hook 更改 context 寄存器的值,查看变量的值。

逆向得到加密数据、生成校验的算法后,便可以伪造合法的请求。编码上的细节需要多加考虑,抓包得到 params 或 body 中的参数大都是 urlencode 后的结果,但生成校验时的参数却可能是原始的字符串,构造请求时需要仔细考虑。排查错误时要冷静,关键位置往往是正确的,但完全没料到的地方可能出岔子,比如谁能想到 f-string 中嵌入 bytes 型的参数,生成的字符串里居然还带着引号,而且作为 body 发送居然看上去一模一样?

bstr = b'feiwu'
fstr = f'woshi {bstr}'
print(fstr)
# woshi b'feiwu'

不能以脚本小子的心态写脚本,必须做好代码的类型标注,模块化编程,这样即使无法避免问题的发生,也能在问题出现时快速定位。排查问题时脑子注意转过弯来,如果加密算法中有随机值,先固定下来,在静态的层面上观察结果,与真实样本做对比。

实战案例复盘

某品会 edata 参数(AES 加密)

仅有少量请求有 edata 参数,从一串 query params 型的键值对字符串,得到 AES 加密并 base64 编码后的 edata 结果,具体实现在 esNav 这个 native 函数中。

首先静态分析,IDA 反编译后两百多行,一上来就从全局变量中获取了未知的字符串,然后放入不知所云的 gsigds 函数中进行一通操作。此时盲目扎进细节中耗时耗力而且白费功夫,只需抓住 AES 加密的核心,无非是 key 和 iv,倒过来分析代码发现前者是 md5 后的值,后者是随机的16位 hex 字符串,生成 edata 的前十六位字符便是 iv,后面再拼接 AES 加密的结果,这样服务器获得发送过来的 edata 后即可对称解密,而 key 显然应该是每次固定的,所以只需 hook 生成 md5 的函数获得返回值,便能得到 key 进而实现加密算法。

但在测试手机上发现该应用在运行时 hook 容易崩溃,只能以 spawn 的形式 hook, 而抓包发现 edata 的请求似乎只在初始化时发送,刚启动时 native 层中的关键函数又尚未被加载,很难有合适的时机 hook,这时就可以自制 App 直接调用 Java 层函数,在断点之间 hook 即可拿到 key。

某品会 api_sign 验证头(SHA1 摘要)

每一个请求头都会带上 Authorization: OAuth api_sign={},全局搜索定位到 native 函数 gsNav,是从 TreeMap<String, String>(也就是 query params) 得到一串 SHA1 摘要。

进 IDA 分析,发现仍然调用了 gsigds 函数获取字符串,传入 getByteHash 获得了32位的 hex 字符串作为盐,拼接在从 Map 转成的 query param 型字符串前进行 SHA1 摘要,再对结果再来一次加盐摘要即得 api_sign,实际上如果熟悉 SHA1 的话看到 api_sign 是长为40的 hex 应该就能想到。

import base6
import hashlib
import json
import random
from urllib.parse import unquote, parse_qsl, urlencode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad


def gen_sign(paramstr: str) -> str:
    """paramstr will unquoted automatically"""
    paramstr = unquote(paramstr).encode()
    salt = b"da19a1b93059ff3609fc1ed2e04b0141"  # True
    salt = b"aee4c425dbb2288b80c71347cc37d04b"  # False
    h1 = hashlib.sha1(salt + paramstr)
    cipher1 = h1.hexdigest().encode()
    h2 = hashlib.sha1(salt + cipher1)
    return h2.hexdigest()

def gen_edata(paramstr: str) -> str:
    """paramstr: app_name=...&dinfo=..."""
    paramstr = paramstr.encode()
    paramstr = pad(paramstr, 16)
    key = bytearray.fromhex("8c c7 03 f6 47 8e 58 f0 84 49 d5 c0 cf 2d d5 83")  # True
    key = bytearray.fromhex("cd d1 7a b2 9b 84 b3 25 52 dd cf bb 4a bf 02 25")  # False
    key = bytes(key)
    # ran16b = "4e0f60fc02e91a04".encode()
    ran16b = ''.join(random.choices('0123456789abcdef', k=16)).encode()
    cipher = AES.new(key, AES.MODE_CBC, iv=ran16b)
    enctext = cipher.encrypt(paramstr)
    ans = base64.b64encode(ran16b + enctext)
    return ans.decode()

def dec_edata(b64s: str) -> str:
    enctext = base64.b64decode(b64s.encode())
    key = bytearray.fromhex("8c c7 03 f6 47 8e 58 f0 84 49 d5 c0 cf 2d d5 83")  # True
    key = bytearray.fromhex("cd d1 7a b2 9b 84 b3 25 52 dd cf bb 4a bf 02 25")  # False
    key = bytes(key)
    iv = enctext[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    raw = cipher.decrypt(enctext[16:])
    try:
        return raw.decode()
    except:
        return raw