记录一次uefi劫持hyperV类型的外挂逆向
本文最后更新于 48 天前,其中的信息可能已经有所发展或是发生改变。

背景

  1. 内存挂断崖式下降,新型外挂往硬件层面靠拢(DMA、Kmbox、VT、uefi)

正文

提取作弊uefi程序

FE外挂为uefi劫持hyperV类型外挂,此类型外挂原理基本相同,详细原理可以参考开源的voyager项目。(但是本外挂的原理细节和voyager开源有极大区别,甚至可以认为FE外挂是voyager的超集)。

image-20240918184537633

因此第一件事需要先提取出来作弊的uefi,可以通过map uefi的ESP分区到X盘拿到,命令行如下(可以参考voyager)

mountvol X: /S

即可挂载ESP到X盘,X盘内容直接全部copy就行

image-20240919210126306

拿到后,diff原版文件,发现外挂修改了bootmgfw.efibootmgr.efi(可以通过签名有效性判断出来)

后者不用管,因为那是进PE的时候的bootloader,只看前者。

逻辑分析

拖入ida,发现他对原来的bootmgfw.efi加了壳,这一点和voyager就不一样,voyager是保存了原始文件的一个备份,fake-bootloader启动后,删除自身,然后加载back文件。

image-20240918184839650

加壳之后,外挂efi修改了原始的入口点,到cheater_entry

cheat_entry

  • check_init

这是作弊程序的入口点,他首先通过uefi运行时服务GetVariableSetVariable来获取几个变量

image-20240918185210629

首先进行了一次验证,验证函数由于不重要没有逆向,如果验证失败,则reset 机器。

image-20240919211914156

同时,这里通过BootService分配了一块EfiRuntimeServiceCode,这里要清楚,uefi runtime service内存是可以在os启动(ExitBootService)后仍然驻留。记住这个g_big_pool_buf,很重要哦!因为这是外挂在操作系统启动后还要驻留的payload。

  • pattern_find ImgFwStartBootApplication并hook

image-20240918185404662

image-20240918185524973

ImgFwStartBootApplication是原始bootmgrw的重要初始化函数,可以发现后续也有构造0xff 0x25来进行hook的操作。

  • hook BgpGxParseBitmap

这个作用是修改boot时候的展示,可以理解为机器启动boot的时候,向用户展示的GUI

image-20240918185849054

外挂hook这个,达到了启动的时候是一个巨大的狼头(外挂就叫狼头)。不得不说,还是很酷的。

  • hook EfiGetVarible,伪造安全启动

image-20240918190254039

image-20240918190319470

这个是干啥的呢,继续跟进这个函数

image-20240918190405196

这个其实是uefi的runtime service,也是会长期滞留在内存中,哪怕os已经启动了。

通过这个,可以获取uefi的一些固件信息,比如是否安全启动等。毫无疑问,这个外挂肯定是不能安全启动的,除非微软给他签名,这个证书是非常严格的,因此需要关闭安全启动。

因此他需要欺骗反作弊或者安全软件,就通过挂钩这个来达到目的。巧的是,测试电脑恰好装着vgk的boot驱动,测试发现,这个外挂完美兼容vgk.sys。

函数内具体逻辑比较简单,判断是否是在查询securebootenable,如果是,则返回true。

image-20240918191508724

image-20240918191519510

如果这样看起来比较复杂的话,我让chatgpt帮我优化了下代码,不得不说,o1模型逻辑推理能力简直强到爆炸。

#include <string.h>
#include <ctype.h>

unsigned __int64 filter_func(
    unsigned __int16 *name, // 名称字符串
    int *guid,              // GUID,数组形式
    unsigned __int64 *a3,   // 标志指针
    unsigned char *a4       // 结果指针
)
{
    unsigned __int64 ERROR_CODE_DEFAULT = 0x800000000000000E;
    unsigned __int64 ERROR_CODE_SPECIFIC = 0x8000000000000005;

    // 检查全局变量
    if (!byte_5C5E265)
        return ERROR_CODE_DEFAULT;

    // 将 GUID 组合成完整的 GUID 类型(伪代码,实际需根据具体实现)
    GUID variable_guid = make_guid(guid);

    // 定义要检查的变量名和对应的 GUID
    typedef struct {
        GUID guid;
        const char *variable_name;
    } VariableInfo;

    VariableInfo variables_to_check[] = {
        { EFI_GLOBAL_VARIABLE_GUID, "SecureBoot" },
        { EFI_GLOBAL_VARIABLE_GUID, "SetupMode" },
        { CUSTOM_GUID, "CustomMode" },
        { SB_CONFIG_STATE_GUID, "SbConfigState" },
    };

    // 遍历检查
    for (int i = 0; i < sizeof(variables_to_check) / sizeof(VariableInfo); i++)
    {
        if (compare_guid(variable_guid, variables_to_check[i].guid))
        {
            // 比较变量名(不区分大小写)
            if (_wcsicmp((wchar_t *)name, (wchar_t *)variables_to_check[i].variable_name) == 0)
            {
                if (*a3)
                {
                    // 根据变量名设置 *a4 的值
                    if (strcmp(variables_to_check[i].variable_name, "CustomMode") == 0 ||
                        strcmp(variables_to_check[i].variable_name, "SetupMode") == 0)
                    {
                        *a4 = 0;
                    }
                    else
                    {
                        *a4 = 1;
                    }
                    return 0; // 成功
                }
                else
                {
                    *a3 = 1;
                    return ERROR_CODE_SPECIFIC;
                }
            }
        }
    }

    // 未匹配,返回默认错误码
    return ERROR_CODE_DEFAULT;
}

可以看到,他修改了上面几个变量的返回逻辑,就绕过了os的安全启动检查。

  • 总结

这个cheat_entry做的事情比较少,主要就是预检查、绕过secure boot检测、hook重要函数ImgArchStartBootApplication

ImgArchStartBootApplication

按照顺序执行,接下来会执行这个函数,而这个函数已经被外挂hook了,所以看detour逻辑。

  • 查找winload必要导出函数

image-20240918193233136

image-20240918193248812

这里需要注意的是,image_base是winload.efi!这些函数后面都会用到,而且是winload.efi导出的,因此查找后保存。

  • 随机BIOS特征

这一段代码比较有意思,发现他通过特征定位到了BlUtlCopySmBiosTable函数。尝试搜索这个函数的资料,发现其介绍如下:

NTSTATUS
BlUtlCopySmBiosTable (
    __deref_out PVOID *SmBiosTableCopy
    );

/*++

Routine Description:

    This routine will attempt to find the SMBIOS table.  When successful,
    a buffer is allocated and the table is copied to the newly allocated
    buffer.

Arguments:

    SmBiosTableCopy - On successful output, contains the address of the
        allocated buffer containing the SMBIOS table.

Return Value:

    STATUS_SUCCESS when successful.
    STATUS_NOT_FOUND if the requested table could not be found.
    STATUS_NO_MEMORY if a memory allocation fails.
    STATUS_INVALID_PARAMETER if the table was corrupt.

--*/

SMBIOS table,搜了下:

image-20240918194058224

image-20240918193741905

发现外挂钩了这个函数,跟进detour函数看

image-20240918201559462

random_modify_smbios这个里面可以看到,他生成了一些随机的序列

image-20240918201626026

这个214013和2531011特征太明显了,是C的random函数,这个函数的大体逻辑就是随机了主板bios的一些序列号、制造商等,这个貌似是外挂在进行随机硬件序列的逻辑。

  • hook winload 的EfiGetVariable

image-20240918202649633

bootmgr已经hook了,hook这个来继续伪装SecureBoot等。

  • hook winload.BlLdrLoadImage

在函数最后,hook了winload.BlLdrLoadImage然后调用原始的g_ImgArchStartBootApplication

image-20240918203128381

BlLdrLoadImage函数是作弊程序的核心,里面有几千行伪c。

BlLdrLoadImage

这是一个位于winload.efi的函数,winload作用是加载所有的windows内核基础组件,比如ntoskrnl、hal.dll、hvloader.dll、hv.exe…同时结束uefi阶段,正式进入os。外挂hook这个地方作用不言而喻,此处也正是外挂最核心的逻辑。

  • 调用原始函数

image-20240919094937968

这里和因为需要调用原始函数,真正地获取一个新加载的镜像位置,所以先调用原始函数,在执行过滤、hook逻辑。

  • ntoskrnl

对ntoskrnl.exe的hook和操作,看着玩就行。说实话,看她写了一大堆ntoskrnl.exe的操作,感觉没什么关键信息,是一些辅助性的操作。所以随便看看

image-20240919095152597

对字符串解密,发现是ntoskrnl.exe,发现ntoskrnl被加载,外挂执行如下逻辑

image-20240919095401844

有一些全局变量比如g_hv_entrypoint_rva,第一次逆向可能没办法正确判断,其实这就是一个代码先后执行顺序的问题,hv.exe执行时机是早于ntoskrnl的。

这个do_some_hooks,发现它hook了几个比较奇怪的地方

image-20240923200619934

首先它hook的ntoskrnl.exe的启动

image-20240923200652979

save_something里面似乎也没啥好看的

image-20240923200713221

就是保存一些东西,这个地方的逻辑不用太关心,不是核心内容。

  • hvloader.dll

image-20240919101322188

发现是加载hvloader.dll,开始执行下面的逻辑

image-20240919101748780

查找hvloader.dll的text节,pattern find如下特征0x48 0x8b 0x51 0x10

image-20240919205914272

可以发现是位于hvloader.dll.HvlLaunchHypervisor

image-20240919205954212

image-20240919210328053

因为hvloader.dll都是没有符号表的,所以目前只能判断他hook了这个函数,而这个函数很简单:

image-20240919210501237

就是换了个cr3,没有太多逻辑,因为没有符号,a1 a2这两个参数完全不知道是干嘛的!只能通过逆向外挂来旁敲侧击a1 a2代表着什么。

detour函数如下:

image-20240919210602559

关键逻辑在reloc_hvpayload里面,a1、a2被传了进去,跟进去,可以发现一些端倪。

首先,他进行了非常多的页表操作,

image-20240919212820731

这个地方比较复杂,我们逐行分析,首先是

image-20240919213209425

可以看到,他获取了g_big_pool_buf的pteaddress,然后对其解引用,获得了pte.flags(pte表项) 对其 |=3,参考intel手册

image-20240919213638080

3开启了R/W和U/S位,接着去看rw_physical_mem逻辑

image-20240919214546047

这几行代码,竟然非常类似[原创]AXx-BASE里好玩的代码这个的实现!简而言之就是安全地读写物理内存。顺带着我发现了这个作者写的BUG,即他刷新了两次TLB,而第一次刷新的竟然是PteAddress,这显然不是必须的,可能外挂作者有些逻辑混乱了。

逆向完了rw_pa这个函数,可以看他的参数传递:

image-20240919215100149

这样看就很明显了,a1是个物理地址,而pte是外挂刚才set U/S R/W之后的属性,而1代表写入这块物理内存,同时写入的偏移位于页面内的0x7f8,写入的还是一个pte.

0x7f8,其实这个偏移非常特殊,如果它位于页表中,那么它位于页表4kb255个,也就是刚好位于内核态和用户态中间的Pte,这个非常特殊,同时,难道是否也表明了a1是一个物理地址,并且a1是一个页目录基址(cr3)?,如果是cr3,那么也只可能是hv.exe的cr3?这里先存下疑问,继续往下逆向。因为根据已有的信息,无法推理出确定性结论。

image-20240923185607164

继续往下看,发现它循环了128次,是从g_big_pool_buf开始,写入了一个很明显是Pxe Entry的东西。

现在我们知道了,g_big_pool_buf前0x1000字节就是一个PML4E那么很明显,现在正在填写这个PML4E指向的PDPTE

而这个offset是从g_big_pool_buf+0x1000字节开始的,也就是跳过了第一个PML4E,它设置该PMLE4(PDPT)指向的每一个PDPTE都是rwx,并且不是1Gb的页面,然后offset+=16,ok,现在拨开云雾了。我们得到了重要的结论

g_big_pool_bufg_big_pool_buf+0xfff是一个PML4E指向的PDPT

g_big_pool_buf+0x1000g_big_pool_buf+0x1fff 是每个PDPTE表项,其中PDPT指向他们。

其实到了这一步,有基础的同学应该可以猜出来,这一步和EPT 的身份映射非常像,即构造的第一个PML4E,构造2Mb大页,虚拟地址0就对应物理地址0,以此类推

而下面填充Pdpt操作,则彻底印证了我们的想法。正如同我们猜测,他正在做身份映射。只不过这个不算在EPT 页目录基址,而是在hv.exe的cr3中。

接着,外挂似乎做了一个迷惑性操作

image-20240923192209473

0x7FA000000000不让他去映射物理地址,请记住这个虚拟地址,非常重要!非常重要!非常重要!这里先按下不表,留个悬念。

总之,现在这块内存映射了g_big_pool_buf+0x83000开始的,大小为0xFFE000的地址

image-20240923192253109

接着外挂通过cpuid=0判断了AMD还是intel,从而去重定位不同的payload(此时payload已经解密,似乎hv.exe先于hvloader.dll加载,不然解释不通)

image-20240919210652920

我们看到了什么,是的,0x7FA000000000果然是伏笔,这个地址就是payload重定位的虚拟地址!(reloc函数逆向了下,就是非常基础的重定位pe,老生常谈的iat,重定位表等修复操作).剩下的就是重定位确定比如g_ept_violation等真正的地址。这几个全局变量,在外挂拦截hv.exe的时候会体现的,这下似乎基本上确定了,hv.exe先于hvloader.dll加载,然后后面hv.exe的地址会变?也就是一开始加载的hv.exe似乎是不对的,要不然这里他也不会进行重定位hv.exe了

外挂的这个函数重定位了payload,并插入了hv.exe的cr3的一块特殊地址里面,这里大胆推测下,hv.exe是否就是host的cr3?

说实话我真不太确定hv.exe在开启vtx是否其加载地址会变,但是想想理论上应该会变。这里我看了Voyager的实现,他的实现似乎聪明得多:

image-20240923195332375

它在hv.exe的后面新加了个section,这样就算hv.exe变化,也依然是可以找到新加的代码,也不需要去往hv.exe的cr3去插入payload了。如果以后要自己实现这个的话,我想我应该也会去这样实现。

而这里它进行了一堆映射物理地址,其实大概率就是用来读写内存的,只不过采用的物理地址读写

下面是解密payload的逻辑

其实外挂有N个payload,至少我逆向的时候,有3个以上,而且每个各司其职,非常复杂。

解密函数如下:

void __fastcall decrypt_payload(__int64 org_p, unsigned int len, __int64 dest_p, unsigned int offset)
{
    __int64 v4;  // rax
    __int64 v5;  // r10
    __int64 v6;  // rcx
    __int64 v7;  // r8
    char    v8;  // dl

    if (offset < len)
    {
        v4 = len;
        v5 = offset;
        if ((((_BYTE)len - (_BYTE)offset) & 1) != 0)
        {
            *(_BYTE*)(dest_p + offset) = *(_BYTE*)(org_p + offset) ^ (105 * offset - 85);
            v5                         = offset + 1i64;
        }
        if (len - 1i64 != offset)
        {
            v6 = org_p + 1;
            v7 = dest_p + 1;
            v8 = 105 * v5 - 85;
            do
            {
                *(_BYTE*)(v7 + v5 - 1) = v8 ^ *(_BYTE*)(v6 + v5 - 1);
                *(_BYTE*)(v7 + v5)     = *(_BYTE*)(v6 + v5) ^ (v8 + 105);
                v4 -= 2i64;
                v6 += 2i64;
                v7 += 2i64;
                v8 -= 46;
            } while (v5 != v4);
        }
    }
}

一个很简单的异或加密,解密这个payload,拖入ida,发现果然是一个pe格式的文件,而且还导出了几个函数:

image-20240919211219859

  • hv.exe

接下来到重头戏,外挂对于hv.exe的操作

image-20240923202724725

其实这个我们甚至可以不用看了,因为我们知道了hvloader.dll哪里其实就干了一件事情,而且很傻。

他拦截了hvloader去初始化host cr3,将自己的payload插入,同时去重定位了hv.exe,以及hv.exe的关键函数ept_violation_xxx

不过保持严谨,我们可以继续逆向,说不定还能发现这个外挂的骚操作。

首先定位到hv.exe的text节,进行pattern find,特征码是0xe8 ?? ?? ?? ?? 0x48 0x8d ?? ?? 0xb9 0x6 0x0 0x8

image-20240924094626851

image-20240923204456247

这个特征码,找到hv.exe尝试扫一扫

image-20240924094717179

本机上的hvix(intel平台,win11 23h2)的找到了,换个1909版本的

image-20240924094101906

1909的找到了,总之这个特征码普适性很强,点进去看下:

image-20240924094152277

什么都看不出来,因为hv.exe没有符号,所以看下外挂的逻辑吧,

image-20240924095355364

外挂使用rip+offset获取了那个函数地址,老生常谈的问题了,原理不再赘述。接着往下看

外挂紧接着执行了一次_cpuid(0),获取了当前cpu的型号

image-20240924095525799

因为我机器是intel的,所以amd暂且跳过,直接去看intel的逻辑。这说明刚才的那个特征码不仅全系统hv.exe通用,并且似乎amd的hv.exe和intel的hv.exe也是一样的。

外挂接着往下走,开始pattern find新的特征码

image-20240924095707686

这个特征码是0xe8 ?? ?? ?? ?? 0x48 0x89 0x4 0x24 0xe9,发现怎么都找不到,结果往上一看这个特征码还是amd的,发现逆向的有点错误,总之忽略这些细节

image-20240924101529096

AMD都不管,继续往下找intel的

image-20240924103142562

又是无聊的特征码,因为此时正式进入intel的相关操作,所以特征码一定都是和intel的hv.exe紧紧关联的,这里把第一个特征码称作intel_pattern_code1,特征码内容是0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? 0x41 0x83 0xBD

image-20240924103634179

这个特征码在最新版的win11是找到了的,但是1909版本没有找到,所以应该是比较新的hv.exe才有的,anyway,我们还是点进去看看上下文:

image-20240924103723186

发现神奇的东西,这很明显是一个switch case的结构,所以它switch的什么呢?或者是hv里面有哪些会用到switch case并且被外挂关心呢?其实写过vt的同学心里应该有答案,那就是vm-exit的exit reason,总之我们随便找个vt框架对下吧,看看能不能对的上。

image-20240924104618818

Ept Violation,0x30和0x31,难道真是这样,我们再确认下,随便找个 vm-exit-reason验证下,看下处理:

image-20240924110402082

image-20240924110343237

image-20240924110353436

毫无疑问,外挂找到了hv.exe的vm-exit,并且是ept_violation的相关处理。继续往下看

image-20240924110854449

发现外挂尝试查找第二个特征码(如果第一个找不到),

0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? ?? 8B ?? 83 ?? 0X1F

测试发现,这个最新版的windows还是找不到,总之1909我们找到了,这个估计是其他版本的。剩下的就不上ida逆向图了,简而言之,它搜索了四个特征码

intel hv.exe:
pattern_find_1:0XE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? 0X41 0X83 0XBD  #最新版windows hv.exe
pattern_find_2:0xE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? ?? 0X8B ?? 0X83 ?? 0X1F
pattern_find_3:0xE8 ?? ?? ?? ?? 0x83 0x3e 0x1b 0x74 0x08
pattern_find_4:0xE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? ?? 0X8B 0X4D ?? 0X48 0X33 0XCC #1909

image-20240924155243385

总之,最后全找到了对应的特征,不过这里很疑惑的是,我自己通过vmresume在1909的hv.exe找到的vm-exit和这个好像不太一样?

image-20240924155426313

上图是我自己找到的,这个很明显是在分发Vm Exit Reason,很奇怪,看起来位置是不一样,因此我尝试找到这两个函数的调用关系

image-20240924161138082

果然是有调用关系的,因此这里猜测,早期的hv.exe对于ept violation的处理是内联的,而不是一个call的形式,所以只能往更高层hook。

接下来,外挂解密自己的intel payload

image-20240924161255146

相关解密函数前面已经展示了,是一个非常简单的异或加密。前面一共用了4个不同的特征码,目前还不能确定到底定位到的是不是ept_violation_call,总之继续往下看,上下文全了肯定会有答案。

解密了payload,又开始进行特征码搜索。

image-20240924170006994


_____________解密payload_______________________________________
pattern_find_5:e8 ?? ?? ?? ?? 90 e9 ?? ?? ?? ?? 48 8d 4d 8f #最新版win11
pattern_find_6:e8 ?? ?? ?? ?? eb 24 83

image-20240924165703680

仍然是特征码定位,不过这次定位的刚好是ox30,看来刚才的那个并不是定位到了VMX_EXIT_REASON_EPT_VIOLATION。这个才是?如果这个没找到,则更换特征码为:

0xE8 ?? ?? ?? ?? E9 ?? ?? ?? ?? ?? 83 ?? 31

image-20240924170131947

上面那个特征最新版win11还能搜到,而这个我两个hv.exe都搜不到,继续往下逆:

image-20240924170248385

总之,解密后,又是四个特征

pattern_find_1575:0x8B ?? 0XE8 ?? ?? ?? ?? 0XE9 ?? ?? ?? ?? 0X83 ?? 0X31 
pattern_find_1645:0xe8 ?? ?? ?? ?? 0x90 0xe9 ?? ?? ?? ?? 0x48 0x8d 0x4d 0x8f #win11
pattern_find_1715:0xe8 ?? ?? ?? ?? 0xe9 ?? ?? ?? ?? 0x83 ?? 0x31
pattern_find_1779:0xe8 ?? ?? ?? ?? 0xeb 0x24 0x83 

奇怪的是,win10 1909上面四个我哪个也没找到。而这里他也进行了一次区分,分为找到和没找到,如果找到了,计算ept_violation_call,然后去执行hook逻辑

image-20240924173313481

image-20240924173402816

在这里,我们看到了熟悉的0x7FA00008A000i64,这个就是hvloader.dll插入的物理地址。

所以,到这里我们下了结论,这个地址就是用来装payload的。

外挂首先去定位了payload的导致函数,是按照序号导出的,第二个,接着去hv.exe里面搜索特征:

0xc3 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc

一眼看出来这是在寻找函数空隙,一般这样是为了间接跳转,不会破坏函数字节。

image-20240924173957980

果然,它的这块free space就是跳板,而这块free space位于call ept_violation的上下2GB之内。最终hook到我们自己的ept_violation_handler,即payload的第二个导出函数。

到这里,其实整个劫持hyperV的流程基本上结束了,还有一点,就是如果上面四个pattern_find都没有找到怎么办?

其实外挂也有处理:

image-20240924174521952

image-20240924174315920

这个特征码,我对比了下,发现和voyager的一模一样

pattern_find_1896:0xe8 ?? ?? ?? ?? 0xe9 ?? ?? 0xff 0xff 0x74 和voyager一样
pattern_find_1957:0x80 0xe1 0x0f 0x80 0xf9 0x3  //1909可以找到(从voyager后开始)

image-20240924182757646

可以发现,对于这种情况找到的特征码,hook到的函数是第三个导出函数,

image-20240924182851640

还记得我们前面说的吗,hv.exe 在最新版的windows下的EPT_VIOLATION是一个单独的函数处理的,因此可以直接hook这个单独的函数,但是低版本的windows,是内联到vm -exit中的,因此需要用payload的第三个导出函数来hook,这属于不同情况。

至此,我们基本上逆向完了整个bootmgfw.efi的流程,进一步,我们需要知道外挂具体是如何hook vm-exit-ept-violation的,因此开始逆向Payload的导出函数

Payload

作弊链路总结

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇