吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1597|回复: 5
收起左侧

[Windows] c++控制台模拟web中的命令注入漏洞(含视频)

[复制链接]
小菜鸟一枚 发表于 2026-6-27 21:16

第一部分:编写一个“密码和提示语都是拼出来”的程序

同学们好,欢迎回来,我是小菜鸟,继续练习讲课找工作,有岗位的坛友帮忙内推一下吧,谢谢。

今天主要是演示逆向工程中一个核心的对抗手段:隐藏关键信息,另外,用c++模拟演示web中的命令注入漏洞。

我们将编写一个程序,它的登录密码和所有提示语,都不是以明文形式存在的——它们被拆成了一个个字节,只有在运行时才会被拼回原样。而我们的任务,就是用IDA把这个“拼图”过程还原出来。

你会发现:在逆向工程面前,没有绝对的“隐藏”,只有“藏得深不深”。

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;

// ============================================================
// 1. 提示语:全部转为GBK十六进制码(Windows 7默认编码)
// ============================================================
// "请输入管理员密码" 的GBK编码:
// 请=C7 EB, 输=CA E4, 入=C8 EB, 管=B9 DC, 理=C0 ED, 员=D4 B1, 密=C3 DC, 码=C2 EB
const unsigned char msg_prompt[] = {0xC7, 0xEB, 0xCA, 0xE4, 0xC8, 0xEB, 0xB9, 0xDC, 0xC0, 0xED, 0xD4, 0xB1, 0xC3, 0xDC, 0xC2, 0xEB, 0x00};

// "登录成功" 的GBK编码:
// 登=B5 C7, 录=C2 BC, 成=B3 C9, 功=B9 A6
const unsigned char msg_success[] = {0xB5, 0xC7, 0xC2, 0xBC, 0xB3, 0xC9, 0xB9, 0xA6, 0x00};

// "密码错误" 的GBK编码:
// 密=C3 DC, 码=C2 EB, 错=B4 ED, 误=CE F3
const unsigned char msg_error[] = {0xC3, 0xDC, 0xC2, 0xEB, 0xB4, 0xED, 0xCE, 0xF3, 0x00};

// ============================================================
// 2. 密码:每个字符的ASCII码分散存放
// ============================================================
const char p1 = 0x32;  // '2'
const char p2 = 0x30;  // '0'
const char p3 = 0x32;  // '2'
const char p4 = 0x34;  // '4'

// ============================================================
// 3. 解码函数:把GBK字节数组拼回汉字字符串
// ============================================================
string decodeGBK(const unsigned char* bytes) {
    string result;
    for (int i = 0; bytes[i] != 0; i++) {
        result += bytes[i];
    }
    return result;
}

string getSecretPassword() {
    string pwd;
    pwd += p1;
    pwd += p2;
    pwd += p3;
    pwd += p4;
    return pwd;
}

// ============================================================
// 4. 登录函数
// ============================================================
bool login() {
    string input_pwd;
    cout << decodeGBK(msg_prompt) << ": ";   // 运行时才拼出 "请输入管理员密码"
    cin >> input_pwd;

    if (input_pwd == getSecretPassword()) {
        cout << decodeGBK(msg_success) << endl;  // 运行时拼出 "登录成功"
        return true;
    } else {
        cout << decodeGBK(msg_error) << endl;    // 运行时拼出 "密码错误"
        return false;
    }
}

// ============================================================
// 5. 命令注入漏洞函数
// ============================================================
void pingHost() {
    string ip;
    cout << "请输入要ping的IP地址: ";
    cin >> ip;
    string command = "ping -n 4 " + ip;      // Windows 7 环境
    cout << "[执行命令] " << command << endl;
    system(command.c_str());
}

int main() {
    cout << "========== 网络管理工具 ==========" << endl;

    if (!login()) {
        cout << "登录失败,程序退出。" << endl;
        return -1;
    }

    while (true) {
        cout << "\n请选择功能: " << endl;
        cout << "1. Ping IP地址" << endl;
        cout << "2. 退出" << endl;
        cout << "请输入选项: ";
        int choice;
        cin >> choice;

        if (choice == 1) {
            pingHost();
        } else if (choice == 2) {
            break;
        } else {
            cout << "无效选项,请重新输入。" << endl;
        }
    }
    return 0;
}

同学们,我们平时在屏幕上看到的汉字,在计算机内部其实都是一串数字。Windows 7 用的是 GBK 编码,每个汉字占 2 个字节。我们把这串十六进制码存到程序里,程序运行的时候再用 decodeGBK 函数把它拼回去。

为什么要这样做呢?因为如果直接把汉字写进程序里,在 IDA 的字符串窗口中就一目了然,攻击者很容易定位到关键逻辑。我们把这个编码拆散存放,就是给逆向分析增加一点干扰——虽然不能完全阻止,但能让分析的过程变得更曲折一些。”

同学们,如果这个程序是你写的,并且你把它发布了出去,你能保证别人找不到密码吗?答案是:不能。因为逆向工程可以绕过一切“表面”的隐藏。

第二部分:攻击手法一:IDA逆向分析,从内存中找线索

同学们,我们还是使用IDA工具加载exe程序,然后找到main函数,看到关键函数login:

call    ___main
mov     dword ptr [esp+4], offset asc_489034 ; "========== "
mov     dword ptr [esp], offset __ZSt4cout ; std::ostream::sentry *
call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
mov     dword ptr [esp], offset __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ; std::endl<char,std::char_traits<char>>(std::ostream &)
mov     ecx, eax
call    __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
sub     esp, 4
call    __Z5loginv      ; login(void)
xor     eax, 1
test    al, al
jz      short loc_401B19

同学们,看这里。call login 执行完后,al 里存的是登录结果。但这里不是直接test al, al:它先用 xor eax, 1,把登录结果取反了,相当于 if (!login())。

接下来可以看到他有两个分支,如果看函数地址和十六进制码就按一下空格键(我一般用来看函数地址用),如果要看这种带分支的代码,再按一下空格键,这里可以看到三种颜色箭头。

绿色箭头:条件跳转(成功/成立时跳转)
含义:表示条件成立时(例如 jz、je、jnz、jg 等指令),程序会沿着这条线执行。

示例:如果比较结果相等,jz 跳转,走绿色线;否则继续执行下一条指令。

红色箭头:条件跳转(失败/不成立时跳转)
含义:表示条件不成立时(即条件跳转指令的“失败”分支),程序会沿着这条线继续执行到下一条指令。

注意:这里的“红色”分支通常是条件不满足时继续执行的路径,而不是跳转目标。

蓝色箭头:无条件跳转
含义:表示一定会跳转(例如 jmp 指令),没有条件判断,执行到此处必定走这条线。

这些汇编指令不记得也没关系,遇到就去查,见多了就记得了,关于ida对自定义函数重命名的事,补充一下相关规则。

组成部分 实际内容 含义
__Z 固定前缀 C++ 编译器的标准名称修饰前缀(部分编译器使用 _Z)
5 数字 5 表示接下来的函数名长度是 5 个字符
login 函数名 实际的函数名,正好 5 个字符(l-o-g-i-n)
v 参数类型缩写 表示 void,即该函数不接受任何参数

因为这里核心是分析登录函数,要找出真实的密码,所以先进入login函数内部看看,调用decodeGBK函数,将返回的字符串“请输入管理员密码:”输出到控制台。

lea     eax, [ebp+var_28]          ; 接收返回值
mov     dword ptr [esp+4], offset __ZL10msg_prompt ; 压入第二个参数
mov     [esp], eax                ; 压入第一个参数(返回值地址)
mov     [ebp+fctx.call_site], 1   ; 
call    __Z9decodeGBKPKh          ; 调用 decodeGBK

lea     eax, [ebp+var_28]                ; 取解码后的字符串地址
mov     [esp+4], eax                     ; 作为第二个参数压栈
mov     dword ptr [esp], offset __ZSt4cout ; 压入 cout 对象
call    __ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E ; 10. 输出字符串

同学们,这就是GBK编码还原中文字符串的过程,中文字符按照GBK编码,占2 个字节,把它拼回去,就能输出成汉字了。

隐藏字符串只是增加了分析者的工作量,但并不能阻止逆向工程。在逆向分析中,数据流追踪(从数据定义到函数调用)比直接搜索字符串更可靠。

接下来继续往下看,这里是因为程序是我们自己写的,所以是顺序看,常规方法是通过分支找关键跳-然后向上翻,找到关键函数,解密关键函数。

lea     eax, [ebp+var_24]          ; 取 var_24 的地址,存真实密码用
mov     [esp], eax                 ; 作为第一个参数(返回值容器)压栈
call    __Z17getSecretPasswordv    ; 调用 getSecretPassword()

lea     eax, [ebp+var_24]          ; 取 var_24 的地址(真实密码)
mov     [esp+4], eax               ; 作为第二个参数压栈
lea     eax, [ebp+var_2C]          ; 取 var_2C 的地址(用户输入的密码)
mov     [esp], eax                 ; 作为的第一个参数压栈
call    __ZSteqIc... St11char_traitsIS3_ESaIS3_EESC_              ; 调用 std::operator==,比较两个字符串,这里操作符重载
mov     byte ptr [ebp+lpuexcpt], al ; 将比较结果(true/false)保存到局部变量

cmp     byte ptr [ebp+lpuexcpt], 0 ; 比较结果是否为 false
jz      short loc_4017DB          ; 如果为 false(密码不匹配),跳转到错误处理

到这一步了,同学们有什么想法?是不是鼠标放到getSecretPassword这个函数,进去看看。

mov     dword ptr [esp], 32h ; '2'
mov     ecx, [ebp+arg_0]     ; 取 std::string 对象的地址(this 指针)
call    __ZNSspLEc          ; 调用 string::operator+=(char),追加字符 '2'

mov     dword ptr [esp], 30h ; '0'
mov     ecx, [ebp+arg_0]
call    __ZNSspLEc          ; 追加字符 '0'

mov     dword ptr [esp], 32h ; '2'
mov     ecx, [ebp+arg_0]
call    __ZNSspLEc          ; 追加字符 '2'

mov     dword ptr [esp], 34h ; '4'
mov     ecx, [ebp+arg_0]
call    __ZNSspLEc          ; 追加字符 '4'

jmp     short loc_401684    ; 跳转到清理和返回代码

现在看到的就是 getSecretPassword() 函数的核心。它做的事情非常简单,就是把 '2'、'0'、'2'、'4' 这四个字符依次追加到一个空字符串的末尾。也就是说真正的密码就是2024。

同学们再思考一下:那如果是比较复杂的函数,我们看不懂里面的代码怎么办呢?有没有办法直接在函数调用后获取到真实的密码呢?

IDA除了静态分析还具有动态调试功能,在菜单栏选择window调试器,在汇编代码选择call  Z17getSecretPasswordv这一行,F2打上断点,断点的意思是程序运行到这里就会停下来,可以让我们看到此刻运行中的程序内存情况。而我们指令和数据都在内存中,通过观察内存,就能拿到我们想要的数据,而不需要去阅读里面的实现代码。

运行之后,停在call这一行,再按一下F8单步步过,步过的意思就是不进入函数内部(如果想要进入函数内部看就是按F7单步步入),左上方是反汇编窗口同学们已经见过,右上方是寄存器窗口,左下方是数据窗口,右下方是堆栈窗口。

text:0040174E call    __Z17getSecretPasswordv         ; getSecretPassword(void)
text:00401753 lea     eax, [ebp+var_24]
text:00401756 mov     [esp+4], eax

此时ebp+var_24值是0028FE64,这怎么不是2024呢?因为lea是传地址,mov 带中括号是取地址里面的值,mov后面不带中括号才是传地址,那么我们在hex view窗口按g键输入0028FE64,看到8C 12 35 00,在 x86 电脑上,数据是以小端序存储的,所以这是0x35128c,这也是不是2024。

模式 规则 例子(存储 0x0035128C)
大端序(网络字节序) 高位字节在低地址(和书写顺序一样) 00 35 12 8C(从左到右读)
小端序(x86/x64 默认) 低位字节在低地址(字节顺序反过来) 8C 12 35 00(从右到左读)

同学们,指针并不会只是一级指针,它可能是多级指针,怎么判断是数据还是地址呢?像这种无意义地址和堆栈地址接近的就可能是地址,数据一般都是ASCII码,在数据窗口能看到一串有意义的字符,实在判断不了,还是在数据窗口按g,输入0x35128c这个值,看看能不能跳转,再看数据窗口,出来了2024,从而找到了正确的密码。

00351280  04 00 00 00 04 00 00 00  00 00 00 00 32 30 32 34  ............2024

第三部分:攻击手法二:命令注入,获取系统权限

同学们,输入2024,登录成功后,程序允许我们执行 ping 命令。

输入 127.0.0.1 时,正常执行:

ping -n 4 127.0.0.1

现在输入 127.0.0.1 & whoami:

ping -n 4 127.0.0.1 & whoami

不好,老师翻车了,该死的程序为什么会无限循环,逻辑没问题啊,那么为什么正常输入ip程序可以正常执行,发动攻击后程序无限循环了?我们找到比较前的cin函数,F2下上断点。

lea     eax, [ebp+var_C]          ; 取 var_C 的地址(这就是 choice 变量的地址)
mov     [esp], eax                ; 将这个地址作为参数压栈
mov     ecx, offset __ZSt3cin     ; 将 cin 对象的地址存入 ecx(this 指针)
call    __ZNSirsERi               ; 调用 std::istream::operator>>(int &)
sub     esp, 4                    ; 恢复栈平衡
mov     eax, [ebp+var_C]          ; 读取用户输入的值到 eax,用于后面if比较

F8一直单步步过往前走,正常选择1,输入127.0.0.1 & whoami,继续F8走完一个循环,没什么问题,再来第二个循环,我们发现程序没有暂停接受我的输入,而是直接取了0,为什么?看看控制台。

请选择功能:
1. Ping IP地址
2. 退出
请输入选项: 1
请输入要ping的IP地址: 127.0.0.1 & whoami
[执行命令] ping -n 4 127.0.0.1

可以看到第一次攻击命令执行后 & whoami并没有执行,我们看代码:int choice(choice只能是数字),cin >> choice 有几种停止方式:正常读取完一个数字后遇到空格或换行就停;如果读到的不是数字,就会进入错误状态,之后所有的 cin >> 操作都会直接跳过,choice 变成0。

这里我们修改一下,让127.0.0.1 & whoami被完整读取,修改程序代码,使用geline函数保证读一整行,cin >> choice这后面加一行cin.ignore();丢掉一个字符也就是回车符,防止后续geline脏读:

getline(cin,ip);  //读字符串时使用getline读一整行。

cin >> choice
cin.ignore();                // 丢弃那个 '\n'

编译运行后,重新进行127.0.0.1 & whoami攻击

请输入选项: 1
请输入要ping的IP地址: 127.0.0.1 & whoami
[执行命令] ping -n 4 127.0.0.1 & whoami

正在 Ping 127.0.0.1 具有 32 字节的数据:
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64

127.0.0.1 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
lenovo-pc\lenovo

程序不仅执行了ping,还执行了 whoami,显示了当前登录的用户名,这里同学们可以替换成其他的命令试试。

system() 会把整个字符串交给 cmd.exe 解析,&、|、&& 等符号都会被解释为命令连接符。这就是命令注入的根源。

第四部分:总结——逆向分析与漏洞利用的完整链条

同学们,今天我们演示了一条完整的攻击链,从分析汇编代码,找出真实密码,到实现命令注入,只用了三步:

步骤 操作 技术手段 关键突破点
第一步 找到隐藏的密码 IDA静态分析 + 数据流追踪 GBK编码被还原,密码被拼接
第二步 登录成功 动态调试 + 内存查看 多级指针追踪,抓取明文密码
第三步 获取系统权限 命令注入 system() + & 命令连接符

IDA中函数重命名(Name Mangling)解析,除了Z5loginv,还有Z9decodeGBKPKh = decodeGBK(const unsigned char*),相关释义如下:

符号模式 含义 示例
__Z + 数字 + 函数名 + 参数缩写 C++ 函数名改编 __Z5loginv = login(void)
数字 表示函数名长度 5 = 函数名有5个字符
v void 无参数
Ss std::string 字符串参数
PKh const unsigned char* 常量无符号字符指针

最后,再补充一下这节课相关的C++ 函数名速查和动态调试实战常用快捷键

IDA中的符号 C++ 原始代码 含义
__Z5loginv login(void) 登录函数
__Z9decodeGBKPKh decodeGBK(const unsigned char*) GBK解码函数
__Z17getSecretPasswordv getSecretPassword(void) 获取密码函数
__ZSteqIc... std::operator== 字符串比较操作符
__ZStpl std::operator+ 字符串拼接操作符
__ZStls std::operator<< 流输出操作符
__ZNSspLEc std::string::operator+=(char) 字符串追加字符
__ZNKSs5c_strEv std::string::c_str(void) 获取C风格字符串指针
_system system() 执行系统命令
快捷键 功能
F2 设置/取消断点(IDA/OD通用)
F7 单步步入(进入call内部)
F8 单步步过(不进入call内部)
F9 继续运行程序
G 跳转到指定地址(IDA中)
空格 切换图形/文本视图(IDA中)
Shift+F12 打开字符串窗口(IDA中)
Ctrl+F 搜索当前窗口

最后给同学们留2个作业,编程方面就把GBK编码两个16进制互换位置,用的时候再换回来再解析,不然x32 dbg和OD工具有可能直接解码识别出来GBK编码的,逆向作业还是修改跳转和程序密码,自由发挥即可。

课程笔记和视频:c++控制台模拟web中的命令注入漏洞
链接: https://pan.baidu.com/s/1ZY_KAXmUSe6yUtb85ul7Lw?pwd=nv16 提取码: nv16

免费评分

参与人数 3吾爱币 +3 热心值 +3 收起 理由
lsb2pojie + 1 + 1 热心回复!
laozhang4201 + 1 + 1 热心回复!
congcongzhidao + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Manson9527 发表于 2026-6-28 09:56
感谢分享
lsb2pojie 发表于 2026-6-29 09:20
 楼主| 小菜鸟一枚 发表于 2026-6-29 15:17
lsb2pojie 发表于 2026-6-30 08:15
小菜鸟一枚 发表于 2026-6-29 15:17
结尾不就是视频吗???

是,网盘我没点击进去,我还以为网页支持视频了!
wjbedu 发表于 2026-7-2 09:26

感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-3 06:54

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表