Gravitational plate of three masses and a slashed discABC0Static engraved plate. Three-dimensional view is unavailable or reduced motion is requested.

โ† back to fieldarticle

articleAug 15, 2020

CodeGate 2017 angrybird writeup

CodeGate 2017 angrybird: patch early exits and canary-related checks, then use angr from 0x4007c2 to recover the 20-byte input that reaches the final printf.

Vulnerability analysis

Running Angry Bird

./0.png
./0.png

The binary prints nothing and exits immediately.

main

./1.png
./1.png

Stack smashing protection is on. The prologue pulls a canary from the TEB into rax and stores it on the stack. Then eax is cleared, compared to zero, and the path jumps to exit. That is why a normal run ends at once.

NOP sled past exit

./2.png
./2.png

Patch the _exit bytes to 90 (NOP) so the jump cannot terminate the process.

./3.png
./3.png

./4.png
./4.png

After that bypass, the binary reaches the string you should return 21 not 1 :( .

you should return 21 not 1 :(

The message says the return value must be 21. Dig into that path next.

sub_4006F6

./5.png
./5.png

Inside the block that printed the message, check dword_606060.

./6.png
./6.png

It holds 1. Patch it to 21.

./7.png
./7.png

The return value is then written with a mov into [rbp+n]. Operand 2 is eax (32-bit), so a mismatched width triggers a segfault.

./8.png
./8.png

./9.png
./9.png

NOP that store out. Do the same for sub_40070C.

./10.png
./10.png

sub_40072A

./11.png
./11.png

This function compares input against "hello" and only continues on a match. 0x606038 turned out to be a GOT slot for __libc_start_main; Ghidra helped clarify which string comparison was happening.

./12.png
./12.png

NOP that check as well.

./13.png
./13.png

Filled solid. ๐Ÿ˜€

./14.png
./14.png

After the patches, fgets finally accepts input.

The long condition chain

./15.png
./15.png

Many conditions sit between input and the final "you typed : %s\n" path.

./16.png
./16.png

Use angr to solve for the input.

Solution

Solved code

import angr
 
def solved():
    p=angr.Project('./angrybird_3', auto_load_libs=False)
		# state๋ฅผ ์ž„์˜๋กœ ์œ„ ํ•จ์ˆ˜๋ฅผ ๋ฌด์‹œํ•œ์ฑ„ ํ•ด๋‹น ์ง€์—ญ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋„๋ก ํ•œ๋‹ค.
    state = p.factory.blank_state(addr=0x4007C2)
 
    sm = p.factory.simgr(state)
    sm.explore(find=0x404fab) # ์ตœํ•˜๋‹จ True ์ฃผ์†Œ ๊ฐ’
 
    flag = sm.found[0].posix.dumps(0)
    print(flag[:20])

Solved code 2

You can also skip the binary patches and let angr start mid-function.

./17.png
./17.png

Ignore sub_4006F6 / sub_40070C, set rbp as if the prologue had run at 0x4007c2, and seed the locals the later checks expect.

./18.png
./18.png

import angr
 
START_ADDR = 0x4007c2
FIND_ADDR = 0x404fab  # This is right before the printf
 
def main():
    proj = angr.Project('./angrybird')
 
    # ํ•ด๋‹น ๋ฐ”์ด๋„ˆ๋ฆฌ์—์„œ ํ•จ์ˆ˜ ๋ถ€๋ถ„์„ ์šฐํšŒ ํ•œ๋‹ค.
    state = proj.factory.entry_state(addr=START_ADDR)
    # ํ•จ์ˆ˜ ํ”„๋กค๋กœ๊ทธ ๋ณ€๊ฒฝ
    state.regs.rbp = state.regs.rsp
    # 0x40์˜ ๊ธธ์ด ๊ฐ’
    state.mem[state.regs.rbp - 0x74].int = 0x40
 
    state.mem[state.regs.rbp - 0x70].long = 0x1000 # strncmp@got
    state.mem[state.regs.rbp - 0x68].long = 0x1008 # puts@got
    state.mem[state.regs.rbp - 0x60].long = 0x1010 # _stack_chk_fail@got
    state.mem[state.regs.rbp - 0x58].long = 0x1018 # _lib_start_main
 
    sm = proj.factory.simulation_manager(state)  # ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒ์„ฑ
    sm.explore(find=FIND_ADDR)
 
    found = sm.found[-1]
    flag = found.posix.dumps(0)
 
    print(flag[:20])    
 
if __name__ == '__main__':
    main()

./19.png
./19.png

related

  1. Aug 16, 2020/articleHITCON 2017 Sakura writeup
  2. Aug 14, 2020/archiveangr binary analysis notes 2: the loader
  3. Sep 18, 2020/articleDEF CON 2016 feedme Vulnerability Analysis

graphfeed