$ file 04_angr_symbolic_stack
04_angr_symbolic_stack: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=dbb593c5082a34aa879daf76e62b081b0676d71b, not stripped
main 함수
int __cdecl main(int argc, const char **argv, const char **envp)
{
printf("Enter the password: ");
handle_user();
return 0;
}
handler_user() 함수
int handle_user()
{
int result; // eax
int v1; // [esp+8h] [ebp-10h]
int v2; // [esp+Ch] [ebp-Ch]
__isoc99_scanf("%u %u", &v2, &v1);
v2 = complex_function0(v2);
v1 = complex_function1(v1);
if ( v2 == 0x7C315173 && v1 == 0xFA12140A )
result = puts("Good Job.");
else
result = puts("Try again.");
return result;
}
scanf로 v1, v2에 입력을 받는다. angr는 scanf에서의 multiple 입력을 처리하지 못 하므로 이번에도 직접 register에 값을 세팅해줘야 한다.
이후 입력값으로 complex_function0,1을 호출해서 리턴값을 각각 v1, v2에 저장 후 if 조건에 맞으면 Good ~ 을 출력해주고 맞지 않으면 Try ~ 를 출력해준다.
.text:08048690 handle_user proc near ; CODE XREF: main+21↓p
.text:08048690
.text:08048690 var_10 = dword ptr -10h
.text:08048690 var_C = dword ptr -0Ch
.text:08048690
.text:08048690 ; __unwind {
.text:08048690 push ebp
.text:08048691 mov ebp, esp
.text:08048693 sub esp, 18h
.text:08048696 sub esp, 4
.text:08048699 lea eax, [ebp+var_10]
.text:0804869C push eax
.text:0804869D lea eax, [ebp+var_C]
.text:080486A0 push eax
.text:080486A1 push offset aUU ; "%u %u"
.text:080486A6 call ___isoc99_scanf
.text:080486AB add esp, 10h
.text:080486AE mov eax, [ebp+var_C]
0x080486AB 주소로 준다.
scanf(”%u %u”) 대신 값을 넣어주기 위해, bitvectors를 바이너리에 넣어준다.
일단 두 변수가 int 형이므로 32bit 크기의 ‘input0’, ‘input1’ 이름의 bitvector를 만든다.
state.solver.BVS('input0', 32)
state.solver.BVS('input1', 32)
.text:08048690 public handle_user
.text:08048690 handle_user proc near ; CODE XREF: main+21↓p
.text:08048690
.text:08048690 var_10 = dword ptr -10h
.text:08048690 var_C = dword ptr -0Ch
.text:08048690
.text:08048690 ; __unwind {
.text:08048690 push ebp
.text:08048691 mov ebp, esp
.text:08048693 sub esp, 18h
.text:08048696 sub esp, 4
.text:08048699 lea eax, [ebp+var_10]
.text:0804869C push eax
.text:0804869D lea eax, [ebp+var_C]
.text:080486A0 push eax
.text:080486A1 push offset aUU ; "%u %u"
.text:080486A6 call ___isoc99_scanf
.text:080486AB add esp, 10h
.text:080486AE mov eax, [ebp+var_C]
.text:080486B1 sub esp, 0Ch
.text:080486B4 push eax
.text:080486B5 call complex_function0
.text:080486BA add esp, 10h
.text:080486BD mov [ebp+var_C], eax
.text:080486C0 mov eax, [ebp+var_10]
.text:080486C3 sub esp, 0Ch
.text:080486C6 push eax
.text:080486C7 call complex_function1
.text:080486CC add esp, 10h
.text:080486CF mov [ebp+var_10], eax
.text:080486D2 mov eax, [ebp+var_C]
.text:080486D5 cmp eax, 7C315173h
.text:080486DA jnz short loc_80486E6
.text:080486DC mov eax, [ebp+var_10]
.text:080486DF cmp eax, 0FA12140Ah
.text:080486E4 jz short loc_80486F8
.text:080486E6
.text:080486E6 loc_80486E6: ; CODE XREF: handle_user+4A↑j
.text:080486E6 sub esp, 0Ch
.text:080486E9 push offset s ; "Try again."
.text:080486EE call _puts
.text:080486F3 add esp, 10h
.text:080486F6 jmp short loc_8048709
.text:080486F8 ; ---------------------------------------------------------------------------
.text:080486F8
.text:080486F8 loc_80486F8: ; CODE XREF: handle_user+54↑j
.text:080486F8 sub esp, 0Ch
.text:080486FB push offset aGoodJob ; "Good Job."
.text:08048700 call _puts
.text:08048705 add esp, 10h
.text:08048708 nop
handler_user 함수에서는 어떤 작업을 하기 전 함수 초기에 함수에서 사용되는 지역변수들을 위한 공간을 stack에 만들어놓는다.
pwndbg> disass handle_user
Dump of assembler code for function handle_user:
0x08048690 <+0>: push ebp
0x08048691 <+1>: mov ebp,esp
0x08048693 <+3>: sub esp,0x18
0x08048696 <+6>: sub esp,0x4
0x08048699 <+9>: lea eax,[ebp-0x10]
0x0804869c <+12>: push eax
0x0804869d <+13>: lea eax,[ebp-0xc]
0x080486a0 <+16>: push eax
esp를 ebp에 넣은 후, ebp-0x10 위치에 하나의 지역변수를 위치시키고, ebp-0xc 위치에 다른 하나의 지역변수를 위치시킨다.
그럼 스택 구조는 다음과 같다.
ebp-0xc는 4byte 이므로 ebp-0x9 부터 ebp-0xc를 차지하고, ebp-0x10도 4byte이므로 ebp-0xd 부터 ebp-0x10까지 차지한다.
그럼 남은 크기는 ebp-0x0 ~ ebp-0x8인 8byte로 총 8byte의 padding이 생긴다.
따라서 두 개의 bitvector를 넣기 전에 8byte의 padding을 stack에 만들어놓는다.
# mov ebp, esp (set ebp to esp register value)
state.regs.ebp = state.regs.esp
# allocate padding by decreasing esp before push bitvectors
padding = 8
state.regs.esp = state.regs.esp - padding
state.stack_push(input0)
state.stack_push(input1)
두 개의 입력에 대한 bitvectors를 스택에 push 한다.
이후부터는 angr에 find로 찾을 주소, avoid로 피할 주소를 주고 explore를 사용하면 가능한 경로를 찾아서 찾으면 found stash에 넣어준다. (이 과정은 앞선 과정들과 동일)
#!/usr/bin/python
import angr, sys
# create angr project
p = angr.Project('./04_angr_symbolic_stack')
# set start state
state = p.factory.blank_state(
addr=0x080486AE,
add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS})
# create bitvectors
input0 = state.solver.BVS('input0', 32)
input1 = state.solver.BVS('input1', 32)
# mov ebp, esp (set ebp to esp register value)
state.regs.ebp = state.regs.esp
# allocate padding by decreasing esp before push bitvectors
padding = 8
state.regs.esp -= padding
# push bitvectors to stack
state.stack_push(input0)
state.stack_push(input1)
# create simulation manager
simgr = p.factory.simgr(state)
# search possible path with given address
simgr.explore(find=lambda s: 'Good'.encode() in s.posix.dumps(sys.stdout.fileno()), avoid=lambda s: 'Try'.encode() in s.posix.dumps(sys.stdout.fileno()))
if simgr.found:
solution_state = simgr.found[0]
print('solution state:', end=' ')
print(solution_state)
solution0 = solution_state.solver.eval(input0)
solution1 = solution_state.solver.eval(input1)
print("%u %u" % (solution0, solution1))
else:
raise Exception('could not find solution')
전체 코드이다.
angr가 구해준 solution을 프로그램 실행 시 입력으로 넣어주면 “Good Job.”이 출력된다.
[angr_ctf] 06_angr_symbolic_dynamic_memory (0) | 2022.05.14 |
---|---|
[angr_ctf] 05_angr_symbolic_memory (0) | 2022.05.14 |
[angr_ctf] 03_angr_symbolic_registers (0) | 2022.05.14 |
[angr_ctf] 02_angr_find_condition (0) | 2022.05.14 |
[angr_ctf] 01_angr_avoid (0) | 2022.05.14 |