在对电力行业某二次设备进行渗透测试时,通过弱口令telnet服务获取了设备的权限,并提取出了设备固件文件,现在需要从固件中分析出其他默认硬编码密码或厂家后门口令。
- 题目附件: 二次设备固件分析
- flag: 689078
粗略定位
对于固件,我采取的分析方式如下,首先如果是一个bin文件,那么基本是先通过binwalk提取,一般会得到一堆文件,然后进行粗略的观察,主要用到的命令如下:
binwalk -Me xxx.binwalk # binwalk解压
tree -N -L 3 --sort=name # 列目录
find . -name "*" | xargs file # 全局观察文件类型
find . -name "*" | xargs strings -f | grep -i pass # 全局搜索敏感字符串
strings -f * | grep -i pass # 单个目录下搜索敏感字符串
这道题目的固件,分析目录可知:
➜ tree -N -L 1 --sort=name
.
├── Drive # 驱动
├── FuncDll # 动态链接库
├── Icon # 图标和配置文件
├── NandFlash # 存储
├── Process # 运行的程序代码
└── lost+found # 空
分析文件类型可以了解到无论是驱动,动态链接库,还是程序代码都是ARM32位的ELF文件,继续搜索敏感字符串:
pass
password
passwd
pwd
有如下结果:
./Process/JZPHMISystem: inputPassword
./Process/JZPHMISystem: passWd.c
./Process/JZPHMISystem: rootPasswd
./Process/JZPHMISystem: passWdPID
./Process/JZPHMISystem: inputPassword
./Process/PHMISystem: inputPassword
./Process/PHMISystem: passWd.c
./Process/PHMISystem: rootPasswd
./Process/PHMISystem: passWdPID
./Process/PHMISystem: inputPassword
./Process/JZPHMISystem: InputPwd_pro
./Process/JZPHMISystem: InputPwdPro
./Process/PComManager: AT$MYNETCON=0,"USERPWD","[USER],[PWD]"
./Process/PComManager: AT$MYNETCON=0,"USERPWD","%s,%s"
./Process/PHMISystem: InputPwd_pro
./Process/PHMISystem: InputPwdPro
./Process/PACSample: CheckPwd
./Process/PACSample: DL64507ChangePwd
./Process/PACSample: AC07ChangePwd
./FuncDll/libEsam.so: GW376_PWD_F11
./FuncDll/libEsam.so: GW376_PWD_F12
./FuncDll/libEsam.so: GW376_PWD_F13
./FuncDll/libEsam.so: GW376_PWD_F14
./FuncDll/libEsam.so: GW376_PWD_F15
./FuncDll/libEsam.so: GW376_PWD_F16
./FuncDll/libEsam.so: GW376_PWD_F17
./FuncDll/libEsam.so: GW376_PWD_F18
./FuncDll/libEsam.so: GW376_PWD_F19
./FuncDll/libEsam.so: GW376_PWD_F20
./FuncDll/libEsam.so: GW376_PWD_F21
./FuncDll/libEsam.so: GW376_PWD_F12
./FuncDll/libEsam.so: GW376_PWD_F16
./FuncDll/libEsam.so: GW376_PWD_F15
./FuncDll/libEsam.so: GW376_PWD_F11
./FuncDll/libEsam.so: GW376_PWD_F19
./FuncDll/libEsam.so: GW376_PWD_F18
./FuncDll/libEsam.so: GW376_PWD_F14
./FuncDll/libEsam.so: GW376_PWD_F21
./FuncDll/libEsam.so: GW376_PWD_F13
./FuncDll/libEsam.so: GW376_PWD_F17
./FuncDll/libEsam.so: GW376_PWD_F20
./FuncDll/libGW376ProtProc.so: GW376PWDefine
./FuncDll/libGW376ProtProc.so: GW376PWDEFINE
./FuncDll/libGW376ProtProc.so: PWDefineProcess
./FuncDll/libGW376ProtProc.so: GW376PWDefine.c
./FuncDll/libGW376ProtProc.so: PWDefine11
./FuncDll/libGW376ProtProc.so: PWDefine12
./FuncDll/libGW376ProtProc.so: PWDefine13
./FuncDll/libGW376ProtProc.so: PWDefine14
./FuncDll/libGW376ProtProc.so: PWDefine15
./FuncDll/libGW376ProtProc.so: PWDefine16
./FuncDll/libGW376ProtProc.so: PWDefine17
./FuncDll/libGW376ProtProc.so: PWDefine18
./FuncDll/libGW376ProtProc.so: PWDefine19
./FuncDll/libGW376ProtProc.so: PWDefine20
./FuncDll/libGW376ProtProc.so: PWDefine21
./FuncDll/libGW376ProtProc.so: GW376PWDefine
./FuncDll/libGW376ProtProc.so: GW376PWDEFINE
./FuncDll/libGW376ProtProc.so: PWDefineProcess
对于固件的硬编码一般应该不太可能出现在动态链接库里,所以这里我们主要关注Process目录下面的文件:JZPHMISystem,PHMISystem,PComManager,PACSample
首先可看到JZPHMISystem与PHMISystem,出现的东西基本是重复的,而且第一个只是开头多了JZ两个字符,根据Icon目录下的配置文件,可以猜到应该是“集中”的意思,所以这俩任意分析一个就行了。但是我这个直觉鬼才,真的每次都是试到最后一个才找到答案,我第一天看到PComManager就觉得很可疑,因为题目让找后门密码么,我就觉得类似串口登录那种,所以盯着COM就不放了。结果,,,还是找到点东西的,一会再说。还有就是我觉得后门密码,是不是过了怎么得给个shell吧,所以我又搜了一下”/bin/sh”结果如下:
./FuncDll/libDataDict.so: /bin/sh
./FuncDll/libGW376ProtProc.so: /bin/sh
./Process/PComManager: /bin/sh
./Process/PPLCCom: /bin/sh
./Process/startRcS: #! /bin/sh
./Process/FileAnalysis: /bin/sh
./Process/InitDDRunEnv: /bin/sh
但是基本每个里面我都去找了,执行的都是写死的命令,没有弹给用户的shell。比如PComManager的“/bin/sh”。在IDA里通过bin/sh的交叉引用(快捷键X)可以找到ExecuteShell函数,继续交叉引用,可以发现四处调用了该函数,但都在SetIPPara这一个函数中。
signed int SetIPPara()
{
char s; // [sp+28h] [bp-6Ch]
char dest; // [sp+68h] [bp-2Ch]
unsigned __int8 *v3; // [sp+6Ch] [bp-28h]
v3 = (unsigned __int8 *)&g_strSysRunData;
memcpy(&dest, &C_2_8361, 4u);
strcpy(&s, "ifconfig eth0 down");
ExecuteShell((int)&s);
sleep(2u);
puts(&s);
sprintf(&s, "ifconfig eth0 hw ether %02X:%02X:%02X:%02X:%02X:%02X", v3[38], v3[39], v3[40], v3[41], v3[42], v3[43]);
ExecuteShell((int)&s);
puts(&s);
if ( !memcmp(v3 + 54, &dest, 4u) || !memcmp(v3 + 62, &dest, 4u) )
strcpy(&s, "ifconfig eth0 192.168.1.233 broadcast 192.168.1.2 netmask 255.255.255.0");
else
sprintf(
&s,
"ifconfig eth0 %d.%d.%d.%d broadcast %d.%d.%d.%d netmask %d.%d.%d.%d",
v3[54],
v3[55],
v3[56],
v3[57],
v3[62],
v3[63],
v3[64],
v3[65],
v3[58],
v3[59],
v3[60],
v3[61]);
ExecuteShell((int)&s);
puts(&s);
strcpy(&s, "#ifconfig eth0 up");
ExecuteShell((int)&s);
puts(&s);
sleep(2u);
return 1;
}
分析可以看到,这些地方都不是给用户弹shell的,但我对串口还是不死心,我决定去搜一下“tty”,虽然我还不知道这玩意怎么串口连上的,结果如下:
./Process/PComManager: isatty
./Process/PComManager: /dev/ttyS5
./Process/PComManager: /dev/ttyS0
./Process/PComManager: GetModemInstantType
./Process/PComManager: isatty@@GLIBC_2.4
./Process/PPLCCom: isatty
./Process/PPLCCom: /dev/ttyS6
./Process/PPLCCom: isatty@@GLIBC_2.4
./Process/PCollectData: /dev/ttyS6
./Process/PControl: isatty
./Process/PControl: /dev/ttyS6
./Process/PControl: isatty@@GLIBC_2.4
./Process/PACSample: isatty
./Process/PACSample: /dev/ttyS3
./Process/PACSample: /dev/ttyS2
./Process/PACSample: /dev/ttyS3
./Process/PACSample: GetTypeFromDI
./Process/PACSample: isatty@@GLIBC_2.4
猜测有的tty是对于其他设备的控制终端,我并不是很懂这些东西,翻了翻没有什么价值。所以我们还是重点关注JZPHMISystem,PComManager,PACSample这三个Process
分析PComManager
回到之前找到pass关键字的地方,在PComManager也找到了类似密码的东西:
AT$MYNETCON=0,"USERPWD","%s,%s"
找到引用这个字符串位置:
signed int GprsDialup()
{
...
switch ( i )
{
case 9u:
if ( byte_1C932 && byte_1C953 )
sprintf(&dest, "AT$MYNETCON=0,\"USERPWD\",\"%s,%s\"\r\n", &byte_1C932, &byte_1C953);
...
可见byte_1C953这个位置就是密码,点进去发现是个.bss段,那一定会有代码对这个位置初始化,于是想到交叉引用,但是发现引用这个位置的代码,只有以上两行,一个if判断,一个sprintf。于是很懵,那这个位置是怎么初始化的呢?于是我在IDA的ida-view窗口往上翻那个bss段:
.bss:0001C8F0 g_strSysRunData % 1 ; DATA XREF: LOAD:00008600↑o
.bss:0001C8F0 ; LoadCommPara+38↑o ...
.bss:0001C8F1 % 1
.bss:0001C8F2 % 1
.bss:0001C8F3 % 1
.bss:0001C8F4 % 1
.bss:0001C8F5 % 1
.bss:0001C8F6 % 1
.bss:0001C8F7 % 1
.bss:0001C8F8 % 1
.bss:0001C8F9 % 1
.bss:0001C8FA byte_1C8FA % 1 ; DATA XREF: GetLinkState+98↑w
.bss:0001C8FB byte_1C8FB % 1 ; DATA XREF: LoginDeal_0+20↑r
.bss:0001C8FC % 1
.bss:0001C8FD % 1
.bss:0001C8FE % 1
.bss:0001C8FF % 1
.bss:0001C900 % 1
.bss:0001C901 % 1
.bss:0001C902 % 1
.bss:0001C903 % 1
.bss:0001C904 % 1
.bss:0001C905 byte_1C905 % 1 ; DATA XREF: GprsDialup+160↑o
.bss:0001C905 ; GprsDialup+164↑r ...
.bss:0001C906 % 1
.bss:0001C907 % 1
.bss:0001C908 % 1
.bss:0001C909 % 1
.bss:0001C90A % 1
.bss:0001C90B % 1
.bss:0001C90C % 1
.bss:0001C90D % 1
.bss:0001C90E % 1
.bss:0001C90F % 1
.bss:0001C910 % 1
.bss:0001C911 % 1
.bss:0001C912 % 1
.bss:0001C913 % 1
.bss:0001C914 % 1
.bss:0001C915 % 1
.bss:0001C916 % 1
.bss:0001C917 % 1
.bss:0001C918 % 1
.bss:0001C919 % 1
.bss:0001C91A % 1
.bss:0001C91B % 1
.bss:0001C91C % 1
.bss:0001C91D % 1
.bss:0001C91E % 1
.bss:0001C91F % 1
.bss:0001C920 % 1
.bss:0001C921 % 1
.bss:0001C922 % 1
.bss:0001C923 % 1
.bss:0001C924 % 1
.bss:0001C925 % 1
.bss:0001C926 % 1
.bss:0001C927 % 1
.bss:0001C928 % 1
.bss:0001C929 % 1
.bss:0001C92A % 1
.bss:0001C92B % 1
.bss:0001C92C % 1
.bss:0001C92D % 1
.bss:0001C92E % 1
.bss:0001C92F % 1
.bss:0001C930 % 1
.bss:0001C931 % 1
.bss:0001C932 byte_1C932 % 1 ; DATA XREF: GprsDialup+100↑o
.bss:0001C932 ; GprsDialup+104↑r ...
.bss:0001C933 % 1
.bss:0001C934 % 1
.bss:0001C935 % 1
.bss:0001C936 % 1
.bss:0001C937 % 1
.bss:0001C938 % 1
.bss:0001C939 % 1
.bss:0001C93A % 1
.bss:0001C93B % 1
.bss:0001C93C % 1
.bss:0001C93D % 1
.bss:0001C93E % 1
.bss:0001C93F % 1
.bss:0001C940 % 1
.bss:0001C941 % 1
.bss:0001C942 % 1
.bss:0001C943 % 1
.bss:0001C944 % 1
.bss:0001C945 % 1
.bss:0001C946 % 1
.bss:0001C947 % 1
.bss:0001C948 % 1
.bss:0001C949 % 1
.bss:0001C94A % 1
.bss:0001C94B % 1
.bss:0001C94C % 1
.bss:0001C94D % 1
.bss:0001C94E % 1
.bss:0001C94F % 1
.bss:0001C950 % 1
.bss:0001C951 % 1
.bss:0001C952 % 1
.bss:0001C953 byte_1C953 % 1 ; DATA XREF: GprsDialup+110↑o
.bss:0001C953 ; GprsDialup+114↑r ...
.bss:0001C954 % 1
.bss:0001C955 % 1
.bss:0001C956 % 1
.bss:0001C957 % 1
.bss:0001C958 % 1
.bss:0001C959 % 1
.bss:0001C95A % 1
.bss:0001C95B % 1
.bss:0001C95C % 1
.bss:0001C95D % 1
发现了一个符号:g_strSysRunData,那有没有可能对于1C932,1C953这两个位置的初始化,是通过g_strSysRunData加上偏移在代码中进行的运算呢?那我们先计算一下偏移吧!g_strSysRunData的地址为0001C8F0:
- offset_1C932 = 0x1C932 - 0x1C8F0 = 0x42
- offset_1C953 = 0x1C953 - 0x1C8F0 = 0x63
算完偏移后,我查看g_strSysRunData这个位置的交叉引用发现有一堆,感觉LoadCommPara有点像设定参数的函数,进入如下:
signed int LoadCommPara()
{
void *v0; // r4
size_t v1; // r0
void *v2; // r4
size_t v3; // r0
void *v4; // r4
size_t v5; // r0
__int16 v6; // r0
__int16 v7; // r0
__int16 v8; // r0
char s; // [sp+10h] [bp-ECh]
char v11; // [sp+16h] [bp-E6h]
char v12; // [sp+1Ah] [bp-E2h]
char v13; // [sp+1Eh] [bp-DEh]
char src; // [sp+22h] [bp-DAh]
char dest; // [sp+26h] [bp-D6h]
char v16; // [sp+2Ch] [bp-D0h]
int v17; // [sp+ACh] [bp-50h]
int v18; // [sp+B0h] [bp-4Ch]
int v19; // [sp+B4h] [bp-48h]
int v20; // [sp+B8h] [bp-44h]
int v21; // [sp+BCh] [bp-40h]
int v22; // [sp+C0h] [bp-3Ch]
int v23; // [sp+C4h] [bp-38h]
int v24; // [sp+C8h] [bp-34h]
int v25; // [sp+CCh] [bp-30h]
int v26; // [sp+D0h] [bp-2Ch]
int v27; // [sp+D4h] [bp-28h]
int v28; // [sp+D8h] [bp-24h]
int v29; // [sp+DCh] [bp-20h]
int v30; // [sp+E0h] [bp-1Ch]
int v31; // [sp+E4h] [bp-18h]
char *v32; // [sp+E8h] [bp-14h]
void *v33; // [sp+ECh] [bp-10h]
v17 = 285212676;
v18 = 268435466;
v19 = 285212683;
v20 = 268435468;
v21 = 285212685;
v22 = 268435470;
v23 = 268435471;
v24 = 285212692;
v25 = 268435477;
v26 = 285212694;
v27 = 285212695;
v28 = 268435487;
v29 = 268435486;
v30 = 268435493;
v31 = 268435494;
v32 = (char *)&g_strSysRunData;
memcpy(&dest, &C_1_8268, 6u);
memcpy(&src, &C_2_8269, 4u);
memcpy(&v13, &C_3_8270, 4u);
memcpy(&v12, &C_4_8271, 4u);
memcpy(&v11, &C_5_8272, 4u);
strcpy(&s, "CMNET");
v33 = &v16;
if ( (*(int (__fastcall **)(char *, signed int, _DWORD, _DWORD, int *, signed int, _DWORD))(g_Lib + 36))(
&v16,
128,
0,
0,
&v17,
15,
0) > 0 )
{
memcpy(v32 + 38, v33, 6u);
v33 = (char *)v33 + 6;
v32[132] = *(_BYTE *)v33;
v33 = (char *)v33 + 1;
memcpy(v32 + 13, v33, 4u);
v33 = (char *)v33 + 4;
v6 = (*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33);
*((_WORD *)v32 + 3) = v6;
v33 = (char *)v33 + 2;
memcpy(v32 + 17, v33, 4u);
v33 = (char *)v33 + 4;
v7 = (*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33);
*((_WORD *)v32 + 4) = v7;
v33 = (char *)v33 + 2;
memcpy(v32 + 21, v33, 0x10u);
v32[37] = 0;
v33 = (char *)v33 + 16;
memcpy(v32 + 54, v33, 4u);
v33 = (char *)v33 + 4;
v8 = (*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33);
*((_WORD *)v32 + 2) = v8;
if ( !*((_WORD *)v32 + 2) )
*((_WORD *)v32 + 2) = 6006;
v33 = (char *)v33 + 2;
memcpy(v32 + 0x3A, v33, 4u);
v33 = (char *)v33 + 4;
memcpy(v32 + 62, v33, 4u);
v33 = (char *)v33 + 4;
v32[11] = *(_BYTE *)v33;
v33 = (char *)v33 + 1;
v32[12] = *(_BYTE *)v33;
v33 = (char *)v33 + 1;
memcpy(v32 + 0x42, v33, 0x20u);
v32[98] = 0;
v33 = (char *)v33 + 32;
memcpy(v32 + 0x63, v33, 0x20u);
v32[131] = 0;
v33 = (char *)v33 + 32;
}
else
{
v32[132] = 5;
memcpy(v32 + 38, &dest, 6u);
memcpy(v32 + 13, &src, 4u);
*((_WORD *)v32 + 3) = 8008;
memcpy(v32 + 17, &src, 4u);
*((_WORD *)v32 + 4) = 8008;
v0 = v32 + 21;
v1 = strlen(&s);
memcpy(v0, &s, v1);
memcpy(v32 + 54, &v13, 4u);
*((_WORD *)v32 + 2) = 6006;
memcpy(v32 + 58, &v11, 4u);
memcpy(v32 + 62, &v12, 4u);
v32[11] = 1;
v2 = v32 + 0x42;
v3 = strlen(&s);
memcpy(v2, &s, v3);
v4 = v32 + 0x63;
v5 = strlen(&s);
memcpy(v4, &s, v5);
}
v32[137] = 85;
v32[138] = 85;
return 1;
}
IDAF5代码说明
在B站上找到个视频:IDA教学F5代码和汇编代码应该怎么看,这个up主也是个CTF选手吧,相关视频很多…
上文代码虽然很长,但是不要怕,就借着这个函数说一下IDAF5出来的结果怎么看吧!首先这个结果和开发者编写的C/C++代码是有区别的,有以下特性:
- 会提示函数的调用约定
- 局部变量集中显示在函数开始处,并且给出在栈中或者寄存器的位置
- 有些在源码中的初始化的局部变量,可能会直接以立即数的形式出现在F5的结果中
- 全局变量没有global的声明,在代码中直接引用
- 对于通过函数指针调用的函数会给出函数原型提示
- 对于C++代码中对象和类并没有很好的支持,需要通过一些技巧(比如分析虚表,this指针等)来确定类之间的关系以及成员函数
另外说明的是,IDA加载的ELF或者PE文件,是模拟把这个可执行程序加载到内存中,并不是直接分析这个二进制文件,所以二进制文件有的部分并不会加载到内存,也就不会再IDA的Hex-view中找到,比如符号表这种。但是符号表的信息会在各种窗口中作为提示信息给出,比如函数窗口中的函数名,IDA-view中的注释等。也可以在names窗口中查看已经从符号表中识别出来的符号。
使用技巧
IDA里的很多窗口都是可以根据IDA-View的反汇编窗口进行自动同步定位的,但是对于F5后的结果的Pesudocode窗口,如果在这个窗口中已经跳到了一些其他函数中,希望在IDA-View中也定位到相同的位置,按tab就好了!tab可以在相对应的汇编代码和f5后的c代码切换。
之前我知道的方法是,找到一个插件:LazyIDA,安装方式就是LazyIDA.py放到插件目录里即可,我MAC的目录为:
/Applications/IDA Pro 7.0/ida.app/Contents/MacOS/plugins/LazyIDA.py
复制好之后重启IDA,就可以直接用W快捷键复制当然代码处的地址啦,然后就可以切到IDA-view用G键调过去啦!另外还找到两篇介绍IDA的基本使用的比较细致的文章:
调用约定
在《IDA Pro 权威指南》第6章第2节栈帧部分讲到了调用约定,x86中主要有以下几种(上面的题目是arm的):
__cdel
:函数调用者清理调用函数时压栈的参数__stdcall
:被调用者通过retn x的方式清理压栈的参数__fastcall
:被调用者清理清栈,且前两个参数保存在ecx和edx中,不压栈__thiscall
:C++的非静态成员函数需要this指针作为第一个参数,和fastcall的区别就是ecx保存this指针- 其他调用约定
可见本函数signed int LoadCommPara()
并没有提示调用约定,因为这个函数压根就没有参数,调用约定还是主要约定参数怎么传,怎么清理的规定。那我们来看几个有调用约定的提示吧!
int __cdecl main(int argc, const char **argv, const char **envp)
// 返回类型是int,调用约定是__cdecl,即调用本函数的函数来清理栈中压得参数
// 函数符号为main
// 三个参数分别是: int型的argc和两个二维数组的指针argv,envp
// argc为参数个数,默认是1
// argv是参数内容,默认第一个参数是文件名,因为每个参数都是一个字符串,所以需要一个二维数组的指针
// argv[0]就是第一个参数(一个字符串)的起始地址,argv[1]就是第二个参数
// envp是环境变量,也是好多字符串,同argv
void __stdcall sub_4032F0(_DWORD *a1, _DWORD *a2)
// 无返回,调用约定是__stdcall,即本函数清理压入栈中的参数,而且a1,a2全部被压入栈中
// 函数符号被删掉了,IDA采用函数的偏移来标记函数:4032F0
// 两个参数都是一个指向一32位数据的指针,故a1,a2总共8个字节(本函数出自一个32位exe)
// 观察IDA-view的汇编代码如下,和我们分析的结果一致
// text:00403328 retn 8
int __fastcall TestFastcall(int a1, int a2, int a3, int a4, int a5)
// 返回类型是int,调约定是__fastcall,32位x86下,a1,a2分别保存在ecx,edx中,a3,a4,a5在栈中,本函数清理栈中的参数
// 函数符号为TestFastcall,参数是5个int
// 看一下调用时的代码与返回的代码如下,与我们的分析一致
.text:08048405 push 5
.text:08048407 push 4
.text:08048409 push 3
.text:0804840B mov edx, 2
.text:08048410 mov ecx, 1
.text:08048415 call TestFastcall
...
.text:080483FF retn 0Ch
// 函数源码如下,编译方式:gcc -m32 test.c
/*
int __attribute__((fastcall)) TestFastcall(int a, int b, int c, int d, int e)
{
return a + b + c + d + e;
}
int main(){
return TestFastcall(1,2,3,4,5);
}
*/
void __thiscall sub_401960(void *this)
// 无返回,调用约定是__thiscall,32位x86下,第一个参数为this指针保存在ecx中,本函数清理栈中的参数
// 函数符号被删掉了,IDA采用函数的偏移来标记函数:sub_401960
// 观察一下调用代码:
.text:00409DCB lea ecx, [ebp+var_14] ; void *
.text:00409DCE call sub_401960
// 可见的确是把一个参数放到ecx中,然后调用的这个函数,但是IDA咋知道的这个参数是this,而不是仅仅fastcall呢?
// 参考IDA Pro权威指南,第8章第7节,IDA猜的!根据就是如果出现了很多次调用一个函数前先把一个地址加载到exc中,那么应该就是thiscall。
int __usercall _do_global_dtors_aux@<eax>(int a1@<ebp>)
// 其他调用约定,不太懂,参考链接https://stackoverflow.com/questions/4823401/hooking-usercall-function
变量位置
计算机程序就是对数据进行运算,那么数据一般以变量或者立即数的形式存储在程序文件中,程序中的变量主要分为全局变量和局部变量,但是加上各种属性比如:是否初始化,是否为静态成员等就可以分为如下几类:
- 非静态未初始化全局变量:bss段
- 静态未初始化全局变量:bss段
- 非静态初始化全局变量:data段
-
静态初始化全局变量:data段
- 非静态未初始化局部变量:栈
- 静态未初始化局部变量:bss段
- 非静态初始化局部变量:栈或者立即数
- 静态初始化局部变量:data段
IDA前缀
因为在程序编译后,有的符号不会保留,比如非静态的局部变量的变量名,或者利用strip工具去除了符号表以后,非动态链接的大部分符号都会被删掉。所以IDA会自动推断数据类型并利用以下的前缀标记一些名称:
- sub_xxxxxx: 地址位于xxxxxx的函数
- loc_xxxxxx: 地址位于xxxxxx的指令
- byte_xxxxxx: 地址位于xxxxxx的8位数据
- word_xxxxxx: 地址位于xxxxxx的16位数据
- dword_xxxxxx: 地址位于xxxxxx的32位数据
- unk_xxxxxx: 地址位于xxxxxx的大小未知的数据
unk_xxxxxx:是因为IDA没推断出来这个数据的类型,有可能是因为在代码中仅仅使用了这个位置的地址作为一个函数参数之类的。unk的意思是unknow,我之前一直以为是unlink….
局部变量与栈
F5后的结果是简化后的结果,如果没有在函数头部提示局部变量并不意味着没有,比如如下:
int __fastcall TestFastcall(int a1, int a2, int a3, int a4, int a5)
{
return a4 + a3 + a2 + a1 + a5;
}
而对应的汇编如下,双击函数开始部分的任意一个var或者arg都可以进入到IDA根据代码计算出的栈视图
.text:080483DB public TestFastcall
.text:080483DB TestFastcall proc near ; CODE XREF: main+13↓p
.text:080483DB
.text:080483DB var_8 = dword ptr -8 ; var为局部变量,8为对ebp的偏移,由于栈是由高地址向低地址增长的,所以局部变量地址在ebp下
.text:080483DB var_4 = dword ptr -4
.text:080483DB arg_0 = dword ptr 8
.text:080483DB arg_4 = dword ptr 0Ch ; arg为参数,但是4并不是相对ebp的偏移,参数在ebp地址之上
.text:080483DB arg_8 = dword ptr 10h ; 但是加上ebp和返回地址本身的8个字节,就是相对ebp的偏移了
.text:080483DB
.text:080483DB ; __unwind {
.text:080483DB push ebp
.text:080483DC mov ebp, esp
.text:080483DE sub esp, 8 ;分配了8个字节的栈空间
.text:080483E1 mov [ebp+var_4], ecx ;把第一个参数放入栈中
.text:080483E4 mov [ebp+var_8], edx ;把第二个参数放入栈中
.text:080483E7 mov edx, [ebp+var_4]
.text:080483EA mov eax, [ebp+var_8]
.text:080483ED add edx, eax
.text:080483EF mov eax, [ebp+arg_0]
.text:080483F2 add edx, eax
.text:080483F4 mov eax, [ebp+arg_4]
.text:080483F7 add edx, eax
.text:080483F9 mov eax, [ebp+arg_8]
.text:080483FC add eax, edx
.text:080483FE leave
.text:080483FF retn 0Ch
栈视图如下
-00000008 var_8 dd ?
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 arg_0 dd ?
+0000000C arg_4 dd ?
+00000010 arg_8 dd ?
可见无论是IDA提供的视图,IDA-VIEW,F5后的结果,这些提供的栈的视图都是栈顶在上,栈低在下,即:
- 上面是低地址,下面是高地址
- esp在上,epb在下
- 数组和字符串填充时,往下填充
函数指针
经常会在F5后的结果中遇到以下这种东西:
(*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33)
(*(void (__fastcall **)(int, int *, int, signed int, signed int (*)()))(dword_1C894 + 168))(v4,&v5,v3,500,DealMessage)
(*(int (__fastcall **)(char *, signed int, _DWORD, _DWORD, int *, signed int, _DWORD))(g_Lib + 36))(&v16,128,0,0,&v17,15,0)
v4 = (int (__fastcall *)(int *, int))dlsym((void *)dword_1C8B0, "GetSysManageFuncList")
貌似很长很复杂,但其实这里面有IDA对于函数的提示,一般是如下格式
(IDA提示的函数原型) 函数名 (参数)
(*(IDA提示的函数原型)(函数指针))(参数)
所以当遇到这种东西时,先把提示的函数原型(一般在F5的结果里呈现为灰色)拆出来,然后分析如下:
(*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33)
- 原型:(int (__fastcall **)(void *)
- 函数:(*(dword_1C898 + 56))(v33)
- 原型解释:返回类型为int,调用约定为__fastcall,__fastcall后面的两个*号表示函数指针的变量需要二次寻址才能找到函数所在的位置,void * 为任意类型参数
- 函数解释:首先将dword_1C898的值加上56作为一个地址,然后这个地址存储的值为函数起始地址,参数为v33
(*(void (__fastcall **)(int, int *, int, signed int, signed int (*)()))(dword_1C894 + 168))(v4,&v5,v3,500,DealMessage)
- 原型:(void (__fastcall **)(int, int *, int, signed int, signed int (*)()))
- 函数:(*(dword_1C894 + 168))(v4,&v5,v3,500,DealMessage)
- 原型解释:返回类型为int,调用约定为__fastcall,__fastcall后面的两个*号表示函数指针的变量需要二次寻址才能找到函数所在的位置,signed int (*)()为一个返回值为有符号整整型无参数的函数指针
- 函数解释:首先将dword_1C894的值加上168作为一个地址,然后这个地址存储的值为函数起始地址,最后一个参数为DealMessage,点进去发现是个函数,所以可以看出IDA对于函数原型的提示是可以嵌套的
(*(int (__fastcall **)(char *, signed int, _DWORD, _DWORD, int *, signed int, _DWORD))(g_Lib + 36))(&v16,128,0,0,&v17,15,0)
- 原型:(int (__fastcall **)(char *, signed int, _DWORD, _DWORD, int *, signed int, _DWORD))
- 函数:(*(g_Lib + 36))(&v16,128,0,0,&v17,15,0)
- 原型解释:返回类型为int,调用约定为__fastcall,__fastcall后面的两个*号表示函数指针的变量需要二次寻址才能找到函数所在的位置
- 函数解释:首先将g_Lib的值加上36作为一个地址,然后这个地址存储的值为函数起始地址,这个函数中的g_Lib符号,是在动态链接的时候设置地址的
v4 = (int (__fastcall *)(int *, int))dlsym((void *)dword_1C8B0, "GetSysManageFuncList")
- 首先这个句并不是通过函数指针调用的函数,但是IDA一样给出了提示,因为这是一个类型提示,v2是函数指针,也可以理解为强制类型转换
- 原型:(int (__fastcall *)(int *, int)),值:dlsym((void *)dword_1C8B0, "GetSysManageFuncList")
- 原型解释:返回类型为int,调用约定为__fastcall,__fastcall后面的一个*号表示函数指针的内容是函数起始地址,后面是两个参数的类型int *和int
- 后续调用时:v4(&dword_1C894, v3),可见v4的确是起到了函数指针的作用,即v4这个变量的内容是函数的起始地址
画了张图如下:
继续分析PComManager
说了这么多我们继续回来分析PComManager,我们关注的是g_strSysRunData+0x42和g_strSysRunData+0x63,那么发现关键的代码如下:
char s; // [sp+10h] [bp-ECh]
char *v32; // [sp+E8h] [bp-14h]
v32 = (char *)&g_strSysRunData;
strcpy(&s, "CMNET");
v33 = &v16;
if((*(int (__fastcall **)(char *, signed int, _DWORD, _DWORD, int *, signed int, _DWORD))(g_Lib + 36))(&v16,128,0,0,&v17,15,0)>0){
v33 = (char *)v33 + 1;
memcpy(v32 + 0x42, v33, 0x20u);
v32[98] = 0;
v33 = (char *)v33 + 32;
memcpy(v32 + 0x63, v33, 0x20u);
}else{
v2 = v32 + 0x42;
v3 = strlen(&s);
memcpy(v2, &s, v3);
v4 = v32 + 0x63;
v5 = strlen(&s);
memcpy(v4, &s, v5);
}
首先可以看到g_strSysRunData的地址赋值给了v32变量,这里强调一下:
- g_strSysRunData是这个符号的地址处的值
- &g_strSysRunData的是这个符号的地址
所以如果走到了else分支,那么这两个位置就都是CMNET,也就是:
sprintf(&dest, "AT$MYNETCON=0,\"USERPWD\",\"%s,%s\"\r\n", &byte_1C932, &byte_1C953);
这个地方的用户名密码了。但如果没有走到else分支,那么将会调用(*(g_Lib + 36))
这个函数,最终影响两个位置的值。一会我们再分析这个函数,先看LoadCommPara函数中另一个地方的函数指针调用,调用的是什么函数呢?
(*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33)
找到这个dword_1C898发现在bss段,而且就在g_Lib下:
.bss:0001C890 g_Lib % 4 ; DATA XREF: LOAD:00008B50↑o
.bss:0001C890 ; LoadCommPara+EC↑o ...
.bss:0001C894 dword_1C894 % 4 ; DATA XREF: ReportDataDeal+30↑r
.bss:0001C894 ; ReportDataDeal+B0↑r ...
.bss:0001C898 dword_1C898 % 4 ; DATA XREF: LoadCommPara+300↑r
.bss:0001C898 ; LoadCommPara+354↑r ...
.bss:0001C89C dword_1C89C % 4 ; DATA XREF: OpenLibAlarmEventList+94↑o
.bss:0001C89C ; .text:off_12160↑o ...
.bss:0001C8A0 dword_1C8A0 % 4 ; DATA XREF: LoginDeal+BC↑r
.bss:0001C8A0 ; HeartDeal+130↑r ...
.bss:0001C8A4 dword_1C8A4 % 4 ; DATA XREF: OpenLibTGDeal+74↑o
.bss:0001C8A4 ; .text:off_123E4↑o ...
.bss:0001C8A8 dword_1C8A8 % 4 ; DATA XREF: OpenLibEsam+74↑o
.bss:0001C8A8 ; .text:off_124FC↑o ...
.bss:0001C8AC EXPORT g_LibHandle
.bss:0001C8AC g_LibHandle % 4 ; DATA XREF: LOAD:00008C60↑o
.bss:0001C8AC ; OpenLibDataDict+24↑o ...
.bss:0001C8B0 dword_1C8B0 % 4 ; DATA XREF: OpenLibSysManage+2C↑w
.bss:0001C8B0 ; OpenLibSysManage+34↑r ...
.bss:0001C8B4 dword_1C8B4 % 4 ; DATA XREF: OpenLibPublic+28↑w
.bss:0001C8B4 ; OpenLibPublic+30↑r ...
.bss:0001C8B8 dword_1C8B8 % 4 ; DATA XREF: OpenLibAlarmEventList+28↑w
.bss:0001C8B8 ; OpenLibAlarmEventList+30↑r ...
.bss:0001C8BC dword_1C8BC % 4 ; DATA XREF: OpenLibGW376Prot+28↑w
.bss:0001C8BC ; OpenLibGW376Prot+30↑r ...
.bss:0001C8C0 dword_1C8C0 % 4 ; DATA XREF: OpenLibTGDeal+28↑w
.bss:0001C8C0 ; OpenLibTGDeal+30↑r ...
.bss:0001C8C4 dword_1C8C4 % 4 ; DATA XREF: OpenLibEsam+28↑w
观察dword_1C898的交叉引用找到函数:OpenLibPublic,关键代码如下:
dword_1C8B4 = (int)dlopen("/home/FuncDll/libPublic.so", 1);
if ( !dword_1C8B4 )
return 0;
v2 = (int (__fastcall *)(int *))dlsym((void *)dword_1C8B4, "GetPublicList");
if ( v2 && v2(&dword_1C898) == 1 )
那就去libPublic.so看一下GetPublicList这个方法吧:
signed int __fastcall GetPublicList(_DWORD *a1)
{
*a1 = &Public_List;
return 1;
}
方法异常的简单,看一下Public_List是什么吧:
.data:0000FB30 Public_List DCD Format03ToInt ; DATA XREF: LOAD:00000510↑o
.data:0000FB30 ; GetPublicList+14↑o ...
.data:0000FB34 DCD Format02ToBin
.data:0000FB38 DCD intToFormat03
.data:0000FB3C DCD U32ToFormat02
.data:0000FB40 DCD ProtocolToStr
.data:0000FB44 DCD GetSystemTimeStamp
.data:0000FB48 DCD GetTtlMinutes
.data:0000FB4C DCD GetTimeStamp
.data:0000FB50 DCD SearchData
.data:0000FB54 DCD GetDate
.data:0000FB58 DCD GetMonthDays
.data:0000FB5C DCD GetDays
.data:0000FB60 DCD GetWeek
.data:0000FB64 DCD JudgeBCD_0
.data:0000FB68 DCD TwoU8ChangeU16
.data:0000FB6C DCD U16ChangeTwoU8
.data:0000FB70 DCD TwoU8ChangeU32
.data:0000FB74 DCD U32ChangeTwoU8
.data:0000FB78 DCD ThreeU8ChangeU32
.data:0000FB7C DCD U32ChangeThreeU8
.data:0000FB80 DCD FourU8ChangeU32
.data:0000FB84 DCD U32ChangeFourU8
.data:0000FB88 DCD BcdInc
.data:0000FB8C DCD BcdToBin_0
.data:0000FB90 DCD BinToBcd_0
.data:0000FB94 DCD TwoBcdToBin
.data:0000FB98 DCD TwoBinToBcd
.data:0000FB9C DCD FourBcdToBin
.data:0000FBA0 DCD FourBinToBcd
.data:0000FBA4 DCD FourBcdInc
.data:0000FBA8 DCD GetBitFlag
.data:0000FBAC DCD SetBitFlag
.data:0000FBB0 DCD ClrBitFlag
.data:0000FBB4 DCD u8GeneralAddition
.data:0000FBB8 DCD CharToU8
.data:0000FBBC DCD TwoCharChangeU8
.data:0000FBC0 DCD U8ChangeTwoChar
.data:0000FBC4 DCD SequenceSwap
.data:0000FBC8 DCD Cal64BitData
.data:0000FBCC DCD PackData
原来如此,所以(dword_1C898 + 56)就是0000FB30+0x38,就是0000FB6C,即TwoU8ChangeU16函数的地址,再次取星号就找到这个函数啦。我们总结一下LoadCommPara中这个函数指针函数的调用:
(*(int (__fastcall **)(void *))(dword_1C898 + 56))(v33)
- dword_1C898这个位置是通过OpenLibPublic函数调用libPublic.so的GetPublicList方法初始化的,值为libPublic.so里一个处于data段地址
- 这个地址是一个函数地址列表的开始地址
- 通过dword_1C898 + 56找到目标函数的地址
- 取星号去调用这函数
这个LoadCommPara方法也就分析到这了,现在回到(*(g_Lib + 36))
这个函数,对于g_Lib这个符号,可以交叉引用找到这个函数:OpenLibDataDict
signed int OpenLibDataDict()
{
int (__fastcall *v2)(int *, int *); // [sp+Ch] [bp-8h]
g_LibHandle = (int)dlopen("/home/FuncDll/libDataDict.so", 1);
if ( !g_LibHandle )
return 0;
v2 = (int (__fastcall *)(int *, int *))dlsym((void *)g_LibHandle, "GetDDOutList");
if ( v2 && !v2(&g_Lib, &g_Lib) )
return 1;
CloseLibDataDict();
return 0;
}
可以看到v2是个函数指针,指向libDataDict.so中的GetDDOutList函数,然后通过调用GetDDOutList函数改变了g_Lib的值,那就去看一下这个函数吧,可以在libDataDict.so中找到:
int __fastcall GetDDOutList(_DWORD *a1, int a2)
{
*a1 = DDOutList;
g_pLib = a2;
g_DDFileLoadFlag = 0;
g_FmtFileLoadFlag = 0;
g_UFmtFileLoadFlag = 0;
pthread_mutex_init((pthread_mutex_t *)&UFmt_mutex, 0);
memset(&g_MDEV_ExistFlag, 0, 0x100u);
return 0;
}
可以看到g_Lib的值就是DDOutList,点进去
.data:0001024C DDOutList DCD DDReadUserDefFile ; DATA XREF: LOAD:00000604↑o
.data:0001024C ; GetDDOutList+18↑o ...
.data:00010250 DCD DDWriteUserDefFile
.data:00010254 DCD DDDeleteUserDefFile
.data:00010258 DCD DDInitMem
.data:0001025C DCD DDSaveMem
.data:00010260 DCD DDHardInit
.data:00010264 DCD DDParaAreaInit
.data:00010268 DCD DDDataAreaInit
.data:0001026C DCD DDGetDataDict
.data:00010270 DCD DDMemDataRead
.data:00010274 DCD DDHisDataRead
.data:00010278 DCD DDMemDataWrite
.data:0001027C DCD DDMpRealToCurDay
.data:00010280 DCD DDMpDayToNand
.data:00010284 DCD DDMpRMDayToNand
.data:00010288 DCD DDDateChangeProc
.data:0001028C DCD DDDeleteMP
.data:00010290 DCD DDGetMDLackItem
.data:00010294 DCD DDSetSupportItem
.data:00010298 DCD DDGetSupportItem
.data:0001029C DCD DDRefreshUserFormat
.data:000102A0 DCD DDSetMPIDMap
.data:000102A4 DCD DDGetMPIDMap
.data:000102A8 DCD DDFormatToStr
.data:000102AC DCD DDUserDefCmd
.data:000102AC ; .data ends
所以(*(g_Lib + 36))
这个函数就是0001024C + 0x24 = 0x10270,就是DDMemDataRead,点进去如下:
signed int __fastcall DDMemDataRead(int a1, int a2, int a3, unsigned __int8 a4, unsigned int *a5, int a6, int a7)
{
size_t v7; // r0
int v9; // [sp+14h] [bp-A8h]
unsigned __int8 v10; // [sp+23h] [bp-99h]
int v11; // [sp+24h] [bp-98h]
int v12; // [sp+28h] [bp-94h]
int v13; // [sp+2Ch] [bp-90h]
unsigned int v14; // [sp+30h] [bp-8Ch]
char s[128]; // [sp+34h] [bp-88h]
int v16; // [sp+B4h] [bp-8h]
v13 = a1;
v12 = a2;
v11 = a3;
v10 = a4;
v16 = 0;
if ( !a1 || !a5 || !a2 || !a6 )
return -1;
if ( a4 == 3 || a4 == 5 || a4 == 6 || a4 == 4 )
v16 = GetSysDateMark();
v14 = *a5;
memset(s, 0, 0x80u);
if ( (unsigned __int8)(v14 >> 12) != 11 && v10 != 4 )
strcpy(s, "/program/RamDisk");
else
strcpy(s, "/home/NandFlash");
v7 = strlen(s);
if ( j_GetFilePathName(&s[v7], (unsigned __int8)(v14 >> 12), v10, v11, v16, 0) == -1 )
v9 = -1;
else
v9 = j_DataReadProc(v13, v12, s, v11, a5, a6, a7);
return v9;
}
可以看到应该大概是去内存中找东西,所以回到LoadCommPara函数,如果能去内存里读到东西,就按照内存中的配置设置用户名密码,否则就是CMNET。
大概就是分析出了一个硬编码的用户名和密码:CMNET,CMNET。提交了一下并不是flag…
疑问
这里出现了个我现在还无法解释的东西:在LoadCommPara函数中有如下:
v32 = (char *)&g_strSysRunData;
这里我们解释把g_strSysRunData这符号的地址给v32,但是那在OpenLibDataDict函数中:
v2(&g_Lib, &g_Lib);
这里解释为传递的参数是g_Lib的地址,那么在GetDDOutList函数中:
int __fastcall GetDDOutList(_DWORD *a1, int a2)
{
*a1 = DDOutList;
a1是一个_DWORD的指针,即g_Lib的地址,下面的赋值语句为啥不是:
*a1 = &DDOutList;
我解释一下我的意思,可见对于g_strSysRunData与g_Lib的使用,都是将符号作为该符号位置处的值来使用,比如g_Lib+36,但是为啥在用DDOutList这个符号的时候,直接使用了符号的值传给*a1,即g_Lib的值,DDOutList处的值是 DDReadUserDefFile 这个函数的地址啊。在加上偏移就是函数里面了啊。很奇怪,没搞懂。
分析PACSample
查到的字符串结果:
./Process/PACSample: CheckPwd
./Process/PACSample: DL64507ChangePwd
./Process/PACSample: AC07ChangePwd
找到个函数,没太看懂:
bool __fastcall CheckPwd(char a1, int a2, char a3)
{
_BOOL4 v4; // [sp+0h] [bp-14h]
if ( a1 != 2 || dword_2F944 != a2 && a2 != 0x550423 )
v4 = a1 == 4 && (dword_2F94C == a2 || a2 == 0x550423) && (a3 == 25 || a3 == 20);
else
v4 = 1;
return v4;
}
为啥就检查了这么点东西,很奇怪,看了半天交叉引用也没看出啥来
分析JZPHMISystem
要不说是直觉鬼才呢,总是分析到最后一个才能找到flag,这个process的字符串结果如下:
./Process/JZPHMISystem: inputPassword
./Process/JZPHMISystem: passWd.c
./Process/JZPHMISystem: rootPasswd
./Process/JZPHMISystem: passWdPID
./Process/JZPHMISystem: inputPassword
./Process/JZPHMISystem: InputPwd_pro
./Process/JZPHMISystem: InputPwdPro
找到了inputPassword和InputPwd_pro函数,InputPwdPro是个bss处的位置,但是就是找不到rootPasswd,passWdPID和passWd.c是啥,搜了hex-view的全局内存也没有,后来才知道这个很可能是符号,在IDA的工具栏里选择view->opensubview->Names,这才找到rootPasswd,如下:
.rodata:00027B67 rootPasswd DCB "689078",0
这个变量在InputPwd_pro函数中被引用:
if ( !strcmp(&s, &v21) || !strcmp("689078", &v21) )
可以看到也是一个密码的硬编码,为正确的flag,经过检查PHMISystem的函数逻辑和这个完全一样。即flag为689078。可见这个硬编码实在输入密码的函数中进行校验的,也算是一个经验了吧。