不常见的内存与流量取证 -- WMCTF 2022 1!5!
2022-9-15 23:45:49 Author: xz.aliyun.com(查看原文) 阅读量:29 收藏

刚刚拿到这道题的时候发现这道题目的内存镜像非常特殊,Misc 取证常用的软件诸如 取证大师 和 vol 都搞不定这种内存镜像。(当然因为我电脑不存在 python2 环境,所以导致我手上的 vol 实际是 vol3, 这还是蛮多区别的,所以我一开始以为我的 vol 锅了。后来问了问同队的师傅, 确实是 vol 和 取证都爆炸了 找不到 Profile 也不能确定类型)

所以面对如此棘手的一个问题,我想起了万年前看到一个神器,尸解 (Autopsy)。

当时我的师傅找了官方的 training 课程的优惠券 (其实就是因为疫情打折直接白送) 不过这是题外话了

官网为 https://www.autopsy.com 先进行一个装

windows 的版本更加阳间一点 但是我这边用的是 web 界面版本的

macOS 直接 brew install autopsy 就行

直接运行 autopsy 然后根据命令行输出打开 https://localhost:9999/autopsy

当然不出意外,就算是 “尸解” 确实也识别不出来镜像类型,如果可以识别和处理镜像类型, autopsy 将会变成一个更为强大的工具,某种意义上是真正的取证大师。(没有碰瓷的意思)

因此最后只能使用最基础的关键词搜索功能,当然这都是后话了。

题目附件快捷方式: https://github.com/wm-team/WMCTF2022

达到取证最高峰 - 流量

内存不是下来就能直接打开的 所以我们先 wireshark 看看流量了,可以看到前面是一堆 quic 流量,看起来是加了密的,可以留意一下。接着是明显的 HTTP 流量 GET 了一个 flag 同时升级协议到了 websocket。

接下来 都是客户机发往服务端的 WebSocket 流量,ws 第一个包里内容是 flag,然后接下来的包似乎不是明文,可以猜测是某种特殊的加密或者编码的内容,没那么简单。不过可以先保存一下。

把过滤条件设置为 websocket 进行分组导出 可以直接导出成 json 格式

因为我更加熟悉命令行操作所以进行如下的剪切
此外 命令 jq 是一款 json jq 解析器

cat tc.json|jq '.[]._source.layers."data-text-lines"' | awk "NR % 3 == 2"|cut -d "\"" -f 2

# 这时候得到的就是下面这样的密文
flag
SlAZT80ZTIXZTIcZSl9ZSlTZT80ZTIXZTIwC
Sx0ZTf1ZTIuZSx0ZSluZSthZTf1ZTIuZSxnC
SthZStQZSt1ZT81ZT8HZSlXZStQZSt1ZT8/C
Sl0ZSlQZSx6ZT8HZS46ZSlTZSlQZSx6ZT8gC
SxuZT86ZTIcZTfQZTfQZTfQZT86ZTIcZTfPC
TI0ZT81ZSx6ZT8HZSxcZSlhZT81ZSx6ZT8gC
SxXZTIXZTIhZSl0ZSxLZT80ZTIXZTIhZSlnC
SxHZT8HZSxkZSt1ZT8QZSt1ZT8HZSxkZSt/C
SxXZTIBZSl0ZSlBZSlQZT8XZTIBZSl0ZSlzC
StHZStQZSluZTIuZSl9ZSxkZStQZSluZTINC
SlXZSlkZSxrZS40ZSthZTIBZSlkZSxrZS4nC
TIuZT8QZS40ZT81ZTIXZTIXZT8QZS40ZT8/C
Sl6ZSx6ZSl9ZSthZT8QZTI9ZSx6ZSl9ZStGC
SxTZTIBZTIXZTf1ZT8XZSlkZTIBZTIXZTf/C
SlkZSlBZS4LZS46ZTI9ZSlQZSlBZS4LZS45C
T8AZSlAZSlhZSxhZSlTZS40ZSlAZSlhZSxGC
TIcZSxrZSlAZSl9ZSlhZSxhZSxrZSlAZSl3C
StHZStTZT8XZTI6ZSxkZS46ZStTZT8XZTI5C
SxTZT8QZT8XZS46ZSlhZSlQZT8QZT8XZS45C
TIcZSxrZS4LZStQZSlBZS4XZSxrZS4LZStPC
StHZSx6ZSluZT8TZSlQZSx0ZSx6ZSluZT8SC
SxuZTIXZSxcZTf1ZSluZSxkZTIXZSxcZTf/C
StAZSx6ZSl0ZSluZSl9ZT86ZSx6ZSl0ZSlNC
T81ZS40ZS4XZTIcZSt1ZTIBZS40ZS4XZTIwC
T8LZTIcZSxcZT86ZSxrZSl6ZTIcZSxcZT85C
StHZS4XZSlkZSluZT8HZS46ZS4XZSlkZSlNC
StQZTIuZSxhZSlAZSxcZSxBZTIuZSxhZSlbC
SlLZSt1ZSthZS4XZSl6ZTI9ZSt1ZSthZS4WC
Sl0ZT8TZStQZTfQZSxrZT86ZT8TZStQZTfPC

流量里基本就只有这些信息了

还是看看远处的内存吧 家人们

autopsy

跟着网上教程 直接创建案件 case

然后编写受害者信息 host

然后以链接方式导入 内存镜像

这样一个初始化的分析就完成了

内存 fake flag

既然他说内存里存在假 flag 那么我们大可搜索 WMCTF{ 这类字符串, 假 FLAG 多半是在测试的时候留下的内容,因此可以想到这类内容边上可能就是我们可以找的代码段,确实 我们可以在多处找到这个字符串,可以发现在 flag 出现的同时伴随着不少 Go 源码。我们可以找到相关的服务端收信逻辑。

如下

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gorila/websocket"
    "net/http"
)

var f1ags = "WMCTF{WebSOcket_And"
var upGrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}
//...flag
func flag (c *gin. Context) {
    //......get.........webSocket......
    ws,err := upGrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        return
    }
    defer ws.Close ()
    //.........flags.........
    for {
        //......ws.......
        mt, message, err := ws.ReadMessage()
        if err ! = nil {
            break
        }
        if string (message) == "flag" {
            //.....flags
            for i:= 0; i< len (flags) ; i++ {
                ch := string(flags[i])
                err : = ws. WriteMessage (mt, []byte (ch))
            //sleep......
            //time. Sleep (time. Second)
            if err != nil {
                break
            }
        }
    }
}
func main() {
    bindAddress := "localhost:2303"
    r := gin. Default ()
    r.GET("/flag", flag)
    r.Run(bindAddress)
}

不过只有一半的 FLAG 而且程序逻辑似乎和我们需要的逻辑并不相符。(这里看起来是服务端,而且没有我们需要的加密解密算法) 不过就这些地方已经可以看到一些端倪了

探寻 websocket 和 key

接下来有两条思路

  1. 搜索 websocket 关键字尝试寻找那段 web 通讯中的具体信息 肯定可以找到对应代码
  2. 搜索 key 关键字 尝试破解 QUIC 流量拿到其他的 key

在 autopsy 的 数据分析中的关键词搜索可以顺带获取对应字符串在镜像中的位置

联合使用关键字搜索(Keyword Search) 和基于 Offset (Data Unit)的搜索

你先会发现可能被破坏的内存区块,但是没关系,肯定有完整的。

有些会有 RgU.... 这种坏字符

接下来我们会大量看到 (除了上面 go 部分的代码) 诸如下面代码块的内容

another go

关键字 key

"fmt"
 "github.com/lucas-clemente/quic-go/http3"
 "log"
 "net/http"
 "os"
)

func HelloHTTP3Server(w http.ResponseWriter, req *http.Request) {
 fmt.Printf("client from : %s\n", req.RemoteAddr)
 fmt.Fprintf(w, "_HTTP3_1s_C000L}\n")
}

func main() {

 mux := http.NewServeMux()
 mux.Handle("/", http.HandlerFunc(HelloHTTP3Server))

 w := os.Stdout

 server := http3.Server{
  Addr: "127.0.0.1:18443",
  TLSConfig: &tls.Config{
   MinVersion:   tls.VersionTLS13,
   MaxVersion:   tls.VersionTLS13,
   KeyLogWriter: w,
  },
  Handler: mux,
 }

 err := server.ListenAndServeTLS("./my-tls.pem", "./my-tls-key.pem")
 if err != nil {
  log.Fatal(err)
 }
}
..........................L...............L...i............................my-tls.pem......-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUAuwgrK8T+kosTHW9KW11AvscB88wDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA3MTkwODUyNTFaFw0yNTA3
MTgwODUyNTFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQD62iNNKuGH54IDYqbg00gD/gbO9wq+UwmiYBXzYqnn
K9lTWvOEqlNvYNLhAoALcRrCkpqhw3ks/dhKqPCbDI3bxbQT3vZrvaRkP/DO1SnX
jmCt5yExDYXhPxNF+lWHs8TP7SjDE6sC6h+lEhYaQsKd/wYhw54NW/USrUR685r5
M1MfVg0+VOu5fqhwbOkn9lmwJaEOAtTIBAyG1jPFlt5LsBshe+2CXEG1cbaCDInB
6Jz6IZ7zN9KQ0YrWY8y2iw0toVODuNZnU7pSeKdWRwX6eYU3NA+QaTYl2zpl939b
jVtNKWlY+DiUFroTucph9W4jWvzu9Yp9uGEO46VvCV+tAgMBAAGjUzBRMB0GA1Ud
DgQWBBSzrkF13VZfMqO3s/1K1gogeETXdTAfBgNVHSMEGDAWgBSzrkF13VZfMqO3
s/1K1gogeETXdTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQD0
/EZXhjszp2KHnekh8Ktz66pIkxRa9ErZCbQt/7os4jCdj77OhcFYQ4O/mhdOeQb4
zvKlb0sAxsLcJiK1WB9cIcG+j4Kmrp6vJ8nRlI2YMBi8dX/MNDgBgXw/DdeuISyU
K05t26oQJxYfZ36zT2k2NVUdnvqAXTbk4IGxnfGRJJXZ/70iBWJYXEaB8UKeTXrn
VSefJKbO9v0CmuxWQxP363nB/e5f+l73ELTO3bs7qqyz9FHqZuR8cCo5YJ05c8G+
CLuL4JtyOX+7Cd+pGtadc54XtNYWw35CbBkHzhKwtU/+c24eM+SXV1AakzrSHHE3
1p80nnkmmO4f9yG5CZ8/
-----END CERTIFICATE-----
..........................L...............L.@..............................my-tls-key.pem......-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA+tojTSrhh+eCA2Km4NNIA/4GzvcKvlMJomAV82Kp5yvZU1rz
hKpTb2DS4QKAC3EawpKaocN5LP3YSqjwmwyN28W0E972a72kZD/wztUp145grech
MQ2F4T8TRfpVh7PEz+0owxOrAuofpRIWGkLCnf8GIcOeDVv1Eq1EevOa+TNTH1YN
PlTruX6ocGzpJ/ZZsCWhDgLUyAQMhtYzxZbeS7AbIXvtglxBtXG2ggyJweic+iGe
8zfSkNGK1mPMtosNLaFTg7jWZ1O6UninVkcF+nmFNzQPkGk2Jds6Zfd/W41bTSlp
WPg4lBa6E7nKYfVuI1r87vWKfbhhDuOlbwlfrQIDAQABAoIBAQD16xPgesFOcm7K
0tO2ZGqdP1N9YkJuAwnW3UunpnnZ3urXBLrmu/O/pLQXUlQk42TQith87RzGNrTr
vGLkHZKUeWTodhQt22RlwylYGzFB2Jp+4a9wX0l4YFWMrLVcq6euD1l+pLFp0gvj
z69LX1dbfL+OKi+v+Q5wmNwhjN/Im89qAxTHAKUlGQGy7cZq0aewVkF7qPrV44tA
4uUk2h36k+MFELUeDBAhegH6todAnjI+Ec72OzhtDDEF5hHM+1e3fsngz2RdfMQM
gnHm5fdb4yVGOV5K1HqVpDqKyCLIr0JvKNf/5HktJ/+lSlliL/mrx3KQCRt1DWN9
O8EBaMgBAoGBAP7GYPtzwXwn5bDmkD+//ejZm5SDq1EZ6ZSIttHl2OfbFwU45X0N
cMyuBXcHkiaVuD2GXiKmy5W4xh3WRPF4o7qMLe4dcUbTqqwc6nnY+2fLY71TMM9a
MjRQuQHnwQsMrCVYiv0/50eKwglc61ogsv+WmFxfYZtjnGMJP6M6xtlBAoGBAPwO
7iAQTNAZhrTefwDXSGirc1BVg0FBB05woVm/Gn0hkZqrl9VJd6g7gCAHh4tndR54
j5IR67eROoPY+tZSF8Gc/ne66BH2yq3Xbh3E281ajft+RLUFuD6k+Rlq2l91J72x
H46mKl/toB9ukPxl0P/8vOViXMYVlFsPHGnAEB9tAoGBAJ/Op36SOUc7b2Pq+4hB
UW8BMAmUHZ2dd1pn9uTqG4gzcNkhuzEZgSuh7GOhKBdzykEtS1bI8OJVKFAG2u/s
ECcvTpARf8BBfMjAyoLri6areUCEMhWeKeeOyr1bNUdNB53VUDlSICxL6TIeSrIZ
2K1hNOicG4lwjePBJV2pvJkBAoGAVERRi9qnM3M1O8aewxM2G/glxxevl+M7pBe3
eZ+QJYFRgloXmrDDFjU+MncR86MU3qkDppvjKC2fWHDz+y7azlnEIRcVetv9Cn1Z
TQ6BRXgeu5ONOM++twLEXKECfKNYM+zBVhlrVULGI3v9cMRBSTOfmzh1N6wDOyYk
I56YRUkCgYBhPvpgPohOzEukToo64UtbmOK25pKJBHfhMH/jGglfFUnzAURXPC4b
7HIxMSGd5A0EjX34M7CA4rRHXcF7Sxc6X4eP43FoZabey6di+rIvm6F/pQFRTT/u
uSNIJWzf9lF5rbqKMxPlHVJLvlnIVZNSjUV2FIdCe4LR55qzByOOvQ==
-----END RSA PRIVATE KEY-----

这里有 key 也能看到似乎是后半个 flag 的变量

JS

var ws = new WebSocket("ws://localhost:2303/flag");  

ws.onopen = function(evt) {  
    console.log("Connection open ...");  
    ws.send("flag");  
};  

ws.onmessage = function(evt) {  
    var rstr = randomString(5)  
    n = evt.data  
    res = n.padEnd(9,rstr)  
    s1= encrypto(res,15,25)  
    f1 = b1.encode(s1)  
    ws.send(f1)  
    console.log('Connection Send:'+f1)  
};  

ws.onclose = function(evt) {  
    console.log("Connection closed.");  
};  


function Encode() {  

  _keyStr = "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC";

这里基本已经可以看到 加密的逻辑了 基本可以猜测我们需要的内容就在这里

找到完整的代码可以试试用关键字 _keyStr

接下来摸到相关的内存 Unit 号之后 在该数字后面稍微减少 10 左右

然后使用 Data Unit 分析附近内存号 ± 20 个内存单元的内容

接着我们可以摸到如下的代码 大约在 内存编号 987382 处

<script>  

function randomString(e) {  
    e = e || 32  
    var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",  
    a = t.length,  
    n = "";  
    for (i = 0; i < e; i++) n += t.charAt(Math.floor(Math.random() * a));  
    return n  
}  


function encrypto( str, xor, hex ) {  
  if ( typeof str !== 'string' || typeof xor !== 'number' || typeof hex !== 'number') {  
    return;  
  }  

  let resultList = [];  
  hex = hex <= 25 ? hex : hex % 25;  

  for ( let i=0; i<str.length; i++ ) {  
    let charCode = str.charCodeAt(i);  
    charCode = (charCode * 1) ^ xor;  
    charCode = charCode.toString(hex);  
    resultList.push(charCode);  
  }  

  let splitStr = String.fromCharCode(hex + 97);  
  let resultStr = resultList.join( splitStr );  
  return resultStr;  
}  

var b1 = new Encode()  

var ws = new WebSocket("ws://localhost:2303/flag");  

ws.onopen = function(evt) {  
    console.log("Connection open ...");  
    ws.send("flag");  
};  

ws.onmessage = function(evt) {  
    var rstr = randomString(5)  
    n = evt.data  
    res = n.padEnd(9,rstr)  
    s1= encrypto(res,15,25)  
    f1 = b1.encode(s1)  
    ws.send(f1)  
    console.log('Connection Send:'+f1)  
};  

ws.onclose = function(evt) {  
    console.log("Connection closed.");  
};  


function Encode() {  

  _keyStr = "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC";  

  this.encode = function (input) {  
    var output = "";  
    var chr1, chr2, chr3, enc1, enc2, enc3, enc4;  
    var i = 0;  
    input = _utf8_encode(input);  
    while (i < input.length) {  
      chr1 = input.charCodeAt(i++);  
      chr2 = input.charCodeAt(i++);  
      chr3 = input.charCodeAt(i++);  
      enc1 = chr1 >> 2;  
      enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);  
      enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);  
      enc4 = chr3 & 63;  
      if (isNaN(chr2)) {  
        enc3 = enc4 = 64;  
      } else if (isNaN(chr3)) {  
        enc4 = 64;  
      }  
      output = output +  
      _keyStr.charAt(enc1) + _keyStr.charAt(enc2) +  
      _keyStr.charAt(enc3) + _keyStr.charAt(enc4);  
    }  
    return output;  
  }  


  _utf8_encode = function (string) {  
    string = string.replace(/\r\n/g,"\n");  
    var utftext = "";  
    for (var n = 0; n < string.length; n++) {  
      var c = string.charCodeAt(n);  
      if (c < 128) {  
        utftext += String.fromCharCode(c);  
      } else if((c > 127) && (c < 2048)) {  
        utftext += String.fromCharCode((c >> 6) | 192);  
        utftext += String.fromCharCode((c & 63) | 128);  
      } else {  
        utftext += String.fromCharCode((c >> 12) | 224);  
        utftext += String.fromCharCode(((c >> 6) & 63) | 128);  
        utftext += String.fromCharCode((c & 63) | 128);  
      }  

    }  
    return utftext;  
  }  

}  
</script>

逆向算法

这里我写了一个小小的 Decode 就交给队里的逆向大师傅了

我的 Decode

// encrypto rev
function decrypto(str ,xor ,hex) {
    console.log("decrypto_get:","str: " + str, xor,hex)
    let splitStr = String.fromCharCode(hex + 97);
    resultStr = str.split(splitStr);
    console.log(resultStr)
    resultList = []
    for(let i = 0; i<resultStr.length; i++) {
        charCode = resultStr[i];
        char = parseInt(charCode,hex);
        char2 = (char ^ xor);
        str = String.fromCharCode(char2);
        resultList.push(str);
    }
    text = resultList.join("")
    console.log("decrypto_return: " + text)
    return text
}

大师傅给出来的结果

this.decode = function(input) {
        // input = String(input)
        //  .replace(REGEX_SPACE_CHARACTERS, '');
        var length = input.length;
        if (length % 4 == 0) {
            input = input.replace(/==?$/, '');
            length = input.length;
        }
        if (
            length % 4 == 1 ||
            // http://whatwg.org/C#alphanumeric-ascii-characters
            /[^+a-zA-Z0-9/]/.test(input)
        ) {
            error(
                'Invalid character: the string to be decoded is not correctly encoded.'
            );
        }
        var bitCounter = 0;
        var bitStorage;
        var buffer;
        var output = '';
        var position = -1;
        while (++position < length) {
            buffer = _keyStr.indexOf(input.charAt(position));
            bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
            // Unless this is the first of a group of 4 characters…
            if (bitCounter++ % 4) {
                // …convert the first 8 bits to a single ASCII character.
                output += String.fromCharCode(
                    0xFF & bitStorage >> (-2 * bitCounter & 6)
                );
            }
        }
        return output;
    };

合作非常愉快

队里的 RE 大师傅用了一会儿就搞定了,搞定之后直接顺手就解出结果了。

什么叫术业有专攻啊 战术后仰 (x

结果

WdzsPXdzs
MrtMmCrtM
CBDkfSBDk
TYKf4XYKf
Fbspppbsp
{kKfEZkKf
LzaTNdzaT
OfGDiDfGD
LxTQYcxTQ
_BmtPGBmt
SnH6CxnH6
ti6kzzi6k
RKPCiwKPC
1xzrcnxzr
nQ74wYQ74
gWZ3X6WZ3
sHWPZ3HWP
_AcyG4Acy
1ic4ZYic4
sH7BQ5H7B
_KmhYMKmh
FzErmGzEr
@KTmPbKTm
k65sDx65s
esEbHRsEb
_5nmf45nm
Bt3WEJt3W
UDC5RwDC5
ThBpHbhBp

==> WMCTF{[email protected]_BUT

BUT 后面明显还有后半句话 猜想是 一半 flag 的藏头

想到之前还存在后半段 Flag 在内存中 进行一个拼接后提交

WMCTF{[email protected]_BUT_HTTP3_1s_C000L}

虽然说有一点点脑洞但是基本题目逻辑是清晰的。可以说是出的很好的一道 misc。

misc 不是套娃捏 misc == 套娃 的坏毛病建议改改

赛后在和队友和其他队伍的成员交流的时候, 发现大家都基本在使用 VOL 取证大师 strings或者一些二进制编辑器,甚至听说有用 WinHex 嗯做来解这道题的。相反对于一些国外的优秀工具了解并不是很多,于是我一合计就有了这篇文章。

在 Autopsy 的帮助下,我基本不需要担心搜素字符串不全或者很慢的问题,这些内容都交给工具自动处理了。Autopsy 不仅非常的贴心的在关键字的搜索中有专门的正则搜索,而且还会额外帮你匹配大小写不敏感的搜索,也可以匹配到诸如 S\x00T\x00R\x00I\x00N\x00G\x00 的字符串。所以我的脑袋基本聚集于根据获取的结果进一步推理相关内容去解题的过程中。

此外 Windows 版本使用流程和 web 版本的基本一致,如果你是使用该版本来解决这道题,你可以直接提取出来镜像里所有的文件。关键字搜索也很方便,他可以直接识别出来 go 文件。但是定位代码相关上下文就非常的困难,就是 data unit 相关的功能有一点点欠缺,不过问题不大。


文章来源: https://xz.aliyun.com/t/11699
如有侵权请联系:admin#unsafe.sh