//
物联网安全技术社区:www.iotsec-zone.com
0、前言
在模拟固件的时候通常需要我们自己对二进制文件进行patch和hook,但无论是patch还是hook,都是通过修改(劫持)二进制文件的执行流程来达到固件顺利启动的目的。本篇文章我会好好的详细说明学会这些技巧的重要性以及如何掌握这些技巧。
1、引入LD_PRELOAD
逻辑很简单 -- 输入密码然后比较密码是否正确:
// gcc -g test1.c -o test1
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void init(){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
}
int main(){
init();
char* password = "cyberangel";
char buffer[0x10] = "";
printf("plz input your password!\n");
read(0,buffer,0x10);
printf("Your input is %s",buffer);
if (!strncmp(password,buffer,strlen(buffer)-1)){ // strlen(buffer)-1 去掉最后的\n
printf("\033[0;32;32mYour password is Correct!\n\033[m");
} else {
printf("\033[0;32;31mYour password is Wrong!\n\033[m");
}
return 0;
}
让我看看谁经常把angel(天使)打成angle(角度)
使用LD_PRELOAD加载如下动态链接库:
// gcc -g -shared -fPIC hook.c -o hook.so
#include <stdlib.h>
#include <stdio.h>
int strncmp(const char *__s1, const char *__s2, size_t __n){
// Dynamic Link Library have't main fucntion
// We want to hook strncmp
// int strncmp(const char *__s1, const char *__s2, size_t __n)
if(getenv("LD_PRELOAD") != NULL){
printf("\033[0;32;32mSuccess hook strncmp\n\033[m");
unsetenv("LD_PRELOAD"); // 必须清除LD_PRELOAD环境变量,否则会陷入hook的死循环。
} else {
printf("\033[0;32;31mFail to hook strncmp!\n\033[m");
}
return 0; // return 0
}
此时无论输入什么都显示的是密码正确,可以使用gdb来看下gdb --args env LD_PRELOAD=$PWD/hook.so ./test1
LD_PRELOAD后gdb无法b main,因此我直接对main的起始地址下断点:b *(0x555555554000+0x91D)
got表也表明此时的strcmp为我们自己的:
Full RELRO
●export LD_PRELOAD=$PWD/hook.so 和 ./test1与LD_PRELOAD=$PWD/hook.so ./test1等价
2、一道开胃小菜(Bin 100)
以2014年的Hack In The Box Amsterdam: Bin 100逆向题作为小菜,hook方法之一的LD_PRELOAD开始在本题中崭露头角:
赋予可执行权限之后尝试运行,如下图所示;可以看到程序似乎一直在打印出英语句子,并且句子的大小写错乱:
在IDA中可以直接看到这些句子的原样,很明显程序在输出时会对句子的部分字母按照一定的规则进行大小写变换:
main函数的第一部分伪代码如下,首先调用qmemcpy函数将加密的数据encode复制到栈上得到encodestring,25到30行是将char randomNum[36]的数组清空:
第二部分如下,这是一个十分重要的循环,它的功能包括:
根据timeSecs2与timeSecs1的差值调用srand生成随机数种子。
保存rand()生成的随机数到randomNum数组。
内层while循环打印变换后的句子。
需要注意到每打印一行句子就要sleep(1),即,要想在人不干预的前提下结束此循环最少需要201527*36 == 7254972秒(程序执行的时间可以忽略不计,大致84天左右吧):
外层的do...while循环结束后会进入到第3部分的代码:
根据程序的流程,最后程序生成的flagchar取决于encodestring和randomNum,encodestring是不会变的,所以唯一的变量为最后一次循环中得到的随机数组randomNum:
对核心代码简化后得到:
count = 201527; timeSecs1 = time(0LL); do { for ( loop = 0LL; loop != 36; ++loop ) { char_loop = 0LL; timeSecs2 = time(0LL); srand(0xDEFACED - timeSecs1 + timeSecs2); // srand(0xDEFACED + timeSecs2 - timeSecs1); tmp = randomNum[loop]; randomNum[loop] = rand() ^ tmp; // rand生成的随机数为伪随机数 // ......(代码省略) sleep(1u); // sleep } --count; } while ( count );
抛开sleep(1)不谈,剩下代码执行所占的时间几乎可以忽略不计,所以timeSecs2 - timeSecs1的差值会以每秒的时间递增1,所以想简单粗暴的直接nop掉sleep()的现在就可以洗洗睡了,否则最终得到的flag肯定是错误的。仔细想想看,既然每一次for循环(每秒)timeSecs2变量递增,那我们能不能在nop的前提下“欺骗”程序已经睡眠1s了?答案是可以的,进一步讲,调用sleep的最终目的是为了timeSecs2的增加而非真的要等待1s。有了这个想法之后我们可以按照如下思路来做。
在动态链接库中重新定义time和sleep这两个函数,通过LD_PRELOAD的预先加载功能达到hook这两个函数的目的,具体操作为每次for循环直接对时间变量timeSecs2自增1并nop掉sleep,从而将程序的休眠时间去掉,所以有如下代码:
// 编译命令:gcc -shared -fPIC hook_time.c -o hook_time.so
static int t = 0; // t变量也就是timeSecs2,该值可以为任意值,因为重点在于时间差(timeSecs2 - timeSecs1)而非某个时刻
void sleep(int sec) {
t += sec; // count = count + sec
} // 每循环一次,count加1
int time() {
return t;
}
// 源码的srand(0xDEFACED + timeSecs2 - timeSecs1);中的timeSecs2 - timeSecs1表示程序已经启动的时间。
printf到最终在屏幕显示所耗费的时间不会拖延程序执行代码的时间,就算是程序在短时间内打印出大量数据,则打印出某一数据时该printf肯定早已完成执行(屏幕的显示与printf的执行并不同步)。
堆在一起打印到屏幕上极费时间,在这里我将程序的输出重定向到tmp文件,以便快速得到结果。LD_PRELOAD=$PWD/hook_time.so ./hitb_bin100.elf > tmp结束后查看flag:
flag:p4ul_1z_d34d_1z_wh4t_th3_r3c0rd_s4ys
3、Cisco RV160W
固件模拟(qemu-system)
使用的固件全称:RV16X_26X-v1.0.01.01-2020-08-17-11-09-01-AM.img,下载链接:
https://software.cisco.com/download/home/286316464/type/282465789/release/1.0.01.01
一条直线表示固件没有加密,直接binwalk 解压binwalk -Me RV16X_26X-v1.0.01.01-2020-08-17-11-09-01-AM.img。文件系统如下图所示:
采用的架构为ARM:
这里我使用qemu-system模拟:
// 虚拟机---------------------------------------------------------------------------- $ sudo tunctl -t tap0 $ sudo ifconfig tap0 192.168.2.1/24 $ sudo qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic // qemu内部------------------------------------------------------------------------- $ ifconfig eth0 192.168.2.2 $ service ssh start // 虚拟机---------------------------------------------------------------------------- $ tar czf rooltfs.tar.gz ./rootfs $ scp ./rootfs.tar.gz root@192.168.2.2:/root/ // qemu内部------------------------------------------------------------------------- $ tar -zxvf ./rootfs.tar.gz $ chmod -R 777 ./rootfs $mount -o bind /dev ./rootfs/dev && mount -t proc /proc ./rootfs/proc $ chroot ./rootfs sh
该路由器的http服务由mini_httpd提供,但是无法直接运行:
IDA交叉引用字符串到sub_145**:
错误原因是setsockopt函数在设置有关套接字的选项时由于参数不合法导致函数返回值小于0,但其实hook掉该函数影响也不是很大,只要保证关键的服务能跑起来就行。安装编译ARM所需要的依赖:
$ sudo apt install libncurses5-dev gcc-arm-linux-gnueabi build-essential synaptic gcc-aarch64-linux-gnu
gcc-arm-linux-gnueabi所采用的是glibc,如下图所示:
而Cisco RV160W则采用的是eglibc:
cp $(which qemu-arm-static) ./sudo chroot . ./qemu-arm-static /lib/libc.so.6
不过没关系,glibc与eglibc编译得到的可执行程序相互兼容,至于我为什么要说到这一点,在稍后的Netgear模拟中会体现到。hook的代码中让setsockopt一直返回true就行:
// 编译命令:arm-linux-gnueabi-gcc -shared -fPIC hook.c -o hook.so #include <sys/socket.h> int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen){ return 1; }
上传qemu:scp ./hook.so root@192.168.2.2:/root/rootfs/
hook mini_httpd,LD_PRELOAD=./hook.so mini_httpd:
它调用了sub_1B5F0导致程序退出:
直接简单粗暴的将BL sub_1B5F0改为NOP:
patch前
patch后
保存导出为mini_httpd_patch,上传qemu:scp ./mini_httpd_patch root@192.168.2.2:/root/rootfs/usr/sbin,备份替换原来的文件、赋予其可执行:
执行:LD_PRELOAD=./hook.so mini_httpd(记得先杀死原来的进程)
界面有点不太正常,没显示全:
回到文件系统寻找原因,搜索mini_httpd字符串grep -r "mini_httpd" ./,得到与之有关的./etc/scripts/mini_httpd/mini_httpd.sh 和./etc/init.d/mini_httpd.init:
尝试启动mini_httpd.init服务:
刷新网页后就正常了:
由于文章字数受限
可点击【阅读原文】阅读全篇。
本文作者:物联网IOT安全
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/185772.html