shellcode逆向基础知识学习记录--[HDCTF 2023]double_code
一.新知识:shellcode
定义:
Shellcode 是一段精炼的、用作有效载荷(Payload) 的机器代码。它之所以叫这个名字,是因为最初这类代码的唯一目的就是启动一个命令行 Shell(例如 /bin/sh),从而让攻击者能够控制被入侵的机器。
如今,这个术语的含义已经扩展,泛指任何被注入到漏洞利用程序(Exploit)中并执行的机器代码,其功能不再局限于获取 Shell,也可以是执行其他操作,如创建用户、下载文件、反弹连接等。
最早的shellcode:
shellcode:
xor eax, eax
push eax ; NULL终止符
push 0x68732f6e ; "n/sh"
push 0x69622f ; "/bi"
mov ebx, esp ; "/bin/sh"地址
mov ecx, eax ; NULL参数
mov edx, eax ; NULL环境变量
mov al, 0xb ; execve系统调用号
int 0x80 ; 触发系统调用
以上都是官方话,我认为通俗理解就是一个能够直接攻击软件的病毒代码。或许,你可以联想到我们的系统命令行,叫shell。这里说的shellcode函数,就是在程序运行时直接通过系统命令或者恶意攻击代码攻击软件的代码部分。而先加密,运行时解密,就是逆向中shellcode的典型特征。
特点总结:位置无关,自包含,自解密 ,最终执行恶意代码。
还有一部分简略笔记我放在下面,需要可自取
https://www.yuque.com/g/memory-luvcr/whgrxl/ohkrekw4u5rtunyv/collaborator/join?token=Dn0BlpZGb8Wf2Zvi&source=doc_collaborator# 《shellcode》
二.[HDCTF 2023]double_code例题复现
做题老规矩先查壳,再放进ida,这里不再过多赘述了。进来就能看见真正的主函数main_0
int __fastcall main_0(int argc, const char **argv, const char **envp)
{
char *v3; // rdi
__int64 i; // rcx
DWORD LastError; // eax
DWORD v6; // eax
DWORD v7; // eax
DWORD v8; // eax
DWORD v9; // eax
DWORD v10; // eax
char v12; // [rsp+40h] [rbp+0h] BYREF
_QWORD v13[17]; // [rsp+50h] [rbp+10h] BYREF
HANDLE hSnapshot; // [rsp+D8h] [rbp+98h]
PROCESSENTRY32W pe; // [rsp+100h] [rbp+C0h] BYREF
BOOL v16; // [rsp+354h] [rbp+314h]
DWORD th32ProcessID; // [rsp+374h] [rbp+334h]
__int64 v18; // [rsp+398h] [rbp+358h]
__int64 v19; // [rsp+3B8h] [rbp+378h]
char v22; // [rsp+594h] [rbp+554h]
v3 = &v12;
for ( i = 254; i; --i )
{
*(_DWORD *)v3 = -858993460;
v3 += 4;
}
v22 = 0;
j___CheckForDebuggerJustMyCode(&unk_14002401B, argv, envp);
sub_140011195("alloc:%p\n", sub_14001F000);
memset(v13, 0, 0x70u);
v13[0] = 0x4000000070LL;
v13[2] = L"open";
v13[3] = L"notepad.exe";
sub_1400110FA(v13);
hSnapshot = j_CreateToolhelp32Snapshot(2u, 0);
if ( hSnapshot == (HANDLE)-1LL )
{
LastError = GetLastError();
sub_140011195("CreateToolhelp32Snapshot:%d\n", LastError);
}
pe.dwSize = 568;
v16 = j_Process32FirstW(hSnapshot, &pe);
if ( !v16 )
{
v6 = GetLastError();
sub_140011195("Process32First:%d\n", v6);
}
while ( v16 )
{
if ( !wcscmp(pe.szExeFile, L"notepad.exe") )
{
v22 = 1;
th32ProcessID = pe.th32ProcessID;
break;
}
v16 = j_Process32NextW(hSnapshot, &pe);
}
v18 = qword_14001F368(0x1FFFFF, 0, th32ProcessID);
if ( !v18 )
{
v7 = GetLastError();
sub_140011195("\nopenprocess error%d\n", v7);
}
sub_140011195("pid:%d", th32ProcessID);
v19 = qword_14001F370(v18, 0, 405, 4096, 64);
if ( !v19 )
{
v8 = GetLastError();
sub_140011195("VirtualAllocEx error%d\n", v8);
}
if ( !(unsigned int)qword_14001F360(v18, v19, sub_14001F000, 405, 0) )
{
v9 = GetLastError();
sub_140011195("WriteProcessMemory:%d\n", v9);
}
if ( !qword_14001F378(v18, 0, 0, v19, 0, 0, 0) )
{
v10 = GetLastError();
sub_140011195("CreateRemoteThread:%d\n", v10);
}
return 0;
}
可以尝试分析一下代码,就会看到这个程序大致的逻辑就是先打开进程,然后写入shellcode,再远线程执行。具体请看后续代码分析-->
我们这里找到shellcode函数是对应的sub_14001F000,那查看一下sub_14001F000具体代码吧
__int64 sub_14001F000()
{
__int64 v0; // rdx
__int64 v1; // rcx
__int64 v2; // r8
__int64 v3; // r9
int v4; // esp
unsigned __int64 v5; // rax
int v6; // esp
unsigned __int64 v7; // rax
int v8; // esp
unsigned __int64 v9; // rax
int v10; // esp
unsigned __int64 v11; // rax
int v12; // esp
unsigned __int64 v13; // rax
_BYTE v15[57]; // [rsp+1Fh] [rbp-41h] BYREF
int v16; // [rsp+58h] [rbp-8h]
signed int i; // [rsp+5Ch] [rbp-4h]
MEMORY[0x140029C60]();
*(_DWORD *)&v15[37] = 1;
*(_DWORD *)&v15[41] = 5;
*(_DWORD *)&v15[45] = 2;
*(_DWORD *)&v15[49] = 4;
*(_DWORD *)&v15[53] = 3;
strcpy(v15, "************************************");
for ( i = 0;
(unsigned int)MEMORY[0x14003A250](v1, v0, v2, v3, *(_QWORD *)&v15[1], *(_QWORD *)&v15[9], *(_QWORD *)&v15[17]) > i;
++i )
{
v0 = (unsigned int)(i / 5);
v1 = (unsigned int)(i % 5);
v16 = i % 5;
if ( i % 5 == 1 )
{
v5 = (unsigned int)(v4 + 31 + i);
v1 = *(unsigned __int8 *)v5 ^ 0x23u;
v0 = (unsigned int)(v4 + 31);
*(_BYTE *)(unsigned int)(v0 + i) = *(_BYTE *)v5 ^ 0x23;
}
else
{
switch ( v16 )
{
case 2:
v7 = (unsigned int)(v6 + 31 + i);
v0 = (unsigned int)*(unsigned __int8 *)v7 + 2;
v1 = (unsigned int)(v6 + 31);
*(_BYTE *)(unsigned int)(v1 + i) = *(_BYTE *)v7 + 2;
break;
case 3:
v9 = (unsigned int)(v8 + 31 + i);
v0 = (unsigned int)*(unsigned __int8 *)v9 - 3;
v1 = (unsigned int)(v8 + 31);
*(_BYTE *)(unsigned int)(v1 + i) = *(_BYTE *)v9 - 3;
break;
case 4:
v11 = (unsigned int)(v10 + 31 + i);
v0 = (unsigned int)*(unsigned __int8 *)v11 - 4;
v1 = (unsigned int)(v10 + 31);
*(_BYTE *)(unsigned int)(v1 + i) = *(_BYTE *)v11 - 4;
break;
case 5:
v13 = (unsigned int)(v12 + 31 + i);
v0 = (unsigned int)*(unsigned __int8 *)v13 - 25;
v1 = (unsigned int)(v12 + 31);
*(_BYTE *)(unsigned int)(v1 + i) = *(_BYTE *)v13 - 25;
break;
}
}
}
return 0;
}
这里你在ida中可以看到爆红部分,此刻我们第一时间想到这里的代码肯定是被隐藏的,或许含有花指令等。

怀疑是花指令就可以看一下汇编代码,很明显这里没有花指令特征,数据代码也没有被损坏。

那我们已了解到shellcode函数可能会在Linux系统中实现,那会不会这里的代码就是32位Linux系统中的写法,放在ida里反编译时出现混乱。那我们就需要通过十六进制文本恢复这部分代码了,提取文本时需要对应地址,这个具体对应到哪是需要根据汇编代码的地址大致算一下的。

然后把这部分数据复制到010editor中保存下来:

保存之后再放到32位ida里去看。
为什么要把这部分提取的数据放到32位ida中?
是因为,我们shellcode是对32位Linux系统中编写的代码,所以自然要通过32位的ida工具解读。
此时,还无法查看反编译的代码。去新建立函数就好了,还记得我们的命令吗---P

之后,就变成了这样:

现在就可以f5了,这就是我们还原出来的代码。
int sub_0()
{
unsigned int v0; // eax
char v2[41]; // [esp+1Fh] [ebp-41h] BYREF
int v3; // [esp+48h] [ebp-18h]
int v4; // [esp+4Ch] [ebp-14h]
int v5; // [esp+50h] [ebp-10h]
int v6; // [esp+54h] [ebp-Ch]
int v7; // [esp+58h] [ebp-8h]
int i; // [esp+5Ch] [ebp-4h]
MEMORY[0xAC60]();
*(_DWORD *)&v2[37] = 1;
v3 = 5;
v4 = 2;
v5 = 4;
v6 = 3;
strcpy(v2, "************************************");
for ( i = 0; ; ++i )
{
v0 = MEMORY[0x1B250](v2);
if ( v0 <= i )
break;
v7 = i % 5;
if ( i % 5 == 1 )
{
v2[i] ^= 0x23u;
}
else
{
switch ( v7 )
{
case 2:
v2[i] += 2;
break;
case 3:
v2[i] -= 3;
break;
case 4:
v2[i] -= 4;
break;
case 5:
v2[i] -= 25;
break;
}
}
}
return 0;
}
当然,根据主程序的逻辑解读,我们还能知道flag信息是在题目压缩包中的output文件中

文件内容是一串十六进制的字节,也符合shellcode最后生成的攻击代码。所以基本上信息我们都分析出来了,那就开始写脚本吧。
flag=[0x48,0x67,0x45,0x51,0x42,0x7b,0x70,0x6a,0x30,0x68,0x6c,0x60,0x32,0x61,0x61,0x5f,0x42,0x70,0x61,0x5b,0x30,0x53,0x65,0x6c,0x60,0x65,0x7c,0x63,0x69,0x2d,0x5f,0x46,0x35,0x70,0x75,0x7d]
deflag=''
for i in range(len(flag)):
if(i%5==1):
deflag+=chr(flag[i]^0x23)
if(i%5==2):
deflag+=chr(flag[i]-2)
if(i%5==3):
deflag+=chr(flag[i]+3)
if(i%5==4):
deflag+=chr(flag[i]+4)
if(i%5==5):
deflag+=chr(flag[i]+25)
if(i%5==0):
deflag+=chr(flag[i])
print(deflag)
解答出来的flag就是HDCTF{Sh3llC0de_and_0pcode_al1_e3sy}
其实我一开始学的是C,大一现在还没学到python,但为了写解密脚本也算是生生通过解题把python学了个大概。好吧,当我自己写出来第一个脚本的时候是很开心的^^
三.shellcode对应解读
主函数中明显的进程代码:
// 1. 打开目标进程
qword_14001F368(0x1FFFFF, 0, th32ProcessID); // OpenProcess
// 2. 在远程进程分配内存
qword_14001F370(v18, 0, 405, 4096, 64); // VirtualAllocEx
// 3. 写入shellcode
qword_14001F360(v18, v19, sub_14001F000, 405, 0); // WriteProcessMemory
// 4. 创建远程线程执行
qword_14001F378(v18, 0, 0, v19, 0, 0, 0); // CreateRemoteThread
完整的攻击链:
主程序(main_0)
│
├─ 查找notepad.exe
│
├─ 打开进程(OpenProcess)
│
├─ 分配内存(VirtualAllocEx)
│
├─ 写入shellcode(WriteProcessMemory)
│ └─ sub_14001F000 (加密状态)
│ ├─ "********************" (36个星号)
│ └─ 解密密钥 [1,5,2,4,3]
│
├─ 远程线程执行(CreateRemoteThread)
│ └─ notepad.exe进程中
│ └─ shellcode开始自解密
│ ├─ 循环36次
│ ├─ 根据位置应用5种变换
│ └─ 还原真实恶意代码
│
└─ 真实payload执行
├─ PEB遍历解析API
├─ 下载更多恶意软件
└─ 建立C2连接
最后,解释一下opcode的意思,是操作码。
OK,今天就到这里吧,我依旧争取每天学一样新的值得记录的写下来!寒假也快结束了,更不能懈怠喽
更多推荐


所有评论(0)